这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Networkengineering1834
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Networkengineering1834/homework/11148
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)

开发信息

结对成员

姓名 学号
林家汇 3118005281
于翔 3118005296

项目地址

GitHub仓库地址:https://github.com/linjiahui020/examination

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 20
· Estimate · 估计这个任务需要多少时间 30 20
Development 开发 815 995
· Analysis · 需求分析 (包括学习新技术) 100 150
· Design Spec · 生成设计文档 50 60
· Design Review · 设计复审 (和同事审核设计文档) 25 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 20
· Design · 具体设计 40 60
· Coding · 具体编码 500 600
· Code Review · 代码复审 20 15
· Test · 测试(自我测试,修改代码,提交修改) 60 80
Reporting 报告 120 170
· Test Report · 测试报告 40 50
· Size Measurement · 计算工作量 20 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60 100
合计 965 1185

效能分析

程序随机生成10000个运算式,从图中可以看到堆内存的消耗在上升后逐渐趋于稳定了

在 Overview 中可看到:类的利用率算是在较高的利用水平,并且CPU的利用率却并不高

对于内存:内存的消耗在短时间内达到很高的占用

从下面数据中看出类的消耗:由于计算中大量使用了字符串的加减,所以为了减轻虚拟机的负担,我们使用StringBuilder来进行大部分的字符串操作

设计实现过程

大致流程分析

首先,队友林家汇讨论并确定了编程语言。为降低开发以及学习成本并能按时完成开发工作,我们最终决定使用比较熟悉的Java语言进行相关的开发。

其次,在分析需求设计项目时,发现涉及到随机生成并且校对,故需要大量操作字符串,所以使用StringBuilder来进行相应的操作,提升系统的执行分析速度;其次需求中还涉及到了相关的文件处理操作,所以使用到IO流及相关操作,同时为了让用户有更好的交互性以及用户体验,又需使用到 Java 提供的 Swing 界面。

最后,由于需要分析效能,故需要使用到相应的性能分析神器 ——JProfiler!

程序类分析

  • CreateUtil类:创建运算相关操作
    • create方法:随机生成式子
    • createNum方法:随机生成操作数
    • createSign方法:随机生成符号
    • formulaNum方法:设定随机生成一定数目的式子
    • numRange方法:判断操作数是否超过最大值
  • HandleUtil类:计算过程的相关方法
    • createNum方法:将答案按规范生成出来
    • gcd方法:求两数的最大公因数
    • charFind方法:存储指定字符的位序式子
    • changeNum方法:将数字字符串转为数字值
    • judge方法:判断式子是否符合规范
    • change方法:将字符串的操作数分子分母转成数字
  • CalculateUtil类:计算方法
    • add方法:加法运算
    • minus方法:减法运算
    • multiply方法:乘法运算
    • divide方法:除法运算
    • calculate方法:对运算符号左右的两个数进行运算
    • calculateFormula方法:计算式子
  • CheckUtil类:校验式子(查重)
    • spiltStringBuilderToArray方法:将式子拆分普通数组
    • spiltStringBuilderToList方法:将式子拆分成List数组
    • spiltStringBuilderToOrderList方法:将式子拆分成有序的 List 数组
    • judgeRepeat方法:判断内容是否有重复
  • IODao类:
    • storageResult方法:将结果存储到文件中
    • storageFile方法:存储过程式子和答案
    • readFile方法:读取文件
  • OperatorVar枚举类:对运算符相关信息进行封装,避免多次的 new 减低效能
  • MainView类:用户交互的主页面
  • UserView类:用户填写等相关操作的页面

流程图分析

代码说明

计算式子

	/**
	 * 对运算符号左右的两个数进行运算
	 * @param index 运算符的位序
	 * @param extraCopy 待计算的式子
	 * @return
	 */
	public static StringBuilder calculate(int index,StringBuilder extraCopy) {
		char sign = extraCopy.charAt(index);
		int beginIndex = 0, endIndex = -1;
		int[] datas;
		for(int index1=0; ; beginIndex=index1) {
			//找到第一个操作数的开头空格
			index1 = extraCopy.indexOf(" ", index1+1);
			if(index1==(index-1)) {
				break;
			}
		}
		datas = HandleUtil.change(extraCopy, beginIndex);
		int numerator1 = datas[1];
		int denominator1 = datas[2];
		datas = HandleUtil.change(extraCopy, index+1);
		int numerator2 = datas[1];
		int denominator2 = datas[2];
		endIndex = datas[0];
		//删除数字部分
		extraCopy.delete(beginIndex+1,endIndex);
		//根据符号进行相应的运算
		switch(sign){
			case '+':
				extraCopy.insert(beginIndex+1, add(numerator1,denominator1,numerator2,denominator2));
				break;
			case '-':
				if(!HandleUtil.judge(numerator1, denominator1, numerator2, denominator2)) {
					//识别答案是否为负数
					extraCopy.insert(0, "@ ");
					break;
				} else{
					extraCopy.insert(beginIndex+1, minus(numerator1,denominator1,numerator2,denominator2));
					break;
				}
			case '*':
				extraCopy.insert(beginIndex+1, multiply(numerator1,denominator1,numerator2,denominator2));
				break;
			case '÷':
				if(numerator2 == 0) {
					//识别答案是否为负数,是的话在开头插入@作为标识
					extraCopy.insert(0, "@ ");
					break;
				} else{
					extraCopy.insert(beginIndex+1, divide(numerator1,denominator1,numerator2,denominator2));
					break;
				}
			default: break;
		}
		return extraCopy;
	}

	/**
	 * 按优先级进行运算(*  /  +  -)
	 * @param extraCopy copy副本
	 * @return 返回
	 */
	public static StringBuilder calculateFormula(StringBuilder extraCopy) {
//		logger.info(extraCopy.toString());
		//记录符号的位序
		int index = -1;
		//计算式子
		Pattern pattern1 = Pattern.compile("[*]|[÷]");
		Matcher m1;
		while((m1 = pattern1.matcher(extraCopy)).find()) {
			index = m1.start();
			calculate(index, extraCopy);
			if(extraCopy.charAt(0)=='@') {
				break;
			}	
		}
		//如果式子正确,在进行加运算(从左到右)
		if(extraCopy.charAt(0)!='@') {
			Pattern pattern2 = Pattern.compile("[-]|[+]");
			Matcher m2;
			while((m2 = pattern2.matcher(extraCopy)).find()) {
				index = m2.start();
				calculate(index, extraCopy);
				if(extraCopy.charAt(0)=='@') {
					break;
				}	
			}
		}
		//如果运算结束后(式子正确),调整答案格式
		if(extraCopy.charAt(0)!='@') {
			int datas[];
			datas = HandleUtil.change(extraCopy, 0);
			//分子
			int numerator = datas[1];
			//分母
			int denominator = datas[2];
			//将原存储内容清空
			extraCopy.setLength(0);
			//将答案换成标准格式
			extraCopy.append(HandleUtil.creatNum(numerator, denominator));
		}
		return extraCopy;
	}

