面向对象程序设计——第二月课程总结
一、前言:
第二个月的课程学习主要涉及到正则表达式的使用,熟悉类的聚合多态封装,三个性质。
发布的三次题目集中有两道有关日期类聚合设计的题目,三道图形继承设计的题目,还有几道正则表达式的练习题。感觉算是循序渐进,老师给的题目至少能够调动我主动学习的积极性,总是能在每次题目集中给我整一些没听说过的新知识(Map类,LocalDateTime类),我也是顺着老师的思路一边学习一边在PTA上肝代码。虽然我一般都是在编译器上写,写完了就拿老师给的测试样例跑一下,能得到正确输出我才会在PTA上提交。
最能让我印象深刻的题目就是题目集04的7-1题 水文数据校验及处理 和 题目集05的7-4 统计Java程序中关键词的出现次数 很考验我的正则表达式基础和功底。
二、日期聚合类设计题目(聚合一、聚合二)
在题目集04(7-2)中根据老师给的类图,加上上个月的代码很快就能将代码写完。一开始写的时候有点蒙,但看懂类图之后我产生了一种想法。
①度量分析题目集04(7-2):
有19的圈复杂度,其实我主要是设计了一个比较两个日期大小的方法,在里边多次使用的 if - else 语句来使得两个输入的日期变成我想要的一种规范。
    
把这个方法的截图放出来,大概就是这样的一个算法,我希望将输入的月份和日期都转为两位数然后在比较两个日期的大小。 至于求前N天和求后N天的算法还是大同小异。稍后再介绍。
度量分析题目集05(7-5)
这一次为了搞出圈复杂度的分析,我换了一个测试的方法。
ev(G) 基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
iv(G) 模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
      v(G) 是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
这些是测试软件的专业术语,用我这半吊子的解释是 ev(G) 是衡量代码是否结构化的一个指标, iv(G) 是判断各种模块之间的隅合度,正所谓:高聚和低耦合,v(G)就是测代码的圈复杂度。
与上一题类似,都是在比较两个日期大小上使用了过多的if - else语句导致这个方法的圈复杂的达到了17,不利于代码迭代和维护,至少现在我会过头看代码的时候觉得非常复杂。
      
