Skip to content

原型模式

1.原型模式介绍

image-20230102101335621

原型模式主要解决的问题就是创建重复对象,而这部分对象内容本身比较复杂,生成过程可能从库或者RPC接口中获取数据的耗时较长,因此采用克隆的方式节省时间。

其实这种场景经常出现在我们的身边,只不过很少用到自己的开发中,就像;

  1. 你经常Ctrl+CCtrl+V,复制粘贴代码。
  2. Java多数类中提供的API方法;Object clone()
  3. 细胞的有丝分裂。

2.案例场景模拟

image-20230102101505449

每个人都经历过考试,从纸制版到上机答题,大大小小也有几百场。而以前坐在教室里答题身边的人都是一套试卷,考试的时候还能偷摸或者别人给发信息抄一抄答案。

但从一部分可以上机考试的内容开始,在保证大家的公平性一样的题目下,开始出现试题混排更有做的好的答案选项也混排。这样大大的增加了抄的成本,也更好的做到了考试的公平性。

但如果这个公平性的考试需求交给你来完成,你会怎么做?

因为需要实现一个上机考试抽题的服务,因此在这里建造一个题库题目的场景类信息,用于创建;选择题问答题

2.1 场景简述

选择题

java
public class ChoiceQuestion {

    private String name;                 // 题目
    private Map<String, String> option;  // 选项;A、B、C、D
    private String key;                  // 答案;B

    public ChoiceQuestion() {
    }

    public ChoiceQuestion(String name, Map<String, String> option, String key) {
        this.name = name;
        this.option = option;
        this.key = key;
    }

    // ...get/set
}
public class ChoiceQuestion {

    private String name;                 // 题目
    private Map<String, String> option;  // 选项;A、B、C、D
    private String key;                  // 答案;B

    public ChoiceQuestion() {
    }

    public ChoiceQuestion(String name, Map<String, String> option, String key) {
        this.name = name;
        this.option = option;
        this.key = key;
    }

    // ...get/set
}

问答题

java
public class AnswerQuestion {

    private String name;  // 问题
    private String key;   // 答案

    public AnswerQuestion() {
    }

    public AnswerQuestion(String name, String key) {
        this.name = name;
        this.key = key;
    }

    // ...get/set
}
public class AnswerQuestion {

    private String name;  // 问题
    private String key;   // 答案

    public AnswerQuestion() {
    }

    public AnswerQuestion(String name, String key) {
        this.name = name;
        this.key = key;
    }

    // ...get/set
}

3.用一坨坨代码实现

在以下的例子中我们会按照每一个用户创建试卷的题目,并返回给调用方。

一把梭实现需求

java
public class QuestionBankController {