规范化答案显示结果

    /**
     * 将答案按规范生成出来
     * @param numerator 分子
     * @param denominator 分母
     * @return 式子
     */
    public static StringBuilder creatNum(int numerator, int denominator) {
        StringBuilder num = new StringBuilder();
        int gcdNum = gcd(numerator, denominator);
        numerator /= gcdNum;
        denominator /= gcdNum;
        if (numerator >= denominator) {
            //分子大于等于分母
            if (numerator % denominator == 0) {
                //结果为整数
                num.append(numerator / denominator + "");
            } else {
                //结果为带分数
                int interger = numerator / denominator;
                numerator = numerator - (interger * denominator);
                num.append(interger + "'" + numerator + "/" + denominator + "");
            }
        } else {
            //分子小于分母
            if (numerator == 0) {
                //分子等于0
                num.append(0 + "");
            } else {
                //其他情况
                num.append(numerator + "/" + denominator + "");
            }
        }
        return num;
    }

    /**
     * 求两数的最大公因数
     * @param num01 数字1
     * @param num02 数字2
     * @return 返回公因数
     */
    public static int gcd(int num01, int num02) {
        int num;
        while (num02 != 0) {
            num = num01 % num02;
            num01 = num02;
            num02 = num;
        }
        return num01;
    }

测试运行

程序启动及介绍

打开命令行,输入java -jar examination-system.jar执行程序:

程序界面:

说明:

进入到程序说明中,可查看程序使用规则指引:

如果不输入程序设置(为空)或者填写内容不符合规则,会智能提示用户:

程序执行流程

输入正确参数并执行题目生成后,提示是否导入题目,选择”确认“:

与此同时在命令行中显示了生成所需要的时间(ms):

并且打印了生成的题目和答案,清晰直观:

设置存放路径:

在本程序中,为了方便用户以及更好地使用体验,尽管不填写绝对路径,也会自动在相对路径(当前命令行执行路径)下自动生成Answers.txt文件(存放答案)以及Exercises.txt文件(存放题目)

打开Exercises.txt文件,可以看到生成了10000条不重复的四则运算题目,且题目均符合用户输入的规则范围:

打开Answers.txt文件,可以看到生成了10000条不重复的四则运算题目对应的答案:

读取并做题功能

生成题目后,程序跳到做题页面:

查错

写完题目的查错功能:

没想到全错了。。

与此同时,当前文件夹中生成检测结果:

快速生成题目

做完题目后,可快速以用户之前输入的相同的配置来再次生成题目:

新生成题目如图:

项目小结

开发遇到的问题:

  • 由于前期没有和队友很好的规划好架构(没有花上太多时间去规划),同时也缺少了一些必要的交流,导致后期项目的开发受阻,难度加大。
  • 在开发过程中,没有对项目有较好和清晰的分包,所以造成了业务层没有完全抽离出来,代码和功能的组织结构性做的不够好。
  • 因为没有比较好的前期规划,所以在后期的开发测试的调试中,遇到BUG并着手解决 BUG 时会比较困难。

结对感受

  • 于翔:和林家汇大佬结对项目实在是太舒服了,因为他是主攻Java语言的,对Java语言以及开发项目有了很多自己的经验和见解,所以功能开发时遇到了很多问题都会找他讨论和解决,同时也能帮我解决很多想要实现的效果,两个人做项目比一个人做轻松太多了,果然是1+1>2。
  • 林家汇:于翔在用户体验以及交互方面的各种想法总是让我折服,他对于项目的开发提出了很多宝贵的想法,实在是加快了我们开发项目的速度。虽然我们开发时遇到了不少困难,但都被我们克服解决了,作为我们的第一次结对项目合作,能做成这样子的配合真的非常难得了,很期待和他的下一次合作。

闪光点

  • 林家汇闪光点:对Java很熟练,擅长对代码的执行时间和内存消耗进行优化,可以很快规划出大致项目代码结构。
  • 于翔闪光点:擅长对页面交互、程序流程及用户体验的优化,学习能力强。会发现出很多代码bug