面向对象程序设计第三单元总结

Posted gxy0615

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象程序设计第三单元总结相关的知识,希望对你有一定的参考价值。

面向对象程序设计第三单元总结

PART1 实现规格采取的设计策略

一般而言,JML 有两种主要的用法:

(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。

(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

本单元实现JML规格,是采用了JML的第一种用法:通过完全参考JML规格从而实现我们的代码。因此,在实现代码之前,需要了解JML规格的几个特点,前人已经总结过很多,这里不再赘述了。不过需要注意的是,规格的前置条件、后置条件、不变式等都只是一个结果,而中间实现的过程是我们自己设计的,如中间变量、存储变量的容器,求得result使用的算法等等

综上,我认为在实现规格时采取的设计策略大体如下:

  • 阅读规格,首先重点了解规格让我们求解的result的意义或者过程是什么,如作业中的平均值、方差等,将规格的代码理解成我们比较熟悉的东西,当然有一部分需要求解的东西并不是我们日常生活中有所定义的东西。
  • 选取容器,阅读完某一类的规格后,应该知道这个类中需要哪些变量,并且根据它们的使用情况选择应该使用的容器,本单元作业大多数采取的容器为ArrayList和HashMap。
  • 以下分两种情况 。
    • 如果你是一个大佬,那么你已经通过前序步骤得知要求解的result是什么,并且一定知道应该用哪一种算法能够有最好的性能,以及一些附加的变量的添加(就像老师课上说的,增加冗余性换取性能)。那么,你可以直接实现性能较高的算法实现JML规格。
    • 如果你不能立刻知道这个规格应该用什么算法实现是最优的,那么,首先把功能实现,并保证准确性。在功能实现后再考虑是否有优化的空间。可能的优化如下。
      • 使用一些容器进行优化,例如用HashMap、HashSet、ArrayList等,具体容器的选择取决于功能。
      • 预处理,提前把变量进行保存,方便计算,如在第十次作业中,在向Group中addPerson、deletePerson的时候预处理,从而把O(n)的方法优化为O(1)。
      • 使用算法优化时间复杂度,比如使用并查集、堆优化的迪杰斯特拉算法等。
  • 代码编写完成后,再次阅读规格,检查自己的代码是否满足规格的前置条件、后置条件、不变式等,以及一些细节的问题如异常的抛出是否符合顺序,以防出现纰漏。

PART2 基于JML规格设计测试的方法和策略

JUnit

JUnit是一个Java语言的单元测试框架,一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

当然JUnit测试存在优点和缺点。优点是能够自己编写测试代码进行自动测试,并且可以把一个大的工程分解为一个个小的项目分别进行测试,更有助于定位bug和测试。缺点是由于测试代码是程序员自己编写的,所以可能存在缺陷,可能有某些情况并没有测试到。

OpenJML

OpenJML最基本的功能是对JML规格的完整性进行检查,包括类型检查、变量可见性与可写性的检查等。

JMLUnitNG

JMLUnitNG会根据JML规格自动生成测试样例,主要是针对边界数据进行测试。

PART3 容器选择和使用的经验

在本单元中,我主要使用了HashMapPriorityQueue两种容器,其中只有最后一次作业为了优化迪杰斯特拉算法时用到了优先队列。

我们观察这次作业,Person类、Message类、Group类都有自己独一无二的Id,因此使用HashMap能够更快的对元素进行增加、删除、查询的操作,而且在这一次的作业中的确有很多增加、删除、查询的方法,如Person类中的addRelation、addMessage为增加操作,Group类中的hasPerson为查询操作,delPerson为删除操作,addPerson为增加操作。因此在使用这些操作比较多的时候,优先选择HashMap容器进行操作。

PART4 本单元容易出现的性能问题

  • 第九次作业
/*@ public normal_behavior
      @ requires contains(id1) && contains(id2);
      @ ensures \\result == (\\exists Person[] array; array.length >= 2; 
      @                     array[0].equals(getPerson(id1)) && 
      @                     array[array.length - 1].equals(getPerson(id2)) &&
      @                      (\\forall int i; 0 <= i && i < array.length - 1; 
      @                      array[i].isLinked(array[i + 1]) == true));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id1);
      @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2);
      @*/
public /*@pure@*/ boolean isCircle(int id1, int id2) throws PersonIdNotFoundException;

/*@ ensures \\result == 
      @         (\\sum int i; 0 <= i && i < people.length && 
      @         (\\forall int j; 0 <= j && j < i; !isCircle(people[i].getId(), people[j].getId()));
      @         1);
      @*/
public /*@pure@*/ int queryBlockSum();

第九次作业中NetWork类的isCircle方法和queryBlockSum方法容易超时,其中,如果isCircle方法按照规格的方式写的话时间复杂度为O(\\(n^2\\)),而在queryBlockSum中甚至需要多次调用isCircle算法,使得时间复杂度达到O(\\(n^3\\))。

因此,在isCircle中采取并查集的算法,并且采取了按秩合并和压缩路径的优化,复杂度为O(\\(log_{n}\\))。

BlockSum采取预处理的方式,再加一个人的时候blockSum++,在addRelation的时候判断两个Person的Father是否相同,如果不同则blcokSum--,复杂度为O(1)。

  • 第十次作业

预处理,提前把变量进行保存,方便计算,如在第十次作业中,在向Group中addPerson、deletePerson的时候预处理,从而把O(n)的方法优化为O(1)。预处理方法如下:

public void addPerson(Person person) {
    size++;
    ageSum += person.getAge();
    ageMean = getAgeMean();
    ageVar = ((person.getAge() - ageMean) * (person.getAge() - ageMean));
    for (Map.Entry entry : persons.entrySet()) {
        MyPerson person1 = (MyPerson) entry.getValue();
        int age = person1.getAge();
        ageVar += ((age - ageMean) * (age - ageMean));
        valueSum = valueSum + person.queryValue(person1) * 2;
    }
    ageVar /= size;
    persons.put(person.getId(), (MyPerson) person);
}

public void delPerson(Person person) {
    size--;
    ageSum -= person.getAge();
    ageMean = getAgeMean();
    ageVar = 0;
    persons.remove(person.getId());
    for (Map.Entry entry : persons.entrySet()) {
        MyPerson person1 = (MyPerson) entry.getValue();
        int age = person1.getAge();
        ageVar += (age - ageMean) * (age - ageMean);
        valueSum = valueSum - person.queryValue(person1) * 2;
    }
    if (size != 0) {
        ageVar = ageVar / size;
    }
}
  • 第十一次作业

Network类中的sendIndirectMessage中需要求解加权最短路径,采取堆优化的迪杰斯特拉算法。时间复杂度 O(\\(mlog_{n}\\)), n 表示点数,m 表示边数。

distanceMap.put(id1, 0);
PriorityQueue<Pair> pairs = new PriorityQueue<>();
pairs.add(new Pair(id1, 0));
while (!pairs.isEmpty()) {
    Pair pair = pairs.peek();
    if (pair.getId() == person2.getId()) {
        return pair.getDistance();
    }
    pairs.remove();
    if (flagMap.get(pair.getId())) {
        continue;
    }
    flagMap.put(pair.getId(), true);
    pairs.remove(pair);
    MyPerson personFrom = (MyPerson) getPerson(pair.getId());
    int dis = pair.getDistance();
    HashMap<Integer, Integer> valueMap = personFrom.getAcquaintanceValueMap();
    for (Integer personId : valueMap.keySet()) {
        if (distanceMap.get(personId) > dis + valueMap.get(personId)) {
            distanceMap.put(personId, dis + valueMap.get(personId));
            pairs.add(new Pair(personId, distanceMap.get(personId)));
        }
    }
}

PART5 作业架构设计

整体架构

这次作业的整体架构官方代码已经通过接口和JML的方式给出,其中,NoticeMessage、EmojiMessage、RedEnvelopeMessage继承了Message,Message中使用了Group、Person,Group中使用了Person,Person中使用了Message,Network中使用了Person、Group、Message,为最顶层的一个类。

下图为接口的UML图:

在自己的类中,为了实现堆优化的迪杰斯特拉算法,新增Pair类,具体的架构如下。

下图为自己实现的类的UML图:

图模型构建与维护策略

本单元作业为构建一个关系网络,其中Person为节点,Relation为边,Value为权重。边在Person类中用HashMap进行存储,addRelation的时候同时在两个Person中添加边。两个节点是否在同一个最大联通子图中(isCircle)采取的是并查集算法,在NetWork中使用HashMap存取PersonId和祖先的Id,在每一次addRelation的时候更新。Group中也采取HashMap保存Group中的Person。

PART6 心得体会

规格化语言的确可以解决一些文字表述上存在歧义的问题,但是也存在一些缺陷,可能这就是现在并没有得到广泛推广和使用的原因吧。

还剩最后一个单元了,冲!

以上是关于面向对象程序设计第三单元总结的主要内容,如果未能解决你的问题,请参考以下文章

面向对象第三单元总结

面向对象程序设计第三单元总结

《面向对象程序设计》第三单元 JML 总结

面向对象第三单元总结

2020面向对象设计与构造 第三单元 博客总结

面向对象第三单元总结