②算法分析:
对于题目集04(7-2)DateUtil Day Month Year 四个类像是一种线性上的关系,只能通过DateUtil.Day.Month.Year 这样的线性调用有点像一个很长很长的链表,但是链表上的每一个节点都是不同类型的数据,然后都存了一个访问下一个节点的“指针”(感觉上和C语言的差不多)。
对于题目集05(7-5)DateUtil Day Month Year 四个类像一个发散的树,从DateUtil类中分出三个叉为Day Month Year,感觉像是一个节点保存了指向其它三个节点的“指针”,而其它的三个类互不干扰,我觉得这样的设计更有效率,也更具安全性和可迭代性。
实际上这两题都是以DateUtil为业务类,然后Day Month Year为实体类,核心都一样,但是只有聚合的方式不同。我认为 聚合一 的聚合方式使得程序表现得像线性结构,遇到 bug 时可能会出现一个类出现问题然后所有类都出问题的情况, 聚合二 则使每个类都有很高的独立性,只在 DateUtil 类中耦合其它的类都保持独立,在一个类出现bug的时候不会引起多个类同时出现问题的窘迫情况。
③核心代码分析
搞清楚题目要求实现的三大功能:求前N天,求后N天,求两个日期之间相差的天数.
public DateUtil getPreviousNDays(int n){
      从本质上是做一天一天的减法日期向前推
              while (day.getValue()- n < 0){
                    if(day.getMonth().getValue() > 1){
                         day.getMonth().monthDecrement();
                    }
                    else {
                        day.getMonth().getYear().yearDecrement();
                        day.getMonth().resetMax();
                    }
                    day.setValue(day.getValue()+day.getMon_maxnum()[day.getMonth().getValue()-1]);
              }
              day.setValue(day.getValue() - n);
              DateUtil dateUtil = new DateUtil(day);
              return dateUtil;
        }
        public DateUtil getNextNdays(int n){
              day.setValue(day.getValue() + n);
              int i = day.getMonth().getValue();
      与求前N天的思路一样,做日期的一天天的加法,然后月份随着日期到达最大值后进1
              for(; day.getValue() > day.getMon_maxnum()[i-1];i++){
                    day.setValue(day.getValue() - day.getMon_maxnum()[i-1]);
                    if(i == 12){
                          i = 0;
                          day.getMonth().getYear().yearIncrement();
                    }
                    day.getMonth().setValue(i+1);
              }
              DateUtil dateUtil = new DateUtil(day);
              return dateUtil;
        }
        public boolean isLeapYear(int year)//判断year是否为闰年
        {
              return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
        }
        private static final int[] mon = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
        public int getDaysofDates(DateUtil date)//求当前日期与date之间相差的天数
        {
              DateUtil dateUtil1 = this; // 小
              DateUtil dateUtil2 = date; // 大
      调用比较两个日期大小的方法
              if (this.compareDates(date)) {
                    dateUtil1 = date;
                    dateUtil2 = this;
              }
              int days;
              int leapYearNum = 0;
先统计两个日期年份之间有多少个闰年,然后根据这个闰年个数来使得日期 +1
for (int i = dateUtil1.getDay().getMonth().getYear().getValue(); i < dateUtil2.getDay().getMonth().getYear().getValue(); i++) {
                    if ((isLeapYear(i))){
                          leapYearNum++;
                    }
              }  
计算两个日期年份的差值 *365 然后加上闰年的个数即可得到结果
days = 365 * (dateUtil2.getDay().getMonth().getYear().getValue() - dateUtil1.getDay().getMonth().getYear().getValue()) + leapYearNum;
              int d1 = mon[dateUtil1.getDay().getMonth().getValue() - 1] + dateUtil1.getDay().getValue() + (dateUtil1.getDay().getMonth().getValue() > 2 &&         (isLeapYear(dateUtil1.getDay().getMonth().getYear().getValue()))?1:0);
              int d2 = mon[dateUtil2.getDay().getMonth().getValue() - 1] + dateUtil2.getDay().getValue() + (dateUtil2.getDay().getMonth().getValue() > 2 &&         (isLeapYear(dateUtil2.getDay().getMonth().getYear().getValue()))?1:0);
              return days - d1 + d2;
        }
两道题目用的基本都是同一个算法,代码除了聚合的形式不同,其它的大同小异。
日期对象设计 聚合小结
对于两种的聚合方法,要做出一个选择的话,我更倾向于选择聚合二,因为类似树状的结构使得代码之间的耦合程度更低,达到了我内心深处的“高聚合,低耦合”的执念。
三、图形继承设计(题目集04 7-3 题目集06 7-5 7-6)
①度量分析:
三道题的难度较低,就不搞圈复杂度的测试。
先上类图(题目集04 7-3):Shap 作为一个父类里面只有一个关于求面积的方法,这个方法在子类中得到重写
    
然后 Box 类 和 Ball 类分别继承了Rectangle 和 Circle 类,具有一个特殊的求体积方法
              
            
  
           
          
写起代码来很轻松,但题目中要求输出
    Constructing Shape
    Constructing Rectangle
    Constructing Box这样的一组结果,导致一开始无从下手。但是我想到可以在构造方法中就输出这个结果,Shape类中就输出
    Constructing Shape
    然后以此类推在每个构造方法中都增加这个输出的语句。
