BUAA OO第三单元总结
前言
本单元的代码任务集中在了学习JML的使用,并根据所给JML实现相应的方法和类。
契约式编程、防御式编程与进攻式编程
契约式编程
契约式编程要求我们在「前提条件」、「后继条件」和「不变量条件」进行契约的检查。类似的,例如检查参数,一旦参数不对,当即撕毁契约。
比如后端的方法因为传入的参数不在设计范围内而导致错误,这时就可以去找前端调用方,要求前端按照前置的设计来进行调用。
约束对象:调用者与被调用者
防御式编程
“人类都是不安全、不值得信任的,所有的人,都会犯错误。”而你写的代码,应该考虑到所有可能发生的错误,让你的程序不会因为他人的错误而发生错误。
思路1:运行前检查参数是否合规,不合规则报错
思路2:对不合规的参数提供缺省值
约束对象:被调用者
进攻式编程
认为是防御式编程的分支,不同的是,进攻式编程让错误明显地存在。
进攻式编程针对的两种错误:
可预期错误(Expectable errors)
-
无效的用户输入(顾客走进酒吧要了一杯null)
-
内存耗尽(数组用完了)
-
硬件出故障(突然断网)
可预防错误(Preventable errors)
-
函数参数不合法(传进来一个null参数)
-
值超出预定范围(比如爆int)
-
返回值异常(比如返回值不在switch的case中)
体现进攻式编程的操作:
-
把数组的大小开得刚刚好
-
不初始化数组,或者初始化成非0的值
-
对所有可能错误的参数都进行检查,出现异常直接throw
-
不对不合规的参数提供缺省值,而是直接报错(区别于防御式编程)
图模型构建与维护策略
图模型的构建
由于JML的规格已经给定,其实自行发挥的空间是比较小的。观察别人的代码后,发现有人维护了JML规格以外的类去处理一些功能。但由于实际上,
由于每次架构都是迭代的增量开发,三次作业当中并未重构,因此,这里直接展示第三次作业后的类图:
理解java对于类的引用的原理,会发现其所有的引用使用的都是指针,因此,在发送消息这一功能的实现当中,递归下降地调用子类的子函数,和在Network
中处理,性能上是差不多的。
由于编码简单,笔者在类似的问题中都选择了在MyNetwork
类中统一完成,优点是所有需要用到的变量都已经被放在了MyNetwork
,互相引用非常方便。缺点就是复杂度会集中在MyNetwork
当中,复杂度的平衡就控制得比较不好。
性能问题和修复情况
性能优化策略
存储中间变量
例如,求取Group
中Person
年龄的平均值,一般的做法如下:
@Override
public int getAgeMean() {
int sum = 0;
for (Person p : people) {
sum += p.getAge();
}
return people.size() == 0 ? 0 : (sum / people.size());
}
虽然取年龄的平均值并不是一个复杂度很高的指令,但这里面其实是有优化空间的,比如小组中人的年龄的平方和是可以提前存好的,最终的的复杂度可以简化成:
@Override
public int getAgeMean() {
return (totalAgeSquare - 2 * mean * totalAge + mean * mean * people.size()) / people.size();
}
无底线地使用HashMap
观察所有待实现的方法,将所有本为数组的元素都转变为一个或多个“从检索到被检索元素”的HashMap,会让后续的遍历、筛选、检索变得易于实现,且性能良好。
如我在Network
类中,按照以下的方式实现了所需的几个属性。
/*@ public instance model non_null Person[] people;
@ public instance model non_null Group[] groups;
@ public instance model non_null Message[] messages;
@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@*/
private HashMap<Integer, Person> id2person = new HashMap<>();
private HashMap<Person, Integer> person2id = new HashMap<>();
private HashMap<Integer, Group> id2group = new HashMap<>();
private HashMap<Integer, Message> id2message = new HashMap<>();
private HashMap<Integer, Integer> emojiId2heat = new HashMap<>();
private HashMap<Integer, HashSet<Integer>> emojiId2messages = new HashMap<>();
于是乎,所有的查询操作都可以通过O(1)的方法完成,示例如下:
@Override
public Message getMessage(int id) {
return id2message.get(id);
}
但是这也就意味着,迭代的时候,增删元素务必记住要处理所有hashmap的增删。这一点非常容易出错,这个在下文的bug修复中有提到。
算法的优化
这一点在第一次作业方法queryBlockSum
的实现中体现的最为明显。其本质是一个判断连通图个数的问题。如果使用深搜的算法,复杂度就会过高;最佳策略应该是使用并查集的算法。
bug修复
第一次作业
无,比较顺利。
第二次作业
BUG1:qgav的计算方式出错
JML格式是这样给出的
/*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length;
@ (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) /
@ people.length));
@*/
public /*@ pure @*/ int getAgeVar();
由于java中int
类型的整除问题,所以是先加还是先除会是一个影响结果的问题。写的时候看错了括号的范围而误写成了先加再除。
BUG2:delPerson未处理用于处理新增指令的HashMap
在MyGroup
中,为新增的指令建立了一个专用的HashMap<Integer, MyPerson>
,用来直接找到发红包的人。但是在为Group
删除人的时候忘记处理这个HashMap
导致出错。
第三次作业
BUG1:对Dijkstra的理解出错
不细说了,就搞错知识点了。
BUG2:遍历删除出错
老生常谈的bug,即以下的写法是错误的
for(Object o : list){
if(dosomething(o)){
list.remove(o);
}
}
严格使用ArrayList
的下标进行遍历,或使用removeif
语句可以避免这个问题。
Network拓展
要求分析
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
分析可知最主要的三个业务为:偏好设置、广告发送、商品购买。
实现代码
偏好设置
/*@ public normal_behavior
@ requires contains(personId);
@ requires getPerson(personId) instanceof Customer;
@ assignable getPerson(personId);
@ requires containsProduct(productId);
@ ensures getPerson(personId).isFavorable(productId) == true;
@ also
@ public normal_behavior
@ requires contains(personId);
@ requires !(getPerson(personId) instanceof Customer);
@ assignable \nothing;
*/
public /*@ pure @*/void setPreference(int personId, int productId);
考虑到可能重复设置偏好,应该也可以被包容为一个正确的操作,因此,前置条件不需要要求此前该Customer
对这一商品出于非偏好的状态。
广告发送
/*@ public normal_behavior
@ requires containsAdvertisement(id);
@ assignable advertisements;
@ ensures !containsAdvertisement(id) && advertisements.length == \old(advertisements.length) - 1;
@ ensures (\exists int i; 0 <= i && i < \old(advertisements.length); \old(advertisements[i].getId()) == id);
@ ensures (\forall int j; 0 <= j && j < advertisements.length;(\exists int i; 0 <= i && i < \old(advertisements.length); advertisements[j].equals(\old(advertisements[i]))));
@ ensures (\forall int i; 0 <= i && i < people.length; people[i].isFavorable(id) ==> (\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements.length == \old(people[i].advertisements.length) + 1) && people[i].advertisements[j] == id) );
@ ensures (\forall int i; 0 <= i && i < people.length; !people[i].isFavorable(id) ==> !(\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements[j] == id)) && people[i].advertisements.length == \old(people[i].advertisements.length));
@ ensures (\forall int i; 0 <= i && i < people.length; (\forall int j; 0 <= j < \old(people[i].advertisements.length)(\exists int k; 0 <= k < people[i].advertisements.length; \old(people[i].advertisements[j]) == people[i].advertisements[k])));
@*/
public void sendAdvertisement(int id);
考虑到Customer
只会消费自己收到广告,且此时对其有篇好的商品,故在Customer
中设置一个广告列表是有必要的,JML中也是按照这种思路进行描述的。
商品购买
/*@ public normal_behavior
@ requires contains(personId);
@ requires getPerson(personId) instanceof Customer;
@ requires containsProduct(productId);
@ ensures getPerson(personId).isFavorable(productId) == true;
@ ensures (\exists int cnt; cnt == (\sum int i;0<=i && i< \old(getPerson(personId).advertisements.length) && \old(getPerson(personId).advertisements[i].getId() == productId);1);getPerson(personId).advertisements.length == \old(getPerson(personId).advertisements.length) - cnt);
@ also
@ public normal_behavior
@ requires !(getPerson(personId) instanceof Customer);
@ assignable \nothing;
@*/
public /*@ pure @*/void buyProduct(int personId, int productId);
考虑到一个商品,Customer
也许会重复地收到它的广告,这里设定:用户买下产品后,他手中的关于该商品的广告全部失效。
个人小发现:有关测试中可能算是遗漏的重要情况
在第三次作业Network
类的addMessage
方法中,判断message相同的方法是这样的。
@ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
@ messages[i].equals(message));
其采用的是调用messages[i].equals(message)
的方法。
而观察Message.equals
方法
/*@ also
@ public normal_behavior
@ requires obj != null && obj instanceof Message;
@ assignable \nothing;
@ ensures \result == (((Message) obj).getId() == id);
@ also
@ public normal_behavior
@ requires obj == null || !(obj instanceof Message);
@ assignable \nothing;
@ ensures \result == false;
@*/
public /*@ pure @*/ boolean equals(Object obj);
会发现如果相比较的其中一个元素为null
,则返回false。
然而大部分人判断Message
相同的方法,是建立一个HashMap
或信息ID的HashSet
。实践发现,当HashMap
或HashSet
当中存在null
元素时,调用contains(null)
或containsKey(null)
将会返回true
,与JML中的行为不符。
不过由于本次作业保证了不存在为null
的Message
,故这个遗漏并不会影响成绩,但个人认为这作为一个考察点。
学习体会
这一单元如果仅仅为了通过测试的话,只需做一个无情的JML翻译机器就可以了。但是如果能够真正掌握JML的撰写方法和撰写规律,才能有更明显的收获。
本单元总体难度比之前低,但是要保证正确,思维的严谨性和视力的要求是很高的。到这一章的时候似乎大多数同学都已经用上了自己的评测机,我很惭愧到现在都还没有做出来,希望下一单元能够弥补这一缺憾。