    public String createPaper(String candidate, String number) {

        List<ChoiceQuestion> choiceQuestionList = new ArrayList<ChoiceQuestion>();
        List<AnswerQuestion> answerQuestionList = new ArrayList<AnswerQuestion>();

        Map<String, String> map01 = new HashMap<String, String>();
        map01.put("A", "JAVA2 EE");
        map01.put("B", "JAVA2 Card");
        map01.put("C", "JAVA2 ME");
        map01.put("D", "JAVA2 HE");
        map01.put("E", "JAVA2 SE");

        Map<String, String> map02 = new HashMap<String, String>();
        map02.put("A", "JAVA程序的main方法必须写在类里面");
        map02.put("B", "JAVA程序中可以有多个main方法");
        map02.put("C", "JAVA程序中类名必须与文件名一样");
        map02.put("D", "JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来");

        Map<String, String> map03 = new HashMap<String, String>();
        map03.put("A", "变量由字母、下划线、数字、$符号随意组成;");
        map03.put("B", "变量不能以数字作为开头;");
        map03.put("C", "A和a在java中是同一个变量;");
        map03.put("D", "不同类型的变量,可以起相同的名字;");

        Map<String, String> map04 = new HashMap<String, String>();
        map04.put("A", "STRING");
        map04.put("B", "x3x;");
        map04.put("C", "void");
        map04.put("D", "de$f");

        Map<String, String> map05 = new HashMap<String, String>();
        map05.put("A", "31");
        map05.put("B", "0");
        map05.put("C", "1");
        map05.put("D", "2");

        choiceQuestionList.add(new ChoiceQuestion("JAVA所定义的版本中不包括", map01, "D"));
        choiceQuestionList.add(new ChoiceQuestion("下列说法正确的是", map02, "A"));
        choiceQuestionList.add(new ChoiceQuestion("变量命名规范说法正确的是", map03, "B"));
        choiceQuestionList.add(new ChoiceQuestion("以下()不是合法的标识符", map04, "C"));
        choiceQuestionList.add(new ChoiceQuestion("表达式(11+3*8)/4%3的值是", map05, "D"));
        answerQuestionList.add(new AnswerQuestion("小红马和小黑马生的小马几条腿", "4条腿"));
        answerQuestionList.add(new AnswerQuestion("铁棒打头疼还是木棒打头疼", "头最疼"));
        answerQuestionList.add(new AnswerQuestion("什么床不能睡觉", "牙床"));
        answerQuestionList.add(new AnswerQuestion("为什么好马不吃回头草", "后面的草没了"));

        // 输出结果
        StringBuilder detail = new StringBuilder("考生:" + candidate + "\r\n" +
                "考号:" + number + "\r\n" +
                "--------------------------------------------\r\n" +
                "一、选择题" + "\r\n\n");

        for (int idx = 0; idx < choiceQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(choiceQuestionList.get(idx).getName()).append("\r\n");
            Map<String, String> option = choiceQuestionList.get(idx).getOption();
            for (String key : option.keySet()) {
                detail.append(key).append(":").append(option.get(key)).append("\r\n");
                ;
            }
            detail.append("答案:").append(choiceQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        detail.append("二、问答题" + "\r\n\n");

        for (int idx = 0; idx < answerQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(answerQuestionList.get(idx).getName()).append("\r\n");
            detail.append("答案:").append(answerQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        return detail.toString();
    }

}
public class QuestionBankController {

    public String createPaper(String candidate, String number) {

        List<ChoiceQuestion> choiceQuestionList = new ArrayList<ChoiceQuestion>();
        List<AnswerQuestion> answerQuestionList = new ArrayList<AnswerQuestion>();

        Map<String, String> map01 = new HashMap<String, String>();
        map01.put("A", "JAVA2 EE");
        map01.put("B", "JAVA2 Card");
        map01.put("C", "JAVA2 ME");
        map01.put("D", "JAVA2 HE");
        map01.put("E", "JAVA2 SE");

        Map<String, String> map02 = new HashMap<String, String>();
        map02.put("A", "JAVA程序的main方法必须写在类里面");
        map02.put("B", "JAVA程序中可以有多个main方法");
        map02.put("C", "JAVA程序中类名必须与文件名一样");
        map02.put("D", "JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来");

        Map<String, String> map03 = new HashMap<String, String>();
        map03.put("A", "变量由字母、下划线、数字、$符号随意组成;");
        map03.put("B", "变量不能以数字作为开头;");
        map03.put("C", "A和a在java中是同一个变量;");
        map03.put("D", "不同类型的变量,可以起相同的名字;");

        Map<String, String> map04 = new HashMap<String, String>();
        map04.put("A", "STRING");
        map04.put("B", "x3x;");
        map04.put("C", "void");
        map04.put("D", "de$f");

        Map<String, String> map05 = new HashMap<String, String>();
        map05.put("A", "31");
        map05.put("B", "0");
        map05.put("C", "1");
        map05.put("D", "2");

        choiceQuestionList.add(new ChoiceQuestion("JAVA所定义的版本中不包括", map01, "D"));
        choiceQuestionList.add(new ChoiceQuestion("下列说法正确的是", map02, "A"));
        choiceQuestionList.add(new ChoiceQuestion("变量命名规范说法正确的是", map03, "B"));
        choiceQuestionList.add(new ChoiceQuestion("以下()不是合法的标识符", map04, "C"));
        choiceQuestionList.add(new ChoiceQuestion("表达式(11+3*8)/4%3的值是", map05, "D"));
        answerQuestionList.add(new AnswerQuestion("小红马和小黑马生的小马几条腿", "4条腿"));
        answerQuestionList.add(new AnswerQuestion("铁棒打头疼还是木棒打头疼", "头最疼"));
        answerQuestionList.add(new AnswerQuestion("什么床不能睡觉", "牙床"));
        answerQuestionList.add(new AnswerQuestion("为什么好马不吃回头草", "后面的草没了"));

        // 输出结果
        StringBuilder detail = new StringBuilder("考生:" + candidate + "\r\n" +
                "考号:" + number + "\r\n" +
                "--------------------------------------------\r\n" +
                "一、选择题" + "\r\n\n");

        for (int idx = 0; idx < choiceQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(choiceQuestionList.get(idx).getName()).append("\r\n");
            Map<String, String> option = choiceQuestionList.get(idx).getOption();
            for (String key : option.keySet()) {
                detail.append(key).append(":").append(option.get(key)).append("\r\n");
                ;
            }
            detail.append("答案:").append(choiceQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        detail.append("二、问答题" + "\r\n\n");

        for (int idx = 0; idx < answerQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(answerQuestionList.get(idx).getName()).append("\r\n");
            detail.append("答案:").append(answerQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        return detail.toString();
    }

}
  • 以上的代码主要就三部分内容;首先创建选择题和问答题到集合中、定义详情字符串包装结果、返回结果内容。

测试验证

编写测试类

java
@Test
public void test_QuestionBankController() {
    QuestionBankController questionBankController = new QuestionBankController();
    System.out.println(questionBankController.createPaper("花花", "1000001921032"));
    System.out.println(questionBankController.createPaper("豆豆", "1000001921051"));
    System.out.println(questionBankController.createPaper("大宝", "1000001921987"));
}
@Test
public void test_QuestionBankController() {
    QuestionBankController questionBankController = new QuestionBankController();
    System.out.println(questionBankController.createPaper("花花", "1000001921032"));
    System.out.println(questionBankController.createPaper("豆豆", "1000001921051"));
    System.out.println(questionBankController.createPaper("大宝", "1000001921987"));
}

结果

java
考生:花花
考号:1000001921032
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 ME
D:JAVA2 HE
E:JAVA2 SE
答案:D

第2题:下列说法正确的是
A:JAVA程序的main方法必须写在类里面
B:JAVA程序中可以有多个main方法
C:JAVA程序中类名必须与文件名一样
D:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
答案:A

第3题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:变量不能以数字作为开头;
C:A和a在java中是同一个变量;
D:不同类型的变量,可以起相同的名字;
答案:B

第4题:以下()不是合法的标识符
A:STRING
B:x3x;
C:void
D:de$f
答案:C

第5题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:1
D:2
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:什么床不能睡觉
答案:牙床

第4题:为什么好马不吃回头草
答案:后面的草没了


考生:豆豆
考号:1000001921051
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 ME
D:JAVA2 HE
E:JAVA2 SE
答案:D

第2题:下列说法正确的是
A:JAVA程序的main方法必须写在类里面
B:JAVA程序中可以有多个main方法
C:JAVA程序中类名必须与文件名一样
D:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
答案:A

第3题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:变量不能以数字作为开头;
C:A和a在java中是同一个变量;
D:不同类型的变量,可以起相同的名字;
答案:B

第4题:以下()不是合法的标识符
A:STRING
B:x3x;
C:void
D:de$f
答案:C

第5题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:1
D:2
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:什么床不能睡觉
答案:牙床

第4题:为什么好马不吃回头草
答案:后面的草没了


考生:大宝
考号:1000001921987
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 ME
D:JAVA2 HE
E:JAVA2 SE
答案:D

第2题:下列说法正确的是
A:JAVA程序的main方法必须写在类里面
B:JAVA程序中可以有多个main方法
C:JAVA程序中类名必须与文件名一样
D:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
答案:A

第3题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:变量不能以数字作为开头;
C:A和a在java中是同一个变量;
D:不同类型的变量,可以起相同的名字;
答案:B

第4题:以下()不是合法的标识符
A:STRING
B:x3x;
C:void
D:de$f
答案:C

第5题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:1
D:2
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:什么床不能睡觉
答案:牙床

第4题:为什么好马不吃回头草
答案:后面的草没了

Process finished with exit code 0
考生:花花
考号:1000001921032
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 ME
D:JAVA2 HE
E:JAVA2 SE
答案:D

第2题:下列说法正确的是
A:JAVA程序的main方法必须写在类里面
B:JAVA程序中可以有多个main方法
C:JAVA程序中类名必须与文件名一样
D:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
答案:A

第3题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:变量不能以数字作为开头;
C:A和a在java中是同一个变量;
D:不同类型的变量,可以起相同的名字;
答案:B

第4题:以下()不是合法的标识符
A:STRING
B:x3x;
C:void
D:de$f
答案:C

第5题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:1
D:2
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:什么床不能睡觉
答案:牙床

第4题:为什么好马不吃回头草
答案:后面的草没了


考生:豆豆
考号:1000001921051
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 ME
D:JAVA2 HE
E:JAVA2 SE
答案:D

第2题:下列说法正确的是
A:JAVA程序的main方法必须写在类里面
B:JAVA程序中可以有多个main方法
C:JAVA程序中类名必须与文件名一样
D:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
答案:A

第3题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:变量不能以数字作为开头;
C:A和a在java中是同一个变量;
D:不同类型的变量,可以起相同的名字;
答案:B

第4题:以下()不是合法的标识符
A:STRING
B:x3x;
C:void
D:de$f
答案:C

第5题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:1
D:2
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:什么床不能睡觉
答案:牙床

第4题:为什么好马不吃回头草
答案:后面的草没了


考生:大宝
考号:1000001921987
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 ME
D:JAVA2 HE
E:JAVA2 SE
答案:D

第2题:下列说法正确的是
A:JAVA程序的main方法必须写在类里面
B:JAVA程序中可以有多个main方法
C:JAVA程序中类名必须与文件名一样
D:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
答案:A

第3题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:变量不能以数字作为开头;
C:A和a在java中是同一个变量;
D:不同类型的变量,可以起相同的名字;
答案:B

第4题:以下()不是合法的标识符
A:STRING
B:x3x;
C:void
D:de$f
答案:C

第5题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:1
D:2
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:什么床不能睡觉
答案:牙床

第4题:为什么好马不吃回头草
答案:后面的草没了

Process finished with exit code 0
  • 以上呢就是三位考试的试卷;花花豆豆大宝,每个人的试卷内容是一样的这没问题,但是三个人的题目以及选项顺序都是一样,就没有达到我们说希望的乱序要求。

4.原型模式重构代码

原型模式主要解决的问题就是创建大量重复的类,而我们模拟的场景就需要给不同的用户都创建相同的试卷,但这些试卷的题目不便于每次都从库中获取,甚至有时候需要从远程的RPC中获取。这样都是非常耗时的,而且随着创建对象的增多将严重影响效率。

在原型模式中所需要的非常重要的手段就是克隆,在需要用到克隆的类中都需要实现 implements Cloneable 接口。

原型模式模型结构

image-20230102102546382

代码实现

题目选项乱序操作工具包

java
/**
 * 乱序Map元素,记录对应答案key
 * @param option 题目
 * @param key    答案
 * @return Topic 乱序后 {A=c., B=d., C=a., D=b.}
 */
static public Topic random(Map<String, String> option, String key) {
    Set<String> keySet = option.keySet();
    ArrayList<String> keyList = new ArrayList<String>(keySet);
    Collections.shuffle(keyList);
    HashMap<String, String> optionNew = new HashMap<String, String>();
    int idx = 0;
    String keyNew = "";
    for (String next : keySet) {
        String randomKey = keyList.get(idx++);
        if (key.equals(next)) {
            keyNew = randomKey;
        }
        optionNew.put(randomKey, option.get(next));
    }
    return new Topic(optionNew, keyNew);
}
/**
 * 乱序Map元素,记录对应答案key
 * @param option 题目
 * @param key    答案
 * @return Topic 乱序后 {A=c., B=d., C=a., D=b.}
 */
static public Topic random(Map<String, String> option, String key) {
    Set<String> keySet = option.keySet();
    ArrayList<String> keyList = new ArrayList<String>(keySet);
    Collections.shuffle(keyList);
    HashMap<String, String> optionNew = new HashMap<String, String>();
    int idx = 0;
    String keyNew = "";
    for (String next : keySet) {
        String randomKey = keyList.get(idx++);
        if (key.equals(next)) {
            keyNew = randomKey;
        }
        optionNew.put(randomKey, option.get(next));
    }
    return new Topic(optionNew, keyNew);
}
  • 这个这个工具类的操作就是将原有Map中的选型乱序操作,也就是A的选项内容给BB的可能给C,同时记录正确答案在处理后的位置信息。

克隆对象处理类

java
public class QuestionBank implements Cloneable {

    private String candidate; // 考生
    private String number;    // 考号

    private ArrayList<ChoiceQuestion> choiceQuestionList = new ArrayList<ChoiceQuestion>();
    private ArrayList<AnswerQuestion> answerQuestionList = new ArrayList<AnswerQuestion>();

    public QuestionBank append(ChoiceQuestion choiceQuestion) {
        choiceQuestionList.add(choiceQuestion);
        return this;
    }

    public QuestionBank append(AnswerQuestion answerQuestion) {
        answerQuestionList.add(answerQuestion);
        return this;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        QuestionBank questionBank = (QuestionBank) super.clone();
        questionBank.choiceQuestionList = (ArrayList<ChoiceQuestion>) choiceQuestionList.clone();
        questionBank.answerQuestionList = (ArrayList<AnswerQuestion>) answerQuestionList.clone();

        // 题目乱序
        Collections.shuffle(questionBank.choiceQuestionList);
        Collections.shuffle(questionBank.answerQuestionList);
        // 答案乱序
        ArrayList<ChoiceQuestion> choiceQuestionList = questionBank.choiceQuestionList;
        for (ChoiceQuestion question : choiceQuestionList) {
            Topic random = TopicRandomUtil.random(question.getOption(), question.getKey());
            question.setOption(random.getOption());
            question.setKey(random.getKey());
        }
        return questionBank;
    }

    public void setCandidate(String candidate) {
        this.candidate = candidate;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @Override
    public String toString() {

        StringBuilder detail = new StringBuilder("考生:" + candidate + "\r\n" +
                "考号:" + number + "\r\n" +
                "--------------------------------------------\r\n" +
                "一、选择题" + "\r\n\n");

        for (int idx = 0; idx < choiceQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(choiceQuestionList.get(idx).getName()).append("\r\n");
            Map<String, String> option = choiceQuestionList.get(idx).getOption();
            for (String key : option.keySet()) {
                detail.append(key).append(":").append(option.get(key)).append("\r\n");;
            }
            detail.append("答案:").append(choiceQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        detail.append("二、问答题" + "\r\n\n");

        for (int idx = 0; idx < answerQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(answerQuestionList.get(idx).getName()).append("\r\n");
            detail.append("答案:").append(answerQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        return detail.toString();
    }

}
public class QuestionBank implements Cloneable {

    private String candidate; // 考生
    private String number;    // 考号

    private ArrayList<ChoiceQuestion> choiceQuestionList = new ArrayList<ChoiceQuestion>();
    private ArrayList<AnswerQuestion> answerQuestionList = new ArrayList<AnswerQuestion>();

    public QuestionBank append(ChoiceQuestion choiceQuestion) {
        choiceQuestionList.add(choiceQuestion);
        return this;
    }

    public QuestionBank append(AnswerQuestion answerQuestion) {
        answerQuestionList.add(answerQuestion);
        return this;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        QuestionBank questionBank = (QuestionBank) super.clone();
        questionBank.choiceQuestionList = (ArrayList<ChoiceQuestion>) choiceQuestionList.clone();
        questionBank.answerQuestionList = (ArrayList<AnswerQuestion>) answerQuestionList.clone();

        // 题目乱序
        Collections.shuffle(questionBank.choiceQuestionList);
        Collections.shuffle(questionBank.answerQuestionList);
        // 答案乱序
        ArrayList<ChoiceQuestion> choiceQuestionList = questionBank.choiceQuestionList;
        for (ChoiceQuestion question : choiceQuestionList) {
            Topic random = TopicRandomUtil.random(question.getOption(), question.getKey());
            question.setOption(random.getOption());
            question.setKey(random.getKey());
        }
        return questionBank;
    }

    public void setCandidate(String candidate) {
        this.candidate = candidate;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @Override
    public String toString() {

        StringBuilder detail = new StringBuilder("考生:" + candidate + "\r\n" +
                "考号:" + number + "\r\n" +
                "--------------------------------------------\r\n" +
                "一、选择题" + "\r\n\n");

        for (int idx = 0; idx < choiceQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(choiceQuestionList.get(idx).getName()).append("\r\n");
            Map<String, String> option = choiceQuestionList.get(idx).getOption();
            for (String key : option.keySet()) {
                detail.append(key).append(":").append(option.get(key)).append("\r\n");;
            }
            detail.append("答案:").append(choiceQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        detail.append("二、问答题" + "\r\n\n");

        for (int idx = 0; idx < answerQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(answerQuestionList.get(idx).getName()).append("\r\n");
            detail.append("答案:").append(answerQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        return detail.toString();
    }

}

这里的主要操作内容有三个,分别是;

  • 两个append(),对各项题目的添加,有点像我们在建造者模式中使用的方式,添加装修物料。
  • clone(),这里的核心操作就是对对象的复制,这里的复制不只是包括了本身,同时对两个集合也做了复制。只有这样的拷贝才能确保在操作克隆对象的时候不影响原对象。
  • 乱序操作,在list集合中有一个方法,Collections.shuffle,可以将原有集合的顺序打乱,输出一个新的顺序。在这里我们使用此方法对题目进行乱序操作。

初始化试卷数据

java
public class QuestionBankController {

    private QuestionBank questionBank = new QuestionBank();

    public QuestionBankController() {

        Map<String, String> map01 = new HashMap<String, String>();
        map01.put("A", "JAVA2 EE");
        map01.put("B", "JAVA2 Card");
        map01.put("C", "JAVA2 ME");
        map01.put("D", "JAVA2 HE");
        map01.put("E", "JAVA2 SE");

        Map<String, String> map02 = new HashMap<String, String>();
        map02.put("A", "JAVA程序的main方法必须写在类里面");
        map02.put("B", "JAVA程序中可以有多个main方法");
        map02.put("C", "JAVA程序中类名必须与文件名一样");
        map02.put("D", "JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来");

        Map<String, String> map03 = new HashMap<String, String>();
        map03.put("A", "变量由字母、下划线、数字、$符号随意组成;");
        map03.put("B", "变量不能以数字作为开头;");
        map03.put("C", "A和a在java中是同一个变量;");
        map03.put("D", "不同类型的变量,可以起相同的名字;");

        Map<String, String> map04 = new HashMap<String, String>();
        map04.put("A", "STRING");
        map04.put("B", "x3x;");
        map04.put("C", "void");
        map04.put("D", "de$f");

        Map<String, String> map05 = new HashMap<String, String>();
        map05.put("A", "31");
        map05.put("B", "0");
        map05.put("C", "1");
        map05.put("D", "2");
        
        questionBank.append(new ChoiceQuestion("JAVA所定义的版本中不包括", map01, "D"))
                .append(new ChoiceQuestion("下列说法正确的是", map02, "A"))
                .append(new ChoiceQuestion("变量命名规范说法正确的是", map03, "B"))
                .append(new ChoiceQuestion("以下()不是合法的标识符",map04, "C"))
                .append(new ChoiceQuestion("表达式(11+3*8)/4%3的值是", map05, "D"))
                .append(new AnswerQuestion("小红马和小黑马生的小马几条腿", "4条腿"))
                .append(new AnswerQuestion("铁棒打头疼还是木棒打头疼", "头最疼"))
                .append(new AnswerQuestion("什么床不能睡觉", "牙床"))
                .append(new AnswerQuestion("为什么好马不吃回头草", "后面的草没了"));
    }

    public String createPaper(String candidate, String number) throws CloneNotSupportedException {
        QuestionBank questionBankClone = (QuestionBank) questionBank.clone();
        questionBankClone.setCandidate(candidate);
        questionBankClone.setNumber(number);
        return questionBankClone.toString();
    }

}
public class QuestionBankController {

    private QuestionBank questionBank = new QuestionBank();

    public QuestionBankController() {

        Map<String, String> map01 = new HashMap<String, String>();
        map01.put("A", "JAVA2 EE");
        map01.put("B", "JAVA2 Card");
        map01.put("C", "JAVA2 ME");
        map01.put("D", "JAVA2 HE");
        map01.put("E", "JAVA2 SE");

        Map<String, String> map02 = new HashMap<String, String>();
        map02.put("A", "JAVA程序的main方法必须写在类里面");
        map02.put("B", "JAVA程序中可以有多个main方法");
        map02.put("C", "JAVA程序中类名必须与文件名一样");
        map02.put("D", "JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来");

        Map<String, String> map03 = new HashMap<String, String>();
        map03.put("A", "变量由字母、下划线、数字、$符号随意组成;");
        map03.put("B", "变量不能以数字作为开头;");
        map03.put("C", "A和a在java中是同一个变量;");
        map03.put("D", "不同类型的变量,可以起相同的名字;");

        Map<String, String> map04 = new HashMap<String, String>();
        map04.put("A", "STRING");
        map04.put("B", "x3x;");
        map04.put("C", "void");
        map04.put("D", "de$f");

        Map<String, String> map05 = new HashMap<String, String>();
        map05.put("A", "31");
        map05.put("B", "0");
        map05.put("C", "1");
        map05.put("D", "2");
        
        questionBank.append(new ChoiceQuestion("JAVA所定义的版本中不包括", map01, "D"))
                .append(new ChoiceQuestion("下列说法正确的是", map02, "A"))
                .append(new ChoiceQuestion("变量命名规范说法正确的是", map03, "B"))
                .append(new ChoiceQuestion("以下()不是合法的标识符",map04, "C"))
                .append(new ChoiceQuestion("表达式(11+3*8)/4%3的值是", map05, "D"))
                .append(new AnswerQuestion("小红马和小黑马生的小马几条腿", "4条腿"))
                .append(new AnswerQuestion("铁棒打头疼还是木棒打头疼", "头最疼"))
                .append(new AnswerQuestion("什么床不能睡觉", "牙床"))
                .append(new AnswerQuestion("为什么好马不吃回头草", "后面的草没了"));
    }

    public String createPaper(String candidate, String number) throws CloneNotSupportedException {
        QuestionBank questionBankClone = (QuestionBank) questionBank.clone();
        questionBankClone.setCandidate(candidate);
        questionBankClone.setNumber(number);
        return questionBankClone.toString();
    }

}
  • 这个类主要提供对试卷内容的模式初始化操作(所有考生试卷一样,题目顺序不一致)。
  • 以及对外部提供创建试卷的方法,在创建的过程中使用的是克隆的方式;(QuestionBank) questionBank.clone();,并最终返回试卷信息。

测试验证

编写测试类

java
@Test
public void test_QuestionBank() throws CloneNotSupportedException {
    QuestionBankController questionBankController = new QuestionBankController();
    System.out.println(questionBankController.createPaper("花花", "1000001921032"));
    System.out.println(questionBankController.createPaper("豆豆", "1000001921051"));
    System.out.println(questionBankController.createPaper("大宝", "1000001921987"));
}
@Test
public void test_QuestionBank() throws CloneNotSupportedException {
    QuestionBankController questionBankController = new QuestionBankController();
    System.out.println(questionBankController.createPaper("花花", "1000001921032"));
    System.out.println(questionBankController.createPaper("豆豆", "1000001921051"));
    System.out.println(questionBankController.createPaper("大宝", "1000001921987"));
}

结果

java
考生:花花
考号:1000001921032
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 Card
B:JAVA2 HE
C:JAVA2 EE
D:JAVA2 ME
E:JAVA2 SE
答案:B

第2题:表达式(11+3*8)/4%3的值是
A:1
B:0
C:31
D:2
答案:D

第3题:以下()不是合法的标识符
A:void
B:de$f
C:STRING
D:x3x;
答案:A

第4题:下列说法正确的是
A:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
B:JAVA程序中可以有多个main方法
C:JAVA程序的main方法必须写在类里面
D:JAVA程序中类名必须与文件名一样
答案:C

第5题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:A和a在java中是同一个变量;
C:不同类型的变量,可以起相同的名字;
D:变量不能以数字作为开头;
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:什么床不能睡觉
答案:牙床

第3题:铁棒打头疼还是木棒打头疼
答案:头最疼

第4题:为什么好马不吃回头草
答案:后面的草没了


考生:豆豆
考号:1000001921051
--------------------------------------------
一、选择题

第1题:下列说法正确的是
A:JAVA程序中可以有多个main方法
B:JAVA程序的main方法必须写在类里面
C:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
D:JAVA程序中类名必须与文件名一样
答案:B

第2题:表达式(11+3*8)/4%3的值是
A:2
B:1
C:31
D:0
答案:A

第3题:以下()不是合法的标识符
A:void
B:de$f
C:x3x;
D:STRING
答案:A

第4题:JAVA所定义的版本中不包括
A:JAVA2 Card
B:JAVA2 HE
C:JAVA2 ME
D:JAVA2 EE
E:JAVA2 SE
答案:B

第5题:变量命名规范说法正确的是
A:变量不能以数字作为开头;
B:A和a在java中是同一个变量;
C:不同类型的变量,可以起相同的名字;
D:变量由字母、下划线、数字、$符号随意组成;
答案:A

二、问答题

第1题:什么床不能睡觉
答案:牙床

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:为什么好马不吃回头草
答案:后面的草没了

第4题:小红马和小黑马生的小马几条腿
答案:4条腿


考生:大宝
考号:1000001921987
--------------------------------------------
一、选择题

第1题:以下()不是合法的标识符
A:x3x;
B:de$f
C:void
D:STRING
答案:C

第2题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:2
D:1
答案:C

第3题:变量命名规范说法正确的是
A:不同类型的变量,可以起相同的名字;
B:变量由字母、下划线、数字、$符号随意组成;
C:变量不能以数字作为开头;
D:A和a在java中是同一个变量;
答案:C

第4题:下列说法正确的是
A:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
B:JAVA程序的main方法必须写在类里面
C:JAVA程序中类名必须与文件名一样
D:JAVA程序中可以有多个main方法
答案:B

第5题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 HE
D:JAVA2 SE
E:JAVA2 ME
答案:C

二、问答题

第1题:为什么好马不吃回头草
答案:后面的草没了

第2题:小红马和小黑马生的小马几条腿
答案:4条腿

第3题:什么床不能睡觉
答案:牙床

第4题:铁棒打头疼还是木棒打头疼
答案:头最疼

Process finished with exit code 0
考生:花花
考号:1000001921032
--------------------------------------------
一、选择题

第1题:JAVA所定义的版本中不包括
A:JAVA2 Card
B:JAVA2 HE
C:JAVA2 EE
D:JAVA2 ME
E:JAVA2 SE
答案:B

第2题:表达式(11+3*8)/4%3的值是
A:1
B:0
C:31
D:2
答案:D

第3题:以下()不是合法的标识符
A:void
B:de$f
C:STRING
D:x3x;
答案:A

第4题:下列说法正确的是
A:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
B:JAVA程序中可以有多个main方法
C:JAVA程序的main方法必须写在类里面
D:JAVA程序中类名必须与文件名一样
答案:C

第5题:变量命名规范说法正确的是
A:变量由字母、下划线、数字、$符号随意组成;
B:A和a在java中是同一个变量;
C:不同类型的变量,可以起相同的名字;
D:变量不能以数字作为开头;
答案:D

二、问答题

第1题:小红马和小黑马生的小马几条腿
答案:4条腿

第2题:什么床不能睡觉
答案:牙床

第3题:铁棒打头疼还是木棒打头疼
答案:头最疼

第4题:为什么好马不吃回头草
答案:后面的草没了


考生:豆豆
考号:1000001921051
--------------------------------------------
一、选择题

第1题:下列说法正确的是
A:JAVA程序中可以有多个main方法
B:JAVA程序的main方法必须写在类里面
C:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
D:JAVA程序中类名必须与文件名一样
答案:B

第2题:表达式(11+3*8)/4%3的值是
A:2
B:1
C:31
D:0
答案:A

第3题:以下()不是合法的标识符
A:void
B:de$f
C:x3x;
D:STRING
答案:A

第4题:JAVA所定义的版本中不包括
A:JAVA2 Card
B:JAVA2 HE
C:JAVA2 ME
D:JAVA2 EE
E:JAVA2 SE
答案:B

第5题:变量命名规范说法正确的是
A:变量不能以数字作为开头;
B:A和a在java中是同一个变量;
C:不同类型的变量,可以起相同的名字;
D:变量由字母、下划线、数字、$符号随意组成;
答案:A

二、问答题

第1题:什么床不能睡觉
答案:牙床

第2题:铁棒打头疼还是木棒打头疼
答案:头最疼

第3题:为什么好马不吃回头草
答案:后面的草没了

第4题:小红马和小黑马生的小马几条腿
答案:4条腿


考生:大宝
考号:1000001921987
--------------------------------------------
一、选择题

第1题:以下()不是合法的标识符
A:x3x;
B:de$f
C:void
D:STRING
答案:C

第2题:表达式(11+3*8)/4%3的值是
A:31
B:0
C:2
D:1
答案:C

第3题:变量命名规范说法正确的是
A:不同类型的变量,可以起相同的名字;
B:变量由字母、下划线、数字、$符号随意组成;
C:变量不能以数字作为开头;
D:A和a在java中是同一个变量;
答案:C

第4题:下列说法正确的是
A:JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来
B:JAVA程序的main方法必须写在类里面
C:JAVA程序中类名必须与文件名一样
D:JAVA程序中可以有多个main方法
答案:B

第5题:JAVA所定义的版本中不包括
A:JAVA2 EE
B:JAVA2 Card
C:JAVA2 HE
D:JAVA2 SE
E:JAVA2 ME
答案:C

二、问答题

第1题:为什么好马不吃回头草
答案:后面的草没了

第2题:小红马和小黑马生的小马几条腿
答案:4条腿

第3题:什么床不能睡觉
答案:牙床

第4题:铁棒打头疼还是木棒打头疼
答案:头最疼

Process finished with exit code 0

从以上的输出结果可以看到,每个人的题目和答案都是差异化的乱序的,如下图比对结果; - 花花、豆豆、大宝,每个人的试卷都存在着题目和选项的混乱排序

5.浅拷贝和深拷贝

5.1 浅拷贝的介绍

  1. 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
  2. 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
  3. 前面的案例就是浅拷贝
  4. 浅拷贝是使用默认的 clone()方法来实现

5.2 深拷贝基本介绍

  1. 复制对象的所有基本数据类型的成员变量值
  2. 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
  3. 深拷贝实现方式 1:重写 clone 方法来实现深拷贝
  4. 深拷贝实现方式 2:通过对象序列化实现深拷贝(推荐)

6.深拷贝应用实例

复制对象都是基本数据类型,进行深拷贝直接实现Cloneable接口即可

java
public class DeepCloneableTarget implements Serializable, Cloneable {
   
   /**
    * 
    */
   private static final long serialVersionUID = 1L;

   private String cloneName;

   private String cloneClass;

   //构造器
   public DeepCloneableTarget(String cloneName, String cloneClass) {
      this.cloneName = cloneName;
      this.cloneClass = cloneClass;
   }

   //因为该类的属性,都是String , 因此我们这里使用默认的clone完成即可
   @Override
   protected Object clone() throws CloneNotSupportedException {
      return super.clone();
   }
}
public class DeepCloneableTarget implements Serializable, Cloneable {
   
   /**
    * 
    */
   private static final long serialVersionUID = 1L;

   private String cloneName;

   private String cloneClass;

   //构造器
   public DeepCloneableTarget(String cloneName, String cloneClass) {
      this.cloneName = cloneName;
      this.cloneClass = cloneClass;
   }

   //因为该类的属性,都是String , 因此我们这里使用默认的clone完成即可
   @Override
   protected Object clone() throws CloneNotSupportedException {
      return super.clone();
   }
}

复制对象既有基本数据类型又有引用类型

java
public class DeepProtoType implements Serializable, Cloneable{
   
   public String name; //String 属性
   public DeepCloneableTarget deepCloneableTarget;// 引用类型
   public DeepProtoType() {
      super();
   }
   
   
   //深拷贝 - 方式 1 使用clone 方法
   @Override
   protected Object clone() throws CloneNotSupportedException {
      
      Object deep = null;
      //这里完成对基本数据类型(属性)和String的克隆
      deep = super.clone(); 
      //对引用类型的属性,进行单独处理
      DeepProtoType deepProtoType = (DeepProtoType)deep;
      deepProtoType.deepCloneableTarget  = (DeepCloneableTarget)deepCloneableTarget.clone();
      
      // TODO Auto-generated method stub
      return deepProtoType;
   }
   
   //深拷贝 - 方式2 通过对象的序列化实现 (推荐)
   
   public Object deepClone() {
      
      //创建流对象
      ByteArrayOutputStream bos = null;
      ObjectOutputStream oos = null;
      ByteArrayInputStream bis = null;
      ObjectInputStream ois = null;
      
      try {
         
         //序列化
         bos = new ByteArrayOutputStream();
         oos = new ObjectOutputStream(bos);
         oos.writeObject(this); //当前这个对象以对象流的方式输出
         
         //反序列化
         bis = new ByteArrayInputStream(bos.toByteArray());
         ois = new ObjectInputStream(bis);
         DeepProtoType copyObj = (DeepProtoType)ois.readObject();
         
         return copyObj;
         
      } catch (Exception e) {
         // TODO: handle exception
         e.printStackTrace();
         return null;
      } finally {
         //关闭流
         try {
            bos.close();
            oos.close();
            bis.close();
            ois.close();
         } catch (Exception e2) {
            // TODO: handle exception
            System.out.println(e2.getMessage());
         }
      }
      
   }
   
}
public class DeepProtoType implements Serializable, Cloneable{
   
   public String name; //String 属性
   public DeepCloneableTarget deepCloneableTarget;// 引用类型
   public DeepProtoType() {
      super();
   }
   
   
   //深拷贝 - 方式 1 使用clone 方法
   @Override
   protected Object clone() throws CloneNotSupportedException {
      
      Object deep = null;
      //这里完成对基本数据类型(属性)和String的克隆
      deep = super.clone(); 
      //对引用类型的属性,进行单独处理
      DeepProtoType deepProtoType = (DeepProtoType)deep;
      deepProtoType.deepCloneableTarget  = (DeepCloneableTarget)deepCloneableTarget.clone();
      
      // TODO Auto-generated method stub
      return deepProtoType;
   }
   
   //深拷贝 - 方式2 通过对象的序列化实现 (推荐)
   
   public Object deepClone() {
      
      //创建流对象
      ByteArrayOutputStream bos = null;
      ObjectOutputStream oos = null;
      ByteArrayInputStream bis = null;
      ObjectInputStream ois = null;
      
      try {
         
         //序列化
         bos = new ByteArrayOutputStream();
         oos = new ObjectOutputStream(bos);
         oos.writeObject(this); //当前这个对象以对象流的方式输出
         
         //反序列化
         bis = new ByteArrayInputStream(bos.toByteArray());
         ois = new ObjectInputStream(bis);
         DeepProtoType copyObj = (DeepProtoType)ois.readObject();
         
         return copyObj;
         
      } catch (Exception e) {
         // TODO: handle exception
         e.printStackTrace();
         return null;
      } finally {
         //关闭流
         try {
            bos.close();
            oos.close();
            bis.close();
            ois.close();
         } catch (Exception e2) {
            // TODO: handle exception
            System.out.println(e2.getMessage());
         }
      }
      
   }
   
}

测试类

编写测试类

java
public static void main(String[] args) throws Exception {
      DeepProtoType p = new DeepProtoType();
      p.name = "宋江";
      p.deepCloneableTarget = new DeepCloneableTarget("大牛", "小牛");

      //方式1 完成深拷贝

//    DeepProtoType p2 = (DeepProtoType) p.clone();
//
//    System.out.println("p.name=" + p.name + "p.deepCloneableTarget=" + p.deepCloneableTarget.hashCode());
//    System.out.println("p2.name=" + p.name + "p2.deepCloneableTarget=" + p2.deepCloneableTarget.hashCode());

      //方式2 完成深拷贝
      DeepProtoType p2 = p.deepClone();
      
      System.out.println("p.name=" + p.name + "p.deepCloneableTarget=" + p.deepCloneableTarget.hashCode());
      System.out.println("p2.name=" + p.name + "p2.deepCloneableTarget=" + p2.deepCloneableTarget.hashCode());

   }

}
public static void main(String[] args) throws Exception {
      DeepProtoType p = new DeepProtoType();
      p.name = "宋江";
      p.deepCloneableTarget = new DeepCloneableTarget("大牛", "小牛");

      //方式1 完成深拷贝

//    DeepProtoType p2 = (DeepProtoType) p.clone();
//
//    System.out.println("p.name=" + p.name + "p.deepCloneableTarget=" + p.deepCloneableTarget.hashCode());
//    System.out.println("p2.name=" + p.name + "p2.deepCloneableTarget=" + p2.deepCloneableTarget.hashCode());

      //方式2 完成深拷贝
      DeepProtoType p2 = p.deepClone();
      
      System.out.println("p.name=" + p.name + "p.deepCloneableTarget=" + p.deepCloneableTarget.hashCode());
      System.out.println("p2.name=" + p.name + "p2.deepCloneableTarget=" + p2.deepCloneableTarget.hashCode());

   }

}

结果

java
p.name=宋江p.deepCloneableTarget=1554874502
p2.name=宋江p2.deepCloneableTarget=1313922862
p.name=宋江p.deepCloneableTarget=1554874502
p2.name=宋江p2.deepCloneableTarget=1313922862
  • 可以看到两个对象的引用类型属性deepCloneableTarget,输出的hashcode值时不一样,说明可弄DeepProtoType对象实现了深拷贝。

7.原型模式的注意事项和细节

  1. 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
  2. 不用重新初始化对象,而是动态地获得对象运行时的状态
  3. 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码
  4. 在实现深克隆的时候可能需要比较复杂的代码
  5. 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了 ocp 原则(开闭原则)