题目集06 7-5
先输入三个数字表示三个对象的数量(这里加一个判断数量不能小于 0 ),然后再依次输入相应属性的值。
然后再判断输入的数据是否合法,比如三角形三边必须要满足 两边之和大于第三边
再提交代码后,发现有一个圆属性非法的测试点没过,这让我感到很诧异,当我把 0 作为圆的半径传入时,可以得到面积为0.00的输出结果,但是仍然通过不了PTA的测试点。在同学的提示下:当圆的半径为 0 时就不再是一个圆而是一个点,所以更改了圆属性合法性校验的判断语句,当半径为 0 时报错。
①度量分析:
类图:
      
最大圈复杂度为16,出现了很多重复的步骤,可以将这些重复的方法拎出来写一个新方法然后反复调用。
       
②算法分析:
给定了三个对象的各自的数量,利用循环分别创建对象。可以通过使用 ArrayList 获取创建的对象
ArrayList<Shape> shapeArrayList = new ArrayList<Shape>()
利用类的多态性 Shape shape = new Circle(input.nextDouble());这样可以创建一个Circle类型的对象
                    
其它的对象以此类推,将求出来的面积存入 shapeAreaArrayList 链表中。求出面积后需要对面积进行排序,我选择了冒泡排序的算法。最近迷上了增强for循环语句,我感觉这样的循环效率会很高。
③代码分析
在针对链表的冒泡排序时,我一时想不到链表的排序操作或者方法,所有我选择将shapeAreaArrayList 链表转化为一个 Object 类型的数组,里面这个链表里面所保存的类型是 double 类型,所以在排序的时候将每一项都强制转化为 double 类型就可以进行排序了
             
④小结
图形多态与继承题目中一直设计到父类中定义的方法在子类中实现或者得到重写,然后再主方法中调用父类创建一个子类,使它多态化。
我认为多态的作用可以使得代码更加简洁,在父类中给出方法的签名或者大体细节,然后子类中对父类的方法进行重写。
题目集06 7-6
这道题要求使用接口,我认为接口是一个保存了方法签名的一个集合。每一个类在使用接口的时候,可以对接口给出的方法进行重写
根据题目类图设计可以得到接口
         
算法倒是和前两道图形继承与多态的类似,都是获取对象,然后根据对象里求面积的方法进行计算,然后返回相应的值。
图形多态与继承设计 小结
抽象类一般被使用为一种继承关系,而一个类只能继承一个类,通俗的讲它只有一个父类(虽然Object是所有类的父类)。一个类却可以实现多个interface,Java 中没有多重继承的概念,所以可以通过实现多个接口来达到多重继承,可能达不到,但应该算是折中的考虑。
在使用抽象类的时候,它的派生类必须实现抽象类中所有的方法,让我在写代码的时候感到很头痛,因为我在它的派生类中并不需要实现这个方法,不需要使用这个方法。我还是觉得接口更具有效率。
四、Map集合的初次使用 和 正则表达式使用心得
题目集04的7-1题 水文数据校验及处理
只能说这道题目非常的搞人心态,需要使用正则表达式来匹配输入的水文数据,由于数据是一行一行的输入,所以我在进行匹配和处理的时候也是一行一行的处理。
- 如果每一行输入数据不是由“|”分隔的五部分,则输出:
Wrong Format Data:输入的数据
- 如果某一部分数据有误,则按如下方式显示:
Row:行号,Column:列号Wrong Format Data:输入的数据
所以我选择先使用split("\\|")将输入的每一行看作一个字符串,然后拆分成字符串数组,如果字符串数组的大小小于5,那么就是错误的输入。
拆分完字符串数组后,就只剩下 (\\d+)/(\\d+) 这样类型的数据了,通过使用这个正则表达式,匹配,然后再用split(“/”)方法再将这两组数据拆分开,最后所有的数据就是以字符串数组的形式存取了。
既然已经获取到了数据,那么处理也就很简单,一行一行判断数据是否有误或者得到水流量总和。
题目集05 (7-4)
这道题需要我在一段很长的字符串中找的Java语言中的关键字,并统计它们的个数,按照字典顺序进行排序输出。
在一开始不了解 Map 类 的时候我想使用两个链表,一个保存找出的关键字,另外一个保存它的个数,但是在感觉上实现起来非常复杂直接放弃。然后看了老师在“解题报告”中给的提示,我去看了关于 HushMap 的知识,它可以在存入数据的时候保留一个 “ Key ” 和一个 “ valve ”,也就是说我可以同时将找到的关键字和它出现的次数存入 HushMap 中。
但 HushMap 在做数据储存时是一个无序的存储,给我在做输出的时候造成了很大的困难。然后找到了 TreeMap 类,它可以将存入的数据做到一个关于“Key” 按字典顺序进行排序。所以我在使用 HushMap 存入数据后然后强制转换为 TreeMap 类,最后使用迭代器输出。
              
