OO第三单元作业总结
单元概述
本单元涉及到契约式编程,即供应方和用户在各自履行自己的义务,获得自己的利益的情况下,达成某种契约,用契约规定双方的权益和义务。契约式设计强调三个概念:前置条件,后置条件和不变式。前置条件发生在每个操作(方法,或者函数)的最开始,后置条件发生在每个操作的最后,不变式实际上是前置条件和后置条件的交集。违反这些操作会导致程序抛出异常。
本单元我们主要通过了解规格化描述语言JML的基本语法和语义,进行根据JML给出的规格编写Java代码,实现契约式编程。这就代表这要求我们具有两种能力,一种是对JML正确的理解和解读能力,另一种是根据JML给出的规格,编写Java代码的能力。
JML基础语法总结
以下为本单元主要涉及到的一些语法概述。
-
\result
:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。 -
\old(expr)
:表示一个表达式expr
在相应方法执行前的取值,该表达式涉及到评估expr
中的对象是否发生变化。 -
\forall
:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。 -
\exists
:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。 -
\sum
:返回给定范围内的表达式的和。 -
\max
:返回给定范围内的表达式的最大值。 -
\min
:返回给定范围内的表达式的最小值。
-
前置条件 (pre-condition) : 用
requires
子句表示,表明需要满足何种前置条件; -
后置条件 (post-condition) : 用
ensures
子句表示,表明方法结束后一定满足何种条件; -
副作用范围限定 (side-effects) : 副作用指方法在执行过程中会修改对象的属性数据或静态成员数据,可用
assignable
和modifiable
关键词表示。 -
(/*@ pure @ */)
指不会对对象的状态进行任何改变,也不需要提供输入参数,这样的方法无需描述前置条件,也不会有任何副作用,且执行一定会正常结束。有些前置条件可以引用pure
方法的返回结果 -
public normal_behavior
和public exception_behavior
分别针对正常功能行为和异常行为。 -
signals (***Exception e) b_expr
在b_expr条件下,抛出***Exception异常
第九次作业
本次作业,需要通过实现官方提供的接口 Person
、Network
和 Group
,来实现自己的 Person
、Network
和Group
类,也就是进行了本单元的总体架构,之后几次作业便是在次基础上添加相应的方法实现。
在本次实验中,类及其实现的接口的对应关系如下图。而我们要做的就是根据接口中JML描述的要求,编写相应的My...类,实现这些对应的接口。
UML类图
我们主要需要实现的几个类的UML图如下图所示。
实现
JML的阅读理解
下面以两个函数的JML描述为例,进行解读
/*@ 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;
这段即描述了isCircle这个方法的作用,即是判断是否存在一条从id1这个人到id2这个人的一条路径,如果存在返回true,不存在返回false(\result
后面内容说明返回值)。\exists
表示存在,\forall
表示对于所有,requires
声明前置条件,ensures
声明后置条件。当id1或id2不存在的时候,抛出PersonIdNotFoundException
的异常。我们可以考虑使用搜索图中两节点是否存在路径的算法,例如宽度优先搜索或是广度优先搜索的方法,但在实际测试中由于方法复杂度过高,我们考虑使用并查集进行优化,相应说明见bug修复部分。
/*@ 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();
初读理解本段说明,意思就是统计对于每个人i,如果所有的人j<i,都与i没有路径连通,那么统计总数sum+1,最后返回统计总数。我们当然可以直接按照JML的说明,通过遍历的方法来实现,但显然这种方法的复杂度为O(n^2),复杂度较高实现可能在成RUNTIME_ERROR。那么我们实际可以把这段说明结合实际理解一下,将所有相互之间有连接的节点组成一个Block,这个方法的目的就相当于是统计这个NetWork中一共有多少个Block。我们可以给MyNetWork设置一个属性用于记录Block的数量,看到有新人加入,分区数量加一,有不同父的关系加入分区数量减一,这样如果queryBlockSum的时候直接返回Block的数量这个属性的值就可以了,复杂度直接降为了O(1)。
异常处理的实现
-
异常处理函数的实现
本单元的异常处理函数,不光需要能够输出错误的类型,而且要能够输出该类型错误出现的总的次数,这就需要有一个对于该类型错误的不因创建新的该类型对象而改变的变量,我们可以通过为属性加上
static
这个关键字来实现。例如:public class MyEqualGroupIdException extends EqualGroupIdException { private static int cnt = 0; private static HashMap<Integer,Integer> idMap = new HashMap<>(); private final int id; public MyEqualGroupIdException(int id) { cnt++; this.id = id; if (idMap.containsKey(id)) { idMap.put(id,idMap.get(id) + 1); } else { idMap.put(id,1); } }
-
异常处理函数的调用
如果方法直接
throws
相应的异常,我们直接在需要抛出异常的地方,new一个异常对象即可,比如:throw new MyEqualPersonIdException(id1);
如果没有在方法外面没有
throws
相应的异常,但在方法的实际实现时可能产生相应的异常,我们可以使用如下语句try{ ... } catch(Exception e) { }
算法优化——容器
本次作业涉及到很多根据id查询对应的元素的操作,如果每次都要遍历会造成复杂度很高。我们可以使用HashMap这个容器,直接根据key找到对应的元素,将复杂度降为O(1)。
需要注意的是,在使用get方法时,一定要保证这个key已经存在于HashMap之中了,否则会产生错误。
bug修复
bug原因
本次作业的bug都是CPU_TIME_LIMIT_EXCEPTION,即程序运行时间超出规定时间限制。分析是由于在进行qbs和isCircle的时候使用的算法是BFS(广度优先搜索),算法复杂度为O(n^2),导致在规定时间内无法完成指定要求。
修改方案——并查集优化
使用并查集的方法,并结合相应的路径压缩,进行搜索判断,降低了时间复杂度。
代码修改说明
对MyNetwork进行修改。
加入fathers这个对象用于记录每个Person的父节点。要想判断两个节点之间是否有连通路径(isCircle),只需要判断两个节点的根父节点是否相同。
private HashMap<Integer,Integer> fathers;
find方法用于递归搜索
int find(int x) { if (fathers.get(x) == x) { return x; } fathers.put(x,find(fathers.get(x))); //路径压缩 rank.put(x,2); return fathers.get(x); }
同时,加入blockSize用于记录有多少个分区,将qbs的复杂度降至O(1)。
第十次作业
本次作业在上一次作业的基础上增加了Message这个类,以及对于Message的访问,并要求NetWork事项更多的功能。
UML类图
可以看到实际上我不光按题目要求补充了MyMessage这个类,而且补充了一个MyNetWork的关联类Edge,它主要是用于对记录人与人之间的关系,在最短路径查询的时候,便于根据权值进行排序,实现堆优化。
public class Edge implements Comparable<Edge> { private final int from; private final int to; private final int value;
算法分析——Prim
本次作业涉及到最小生成树查询的问题queryLeastConnection。相应JML的描述如下:
/*@ public normal_behavior @ requires contains(id); @ ensures \result == @ (\min Person[] subgroup; subgroup.length % 2 == 0 && @ (\forall int i; 0 <= i && i < subgroup.length / 2; subgroup[i * 2].isLinked(subgroup[i * 2 + 1])) && @ (\forall int i; 0 <= i && i < people.length; isCircle(id, people[i].getId()) <==> @ (\exists int j; 0 <= j && j < subgroup.length; subgroup[j].equals(people[i]))) && @ (\forall int i; 0 <= i && i < people.length; isCircle(id, people[i].getId()) <==> @ (\exists Person[] connection; @ (\forall int j; 0 <= j && j < connection.length - 1; @ (\exists int k; 0 <= k && k < subgroup.length / 2; subgroup[k * 2].equals(connection[j]) && @ subgroup[k * 2 + 1].equals(connection[j + 1]))); @ connection[0].equals(getPerson(id)) && connection[connection.length - 1].equals(people[i]))); @ (\sum int i; 0 <= i && i < subgroup.length / 2; subgroup[i * 2].queryValue(subgroup[i * 2 + 1]))); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id); @*/ public /*@ pure @*/ int queryLeastConnection(int id) throws PersonIdNotFoundException;
常见方法是使用Kruskal算法,或者是Prim算法。我在本次作业中使用的是Prim算法,并在找最短距离的时候借助Collections.sort()进行了堆优化,减少依次查找的复杂度。Prim算法即是查找从已加入待生成最小生成树集合Set的节点到该Block其它节点的最短距离边Edge,将边的另一个端点加入Set,再次查找,直到生成最小生成树。
一个可能的实现代码,如下所示:
private int prim(int id) { int sum = 0; HashSet<Integer> mark = new HashSet<>(); ArrayList<Edge> queue = new ArrayList<>(); //边的起点和终点 HashMap<Integer,Integer> dst = new HashMap<>(); //到指定人的距离 dst.put(id,0); Edge edge = new Edge(id,id,0); queue.add(edge); while (!queue.isEmpty()) { while (!queue.isEmpty() && mark.contains(queue.get(0).getTo())) { } if (queue.isEmpty()) { break; } int index = queue.get(0).getTo(); mark.add(index); sum += queue.get(0).getValue(); queue.remove(0); MyPerson myPerson = (MyPerson)(getPerson(index)); for (Person p : myPerson.getAcquaintanceMap().keySet()) { int cost = myPerson.getAcquaintanceMap().get(p); int thisDst = dst.getOrDefault(p.getId(), 1005); if (cost < thisDst && !mark.contains(p.getId())) { dst.put(p.getId(),cost); Edge edge1 = new Edge(myPerson.getId(),p.getId(),cost); queue.add(edge1); Collections.sort(queue); } } } return sum; }
要想实现Collections.sort(ArrayList<Edge>)
需要给Edge
增加对于Comparable<Edge>
接口的实现,并重写compareTo方法
@Override public int compareTo(Edge other) { if (this.getValue() > other.getValue()) { return 1; } else if (this.getValue() < other.getValue()) { return -1; } return 0; }
bug修复
bug原因分析
在对qgvs进行优化的时候忘记了要求i和j都必需是在group里的人,导致qgvs会出现WRONG_ANSWER
修改方案
加if条件判断
public int getValueSum() { int sum = 0; for (Person i : people) { MyPerson p = (MyPerson) i; for (Person j : p.getAcquaintanceMap().keySet()) { if (people.contains(j)) { sum += p.getAcquaintanceMap().get(j); } } } return sum; }
第十一次作业
本次作业在上一次作业的基础上为Message增加了EmojiMessage,NoticeMessage,RedEnvelopeMessage三个子接口,它们有一些不同的属性。
UML类图
架构实现分析
迭代器删除
我们知道,如果想要删除ArrayList中的一个元素,我们只需要使用remove方法即可,但是对于HashMap,我们想删除一个元素确不能这样操作。我们可以使用迭代器进行处理。例如下面deleteColdEmoji方法中的实现:
public int deleteColdEmoji(int limit) { Iterator<Map.Entry<Integer,Integer>> it = emojiList.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Integer,Integer> tmp = it.next(); if (tmp.getValue() < limit) { if (emojiId2msId.containsKey(tmp.getKey())) { for (int i : emojiId2msId.get(tmp.getKey())) { messages.remove(i); } } emojiId2msId.remove(tmp.getKey()); it.remove(); } } return emojiList.size(); }
最短路径查询
在方法sendIndirectMessage中需要进行最短路径查询,可以使用Dijstra方法,也就是一种松弛的方法
算法复杂度O(|E|)可以使用优先队列进行堆优化,做出一个只需要O(|V|)的算法。一个可能的实现如下。
private int dijkstra(int v0,int v1) { HashMap<Integer,Boolean> visit = new HashMap<Integer, Boolean>(); HashMap<Integer,Integer> dis = new HashMap<>(); for (Person p:people.values()) { visit.put(p.getId(),false); dis.put(p.getId(),Integer.MAX_VALUE); } PriorityQueue<Edge> queue = new PriorityQueue<>(); dis.put(v0,0); queue.add(new Edge(v0,v0,0)); while (!queue.isEmpty()) { Edge edge = queue.poll(); if (visit.get(edge.getTo())) { if (edge.getTo() == v1) { break; } } else { visit.put(edge.getTo(),true); for (int i:((MyPerson)people.get(edge.getTo())).getAcquaintanceMap().keySet()) { try { if (dis.get(i) > edge.getValue() + queryValue(i,edge.getTo())) { dis.put(i,edge.getValue() + queryValue(i,edge.getTo())); queue.add(new Edge(v0,i,dis.get(i))); } } catch (PersonIdNotFoundException | RelationNotFoundException e) { e.printStackTrace(); } } } } return dis.get(v1); }
bug修复
bug原因分析
sim方法返回最短路径,使用dijstra算法书写有误,循环结束条件有误导致出现RunTimeError的问题。修改方法:修改dijstra的写法。在对addRelation、sendMessage的时候忘记及时更新对应MyGroup中的Money和Value的值,导致qgvs会出现错误。Djistra算法写法有误,导致可能无法获得最短路径。
修改方案
为MyGroup补充addValue和addMoney的方法,及时更新其值。
修改Djistra算法的写法,见架构实现部分。
测试
借助JUnit准备测试数据
借助JUnit框架可以针对具体的方法进行根据JML规格的测试。
下面举addPerson这个方法的例子说明具体实现。
首先,我们先来看一下addPerson的JML说明:
/*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < people.length; people[i].equals(person)); @ assignable people; @ ensures people.length == \old(people.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(people.length); @ (\exists int j; 0 <= j && j < people.length; people[j] == (\old(people[i])))); @ ensures (\exists int i; 0 <= i && i < people.length; people[i] == person); @ also @ public exceptional_behavior @ signals (EqualPersonIdException e) (\exists int i; 0 <= i && i < people.length; @ people[i].equals(person)); @*/ public void addPerson(/*@ non_null @*/Person person) throws EqualPersonIdException;
使用JUnit生成测试框架,其中以下部分就是针对addPerson方法的
@Test public void addPerson() { }
这样看起来似乎对于JML语句没有较强的针对性,那么我们可以来逐条分析,补全代码
public class MyNetworkTest { private MyNetwork myNetwork; public MyNetworkTest() { myNetwork = new MyNetwork(); } @Test public void addPerson() { System.out.println("test addPerson"); for (int i = 0; i < 5; i++) { int id = (int) (Math.random() * 100); MyPerson p = new MyPerson(id, Integer.toString(id), id); /* @ requires !(\exists int i; 0 <= i && i < people.length; people[i].equals(person)); @ assignable people;*/ int oldLength = myNetwork.getPeople().size(); ArrayList<Person> oldPeople = myNetwork.getPeople(); try { myNetwork.addPerson(p); System.out.println("addPerson " + id); } catch (EqualPersonIdException e) { System.out.println(e); } /* @ ensures people.length == \old(people.length) + 1;*/ Assert.assertEquals(myNetwork.getPeople().size(), oldLength + 1); /* @ ensures (\forall int i; 0 <= i && i < \old(people.length); @ (\exists int j; 0 <= j && j < people.length; people[j] == (\old(people[i]))));*/ for (Person ps : oldPeople) { Assert.assertTrue(myNetwork.getPeople().contains(ps)); } /* @ ensures (\exists int i; 0 <= i && i < people.length; people[i] == person);*/ Assert.assertTrue(myNetwork.getPeople().contains(p)); /* @ signals (EqualPersonIdException e) (\exists int i; 0 <= i && i < people.length; @ people[i].equals(person));*/ Assert.assertThrows(EqualPersonIdException.class, () -> { myNetwork.addPerson(p); }); } }
如果方法正确,可以输出如下结果
test addPerson addPerson 47 addPerson 34 addPerson 53 addPerson 31 addPerson 58 Process finished with exit code 0
不正确,则会提示错误
java.lang.AssertionError: Expected :1 Actual :2
自行构造数据生成器
在进行互测的时候,我就是根据题目逻辑自行构造数据生成器,产生大量具有针对性的数据进行hack。主要语法是循环结构。例如以下这样。
for i in range(1,300): print("ap",end=" ") print(i,end=" ") print(i,end=" ") print(i) print("qci",end=" ") print(i,end=" ") print(i) print("qbs")
扩展NetWork
题目说明
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
JML编写
市场营销sendAdvertise
即使得Advertiser持续向外发送产品广告。sendAdvertise将Id为id的Advertiser的广告发送给所有的Customer。
/*@ public normal_behavior @ requires contains(id) && getPerson(id) instanceof Advertiser &&
@ (\forall int j; 0 <= j && j < people.length; people[j].getId() == id && people[j] instanceof Customer &&
@ (\forall int k; 0 <= k && k < people[j].advertises.length; people[j].advertises[k].getId() != getPerson(id).getAdvertiseId())); @ assignable people[*]; @ ensures (\forall int j; 0 <= j && j < people.length; people[j].getId() == id && people[j] instanceof Customer &&
@ (\exists int k; 0 <= k && k < people[j].advertises.length; people[j].advertises[k].getId() == getPerson(id).getAdvertiseId())); @ ensures (\forall int i; 0 <= i && i < people.length && people[i] instanceof Customer;
@ (\forall int j; 0 <= j && j < \old(people[i].getMessages()).length);
@ (\exists int k; 0 <= k && k < people.getMessages().length; people.getMessages().get(k) == \old(people[i].getMessages()).get(j))); @ also @ public exception_behavior @ signals (PersonIdNotFoundException e) !contains(id) || !(getPerson(id) instanceof Advertiser); @ signals (EqualAdvertiseIdException e) contains(id) && getPerson(id) instanceof Advertiser &&
@ (\forall int j; 0 <= j && j < people.length; people[j].getId() == id && people[j] instanceof Customer &&
@ (\exists int k; 0 <= k && k < people[j].advertises.length; people[j].advertises[k].getId() == getPerson(id).getAdvertiseId()))); */ public /*@ pure @*/ void sendAdvertise(int id) throws PersonIdNotFoundException,EqualAdvertiseIdException;
查询某种商品的销售额querySale
所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息。所以要想获得某种商品的销售额,只需要访问该商品的Producer,看他获得了多少销售额sale即可。
/*@ public normal_behavior @ requires contains(id) && getPerson(id) instanceof Producer; @ assignable \nothing @ ensures \result = getPerson(id).sale; @ also @ public exception_behavior @ signals (PersonIdNotFoundException e) !contains(id) || !(getPerson(id) instanceof Producer); */ public /*@ pure @*/ int querySale(int id) throws PersonIdNotFoundexception;
查询某种商品的销售路径querySaleRoute
查询某种商品的销售路径,也就是查看该商品从Producer到Customer经过了哪些人Person。
/*@ public normal_behavior @ requires contains(id1) && contains(id2) && getPerson(id1) instanceof Producer && getPerson(id2) instanceof Customer; @ ensures \result.length >= 3 && (\result.get(0).equals(getPerson(id1))) && (\result.get(\result.length - 1).equals(getPerson(id2))) &&
@ (\forall int i; 0 <= i && i < \result.length - 1; \result.get(i).isLinked(\result.get(i+1))); @ also @ public exception_behavior @ signals (PersonIdNotFoundexception e) !contains(id1) || !getPerson(id1) instanceof Producer; @ signals (PersonIdNotFoundexception e) contains(id1) && getPerson(id1) instanceof Producer && (!contains(id2) || !(getPerson(id2) instanceof Customer)); */ public /*@ pure @*/ List<Person> querySaleRoute(int id1,int id2) throws PersonIdNotFoundexception;
心得体会
本单元的作业应该可以说是被公认的很简单的一个单元,但我被hack产生了很多bug。分析原因主要有以下几个点:阅读JML不仔细,有些要求没有实现;对于算法的写法掌握不充分,导致算法写错;课下测试对拍测试量不够,本应当课下测出来的bug没能够即时更正再提交。
通过这一个单元的练习,我对契约式编程有了一定了解,了解了规格化描述语言JML,能够实现根据规格化描述语言JML,编写Java代码。同时吸取教训,以后编程要更加认真,熟练掌握基本数据结构的写法,尽量多的进行本地测试。