OO第三单元总结 —— JML规格
一、作业架构设计与性能分析
1. 第九次作业
-
架构设计
在本次作业中,类与方法的设计大致完全依照官方包提供的接口实现,故不再赘述。
-
图模型构建与维护
在本单元的作业中,均选择了使用链表的形式来实现图的相关操作。
在本次作业中,以
Network
为图,使用HashMap
存储结点Person
:private final HashMap<Integer, Person> people;
并以
person
的Id
为键值,便于后续进行查找操作。结点
person
中,使用HashMap
保存了其所连接的所有节点:private final HashMap<Integer, Person> acquaintance; private final HashMap<Integer, Integer> value;
通过观察发现只有
ap
、ar
两条指令会影响图,实现方式为:ap
:创建一个新的Person
对象,初始化后放入Network
的HashMap
中ar
:在两个人的HashMap
中分别加上对方。 -
性能问题分析
作为本单元第一次任务,这次作业中对性能的要求并没有很高,并没有出现严重的性能问题。
但这并不代表程序一定是正确的,事实上在此后的两次作业中都出现了由之前的作业中已经实现的方法所引发的性能问题 ,具体的分析见下文。
2. 第十次作业
-
架构设计
在本次作业中,类与方法的设计依旧大致依照官方包提供的接口实现。
-
图模型构建与维护
在本单元的作业中,延续使用了上一次作业的基本框架,并根据新的需求加以补充:
在
Network
和person
中新增了一个存储message
的容器用于完成相关功能:// MyNetwork.java private final HashMap<Integer, Message> messages; // MyPerson.java private final ArrayList<Message> messages;
新增的
Group
中也同样设置了一个ArrayList
,用于记录小组的成员。private final ArrayList<Person> people;
以上容器均根据相应的指令进行元素的插入、查找与删除。
此外,在整个图中维护了一个代表了连通分量数目的变量
blockSum
来直接给出queryBlockSum()
函数的返回值,以及一个标志该变量是否需要更新的布尔变量changed
:private boolean changed = true; private int blockSum;
经观察发现只有
ap
和ar
两条指令会涉及blockSum
的修改,因而在这两条指令的处理函数末尾将changed
置为true
。而在调用
queryBlockSum()
时若changed
为false
,则直接返回blockSum
的值,否则重新计算并更新blockSum
。public int queryBlockSum() { if (people.size() == 0) { return 0; } if (!changed) { return blockSum; } int sum = 0; ArrayList<Integer> peopleID = new ArrayList<>(); ArrayList<Integer> checking = new ArrayList<>(); for (Person index : people.values()) { peopleID.add(index.getId()); } do { checking.add(peopleID.get(0)); peopleID.remove(0); do { Integer[] acs = ((MyPerson) people.get(checking.get(0))).getAcquaintances(); checking.remove(0); for (int index : acs) { if (peopleID.contains(index)) { peopleID.removeIf(integer -> integer == index); checking.add(index); } } } while (!checking.isEmpty()); sum++; } while (!peopleID.isEmpty()); blockSum = sum; changed = false; return sum; }
-
性能问题分析
在本次作业中对
qbs
指令的执行出现了较严重的性能问题,主要是由于执行大量的qbs
指令时,对每一条指令都进行重新计算,进而导致CPU超时。修改时选择记录每一次计算的结果,并在结果没有改变时直接返回之前的计算结果,具体的代码实现见上。
除此之外,其他指令的执行也存在性能问题,但暂未在此次评测和互测中发现。
跑得了和尚跑不了庙
3.第十一次作业
-
架构设计
在上次作业中
qbs
指令出现了严重的性能问题,且在BUG修复环节使用的方法无法从根本上处理这一问题。本次作业在官方包的基础上新增了类
MyBlock
,作为链接Network
和Person
的桥梁,用于处理qci
、qbs
等指令。 -
图模型构建与维护
在本次作业中仍然以
Network
为图,以Person
为结点,但在其间插入了Block
类,代表连通分量。在
Network
类中新增了用于储存表情信息的HashMap
,以及存储连通分量的ArrayList
:private final HashMap<Integer, Integer> emoji; private final ArrayList<MyBlock> blocks;
在
Group
类中新增了用于记录ageSum
和valueSum
的变量:private int valueSum; private int ageSum;
在
Person
类中新增了其所属Block
的引用,并且合并了存储联系人及其value
的HashMap
:private final HashMap<Integer, Integer> value;private MyBlock block;
而新增的
MyBlock
类中包含了一个id
,以及用于存储其内包含的成员的ArrayList
:private final int id;private final ArrayList<Person> people;
经过观察得知,在处理
ap
、ar
、atg
、dfg
等指令时会涉及到Block
类和各类中记录变量的更新。在处理
ap
指令时,新加入的Person
在图中为孤立点,因而为其创建一个Block
并将其加入:public void addPerson(Person person) throws EqualPersonIdException { if (contains(person.getId())) { throw new MyEqualPersonIdException(person.getId()); } people.put(person.getId(), person); MyBlock block = new MyBlock(person.getId()); block.addPerson(person); blocks.add(block);}
在处理
ar
指令时,先将两个Person
加入到对方记录联系人的HashMap
中,之后如果两个人不同属一个Block
,则将二者的Block
合并,之后再更新所有相关group
中valueSum
的值:// MyNetwork.javapublic void addRelation(int id1, int id2, int value) throws PersonIdNotFoundException, EqualRelationException { if (!contains(id1)) { throw new MyPersonIdNotFoundException(id1); } if (!contains(id2)) { throw new MyPersonIdNotFoundException(id2); } if (people.get(id1).isLinked(people.get(id2))) { throw new MyEqualRelationException(id1, id2); } MyPerson person1 = (MyPerson) people.get(id1); MyPerson person2 = (MyPerson) people.get(id2); person1.addAcquaintance(person2, value); person2.addAcquaintance(person1, value); if (person1.getBlockId() != person2.getBlockId()) { if (person1.getBlock().getSize() > person2.getBlock().getSize()) { blocks.remove(person2.getBlock()); person1.getBlock().mergeBlock(person2.getBlock()); } else { blocks.remove(person1.getBlock()); person2.getBlock().mergeBlock(person1.getBlock()); } } for (Group group : groups.values()) { ((MyGroup) group).addRelation(person1, person2, value); }}// MyBlock.javapublic void mergeBlock(MyBlock block) { for (Person person : block.getPeople()) { addPerson(person); }}// MyGroup.javapublic void addRelation(Person person1, Person person2, int value) { if (people.contains(person1) && people.contains(person2)) { valueSum += (value * 2); }}
在处理
atg
和dfg
指令时,除了需要将Person
对象加入或删除外,还需要更新valueSum
的值:public void addPerson(Person person) { for (Person index : people) { if (index.isLinked(person)) { valueSum += (2 * index.queryValue(person)); } } ageSum += person.getAge(); people.add(person);}public void delPerson(Person person) { for (Person index : people) { if (!index.equals(person) && index.isLinked(person)) { valueSum -= (2 * index.queryValue(person)); } } ageSum -= person.getAge(); people.remove(person);}
-
性能问题分析
本次性能问题主要出在了
qgvs
指令的处理上。最初在编写代码时完全按照所给的规格实现所需要的的功能,用如下的方法处理了
qgvs
指令:@Overridepublic int getValueSum() { int res = 0; for (Person index : people) { for (Person index2 : people) { res += index.queryValue(index2); } } return res;}
由于对性能问题不是很敏感,没有意识到这个$O(n^2)$的方法被大量调用时会导致严重的性能问题。
在修复BUG的过程中,选择在
Group
对象中添加新的域valueSum
来记录当前时刻getValueSum()
方法的返回值,且观察到只有atg
,ar
和dfg
三条指令可能会影响到该方法的返回值,因此在这三条指令的处理函数(及相关函数)中添加了更新valueSum
值相关的代码,具体代码见上。修改后大幅度提升了执行大量
qgvs
指令时的性能,并且经过本地粗略的测试认为在处理大量与valueSum
更新相关的指令时也不会产生严重的性能问题(大概)。
二、实现与测试策略
-
实现规格的设计策略
在刚开始进行本单元的作业时,完全按照规格所写来实现代码,即将规格“翻译”为java代码。
这种做法显然是不太妥当的,因为这样并没有体现出JML规格的作用,也可能是其作用应该更多的在测试环节中体现;另外,这样的做法也会在性能上出现很大问题。
在前期一直使用这样的策略,直到开始完成下面这个方法:
/*@ 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();
看似简单的实现方式,实则多次调用了
isCircle()
方法,性能很差。从这里开始在理解的基础上调整方法的实现,大致的流程如下:
graph LR A(阅读JNL规格) -->B(理解含义) B --> C(按照理解编写代码) C --> D{测试} D --> |发现BUG| E(修改) E --> D D --> |未发现BUG| F(结束)但是这样仍然无法避免所有的性能问题,因为有些方法所要求的返回值在多次计算时很难保证性能,因而还需要通过添加一些规格内没有要求的逻辑来辅助实现。
-
设计测试的方法和策略
-
随机数据
大量的随机数据是测试时最简单易行的方法之一,这样可以快速地发现程序中明显的逻辑错误,也可能会发现一些难以察觉的、复杂的错误。
-
针对特定指令多次调用
部分指令如
qbs
、qgvs
等,其实现很容易出现性能问题,多次调用,或以某种特定组合多次循环调用可以发现性能问题。 -
边界测试
针对JML规格中规定的数据范围的边界进行测试,可以发现对于数据处理不当的问题。
-
三、容器的选择和使用
在本单元的作业中,主要使用了HashMap
和ArrayList
两种容器,大致的区别如下:
-
HashMap
对于查找操作有着较高的性能,因而可以用于需要反复进行查找操作的对象。
在一个结点的
key
和value
两个位置可以存储两个相关的数据,因而可以归结为类似关系的数据可以使用这种容器来存储,进而加强相关数据之间的联系。但需要注意在大多数情况下不能以真正的
value
作为HashMap
的key
,因为这样可能会导致键值的重复,从而引发错误。 -
ArrayList
和前者相比来讲,属于一种简单的容器,因而主要应用于一些没有特殊要求的数据。
在本次作业中,尤指那些不需要频繁查找,可以多次添加但尽量不要删除的数据。
在适当的时候使用简单的数据结构可以提高性能,不过在这次左右中貌似主要的性能问题并不是由此引起的。
看似简单,实则不然。
用时较少但学到很多的一个单元。
感谢大家的帮助,希望大家能够在最后一个单元打好收官之战~
2021年6月1日