最大复杂度为14,应该是使用了多个嵌套的循环和一些没用的 if - else 语句,for - each的执行效率要比for循环的要高以后得多用for - each。
①解题过程
输入:
字符串的输入是一个问题,因为输入的字符串是一个“java 源码”是具有换行的输入的,所以我选择循环使用 nextLine 读入直到最后一行为“exit”就终止循环。由于String 类生成的字符串是一个不可变的,所以我选择使用 StringBuilder 来 储存输入的字符串。不过在“java 源码”中会遇到以双斜杠的注释情况,题目中也提示到:注释中的关键词是不用计数的。
这个时候可以使用正则表达式 “ (.*)//(.*) ” 来匹配符合双斜杠注释的情况。因为输入的时候是一行一行地输入,所以这一行就是可以这样匹配。通过这样的正则表达式加上字符串分成字符串数组的 split()方法。将双斜杠前后分组,然后我只需要前一组就行了。(后一组是注释的内容不加入StringBuilder 即可)
            
接下来就将StringBuilder 类型再转换成Sring类的字符串。
处理:
继续处理出现注释的情况。
常见的注释分为 /* */ 和 /** */ 两种形式,这两种形式是可以实现隔行注释(刚刚已经把双斜杠的处理掉了)。在将StringBuilder 转为String类型后整个字符串是很长的一行,那么使用正则表达式“\(.*?)\” 和 “/\\**(.*?)/” 可以匹配到注释的内容使用group方法进行分组,虽然只有一组。然后调用replace()方法将整个组给替换成空格,这样就把注释的内容给删除了。
最后整个字符串中就只剩下符号了,这道题只要求匹配单词所以可以将所有非字母的字符全部替换为空格,使用正则表达式“[^a-zA-Z]”匹配到了之后然后再用replaceAll()方法全部替换成空格。
这样整个字符串中就只剩单词和空格了,将整个字符串拆分成字符串数组,使用正则表达式“\\s”和 split()方法。
得到了一个所有元素都是一个单词的字符串数组。
   使用Map;
先通过循环把获取到的关键词作为 Key 存入 HushMap 中,我把这个过程叫做初始化
            
故技重施,在来一次这样的循环,但是增加一个变量count作为获取出现的次数。
             
关键步骤就结束了,接下来就是输出了。
输出(尝试使用迭代器输出):
按照开始的思路,将HushMap转换为TreeMap类型,这样我就获取到了按字典顺序排完序后的 Map
    
②使用心得
通过使用正则表达式在面对复杂的而又具有某种特性的字符串可以大显身手。就比如:在匹配电话号码或者QQ号这种实际案例时可以使用正则表达式“[0-9].*”来匹配含有多个数字的字符串,然后再根据具体的要求写出判断语句进行修改即可。
正则表达式对于字符串的处理确实方便,但是在进行正则表达式的编写的时候才是最累的。我喜欢把正则表达式写的简单点,使它尽量的短一点。虽然不知道有什么用,但是就是喜欢这样做
对于Map 和 List 之类的集合,可以说是非常有用。在学C语言的时候链表是一个神奇的数据结构,它存结构体的指针,然后结构体中存数据。
List就像是一个包装好了的链表,我可以使用各种方法对链表中的数据进行增 删 改 查,而且还能够使用自定义的类型,List 的增加和删除的方法比数组简单的多,但是在进行排序的时候我还是选择了将List转换为数组然后再排序。
在学Map 的时候无意间看到了哈希表的一种数据结构,哈希表就是一种链表数组,数组中的每一个元素都是一个链表,不过我还不知道它的存入和读取的效率。数组和链表是常见的数据结构,说实话我还不太了解Map类的底层知识,我想要知道这是数据结构还是一个类(把数据存在类里)。
五、第二月课程总结与教学改进建议
在面向对象的程序设计中,对象是最关键的,如何设计类和方法将会是我在java路上的陪伴,写几个类谁都会写,但是否能够高效的将题目做完通过测试那才算学会。正则表达式算是可以接受和理解了,在上个月做简单函数求导的题目还根本理解不了正则表达式的意义,但现在至少能够顺利的使用正则表达式完成PTA上简单的题目。
然后就是日期对象设计的两种聚合方式,我想了很久想清楚了这两种聚合方式的不同和优缺点。
请允许我打一个很差劲的比喻:聚合一的设计就像是一条单线程,而聚合二的设计就像是多线程;聚合一想要得到正确输出结果,需要全部类贯穿性调用,而聚合二就是独立处理,结果集成这就是两种聚合方法的不同之处。
聚合一的缺点:①可视性差,代码写的非常复杂,在阅读起来有很大的困难。②迭代性差。③耦合度过高,基本上所有的类都耦合在了一起,这使得耦合度高违背了“单一职责”的类设计原则。(优点就没找着,可能是我以一种对比的思路进行找,与聚合二的设计差远了)
聚合二的优点:①可视性强,通过类图可以很清晰的得到各个类之间的关系。②迭代性高,实体类都是独立的,在维护上很方便。③代码的执行效率高,因为它可以像是多线程一样,彼此独立运行然后结果汇总。
所以可以得出,由于聚合一的设计是一条线性的,节点与节点之间都高度耦合,代码的可复用性差。其实我在做聚合二题目之前试图将聚合一的代码复制过来然后稍微改一下的,但是改着改着就发现很难改,就还是决定重新设计。(我认为这也算是可复用性差的一种体现),聚合二的设计使得它实体类都是独立互不干扰的,每个类都执行特定的功能和职责,功能已经模块化,可复用性就很强。
此外,我仍然想提一下我的观点,希望老师可以私信我。我认为聚合一的设计类似一条链表,每一个节点都是不同的类型(不同类型的结构体),然后都保留了指向下一节点的指针(Day类中有一个私有属性Month,Month类中有Year),但是在执行的时候往往是遍历链表需要频繁的访问节点,比如从表头的DateUtil节点想要获取有关year的value需要向后走三个节点才能够得到值,这才使得聚合一的代码执行效率低。
              
聚合二的设计类似一个树杈,以DateUtil为主节点然后生成三个树枝(Day,Month,Year),它保留了访问其它三个节点的指针,在执行的时候每一个节点的访问都是独立的,计算的过程也是独立的,所以聚合二的执行效率更高。我才不会说这样的设计类图看起来比聚合一更轻松。(暂时对画图软件不熟练,没有画出树杈状的)
              
对于新的数据结构 List 链表,它的增删改查的效率很高,对于一个已经确定了规模的数组,它在做删除和增加元素的时候显得十分困难,然后List也有一个可以转换为数组的方法,也相当于获取了数组查找速度快的优点。Map等其它的集合现在还用的少,只在查找关键字的时候用到过,但它能够将Key和value连接起来一起储存,甚至还能按照字典顺序排序。
最近查阅资料,提到:接口优与抽象类,因为为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。而接口却没有这样的限制,类也不能多态继承,但是一个类可以有调用多个接口。
希望未来的面向对象程序设计题目能够有更多的解法,希望我能够找到更多的解法。
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号