「BUAA OO」第三单元总结
「BUAA OO」第三单元总结
零、写在前面
1、任务简介
本单元以掌握 JML 规格 为目标,需要建立一个包含人、群组、网络、消息的信息网络。其中还需要运用少量的图论知识与各种方法来进行优化;人在网络中可视作一个结点,整张网络可以视为一个图,人与人之间的熟人关系可视作带权边,于是便有了各种联通问题、通路问题。
- 第一次作业:仅有人、群组、总网络。涉及到的操作如:添加熟人关系、将人加入群组、将人从群组中删除、查询两人间是否能通过若干个熟人联通、查询连通块数目......
- 第二次作业:新增了“消息”这一新元素,同时也有了添加消息、发送消息等操作。除此以外,还新增考察了查询群组内年龄的方差、查询群组内所有人的熟悉程度、查询网络中由一个人引出的最小生成树......
- 第三次作业:“消息”还细分出了表情、提醒、红包这三个种类,也由此有了添加表情包、清除低热度表情、清空提醒消息等操作;还可以在熟人间互相传递消息,并要求返回两人之间传递消息的最短路径
尽管每次作业都有各种各样的函数与花里胡哨的优化操作,但是这个单元的核心应该还是在掌握 JML 的阅读与书写方式
2、JML
然而尽管本单元的主题应该是 jml 的理解与书写,但也没有太多需要赘述,指导书里写得很详尽了;而个人心得则见文末总结
3、架构设计
毕竟这个单元只需实现各个接口,我觉得架构设计实在是不必过分梳理,整体架构见前述“任务简介”即可
各种策略则见后述“性能与优化”
一、性能与优化
不同于第二单元(第二单元由于实时交互所以并不存在绝对的最优调度,因而我没什么优化的想法与动力,搞了个模拟计算动态切换策略已经属于是我最大的挣扎了),这个单元跟第一单元一样极富优化空间,所以我好好研究了一下
通用优化
这个单元通用的一个提升性能的方式是:尽量将各种 List 改写为 HashMap,以尽量避免显式的直接遍历。如网络中的群组可以按如下方式存取(id \(-\) value式的访问方式):
点此查看JML
//@ public instance model non_null Group[] groups;
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < groups.length; groups[i].equals(group));
@ assignable groups;
@ ensures groups.length == \old(groups.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(groups.length);
@ (\exists int j; 0 <= j && j < groups.length; groups[j] == (\old(groups[i]))));
@ ensures (\exists int i; 0 <= i && i < groups.length; groups[i] == group);
@ also
@ public exceptional_behavior
@ signals (EqualGroupIdException e) (\exists int i; 0 <= i && i < groups.length;
@ groups[i].equals(group));
@*/
public void addGroup(/*@ non_null @*/Group group) throws EqualGroupIdException;
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id);
@ ensures (\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id &&
@ \result == groups[i]);
@ also
@ public normal_behavior
@ requires (\forall int i; 0 <= i && i < groups.length; groups[i].getId() != id);
@ ensures \result == null;
@*/
public /*@ pure @*/ Group getGroup(int id);
private final HashMap<Integer, Group> groups;
public void addGroup(Group group) throws EqualGroupIdException {
if (groups.containsKey(group.getId())) {
throw new MyEqualGroupIdException(group.getId());
}
groups.put(group.getId(), group);
}
public Group getGroup(int id) {
return groups.get(id);
}
观察上例可以注意到,在 HashMap 的实现中可以“自动地”解决一些细节问题:如在 getGroup() 中,get() 失败会直接返回 null 所以无需特判。
借助这个性质,我们可以优化更多的函数,以尽量避免重复的查询/取值:
点此查看JML
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) &&
@ (\exists int i; 0 <= i && i < people.length; people[i].getId() == id1) &&
@ getGroup(id2).hasPerson(getPerson(id1)) == false &&
@ getGroup(id2).people.length < 1111;
@ assignable getGroup(id2).people;
@ ensures (\forall Person i; \old(getGroup(id2).hasPerson(i));
@ getGroup(id2).hasPerson(i));
@ ensures \old(getGroup(id2).people.length) == getGroup(id2).people.length - 1;
@ ensures getGroup(id2).hasPerson(getPerson(id1));
@ also
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) &&
@ (\exists int i; 0 <= i && i < people.length; people[i].getId() == id1) &&
@ getGroup(id2).hasPerson(getPerson(id1)) == false &&
@ getGroup(id2).people.length >= 1111;
@ assignable \nothing;
@ also
@ public exceptional_behavior
@ signals (GroupIdNotFoundException e) !(\exists int i; 0 <= i && i < groups.length;
@ groups[i].getId() == id2);
@ signals (PersonIdNotFoundException e) (\exists int i; 0 <= i && i < groups.length;
@ groups[i].getId() == id2) && !(\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id1);
@ signals (EqualPersonIdException e) (\exists int i; 0 <= i && i < groups.length;
@ groups[i].getId() == id2) && (\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id1) && getGroup(id2).hasPerson(getPerson(id1));
@*/
public void addToGroup(int id1, int id2) throws GroupIdNotFoundException, PersonIdNotFoundException, EqualPersonIdException;
public void addToGroup(int id1, int id2) throws GroupIdNotFoundException, PersonIdNotFoundException, EqualPersonIdException {
Group group = getGroup(id2);
if (group == null) {
throw new MyGroupIdNotFoundException(id2);
}
Person person = getPerson(id1);
if (person == null) {
throw new MyPersonIdNotFoundException(id1);
}
if (group.hasPerson(person)) {
throw new MyEqualPersonIdException(id1);
}
if (group.getSize() < 1111) {
group.addPerson(person);
}
}
上面有两个优化细节:1、注意判断条件之间的联系,实际代码实现中的 throw 其实是保证了后面的代码在执行前已经满足某个条件了,所以实现时不必像 JML 里一样对每个情况都进行逐条件的完全判断;2、利用 HashMap 获取失败返回 null 的性质,可以先直接 get() 然后再判断是否为 null,以减少一次 contains() 时的查询操作,后续取用时直接拿来用即可
第一次作业
第一次作业的性能核心在于查询是否联通以及统计连通块数目;而采用并查集是很高效的做法,再加上路径压缩可以进一步优化性能。并查集的具体介绍与实现可以参考一篇文章,这篇文章里写得很直观清晰(不过这篇文章里的路径压缩做得不是很完全,可以做得更优)
另外,在 MyPerson 类中存取 acquaintance 与 value 时,可采用如下形式以高效地存取熟人间的联系。
/*@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;
@*/
private final HashMap<Person, Integer> acquaintanceToValue;
(其实有个细节:由于 Person 的 id 在网络中是唯一的,所以其实 new 出来后 ap() 成功的 Person 的“指针值”与其 id 是一一对应的,因此可以直接比较 Person 的“指针值”来比较 id,实现 equals 的判断。然而其实一想就能想到,这只是缘于本次作业中的约束比较严而已,实际上这应该是个很值得注意的易错点——这最终将在第三次作业里暴露出来)
第二次作业
1、表层性能
第二次作业的表层性能核心是在于最小生成树的算法。我采用了 Kruskal
算法,并采用了并查集进行优化。
为了更方便地获取边集,我维护了 LinkedList<Relation> relations 来统计边集,其中 Relation 这个实现了Comparable接口的自定义类包含了 权值、起点、终点三个信息。每调用一次 addRelation() 指令就向该边集中加一条边。在实现 queryLeastConnection() 指令时,先运用 Collections.sort(relations) 对边集进行排序,然后遍历所有节点调用 isCircle() 来判断是否相连,最后根据总节点数连出若干条边即可
值得一提的是:一开始我以为排序算法比较费时,所以在维护插入边集时便采用二分插入,保证边集有序;结果实测这样挺慢......经过查阅资料后发现 java 自带的 sort() 方法是很优的,所以最终还是采取了每来一次 qlc 就调用一次 sort()
2、暗藏杀机
事情开始于一个周三的晚上,当我搞了一通优化+跟 jbt 对了拍以后,我发现我在稀疏群组下的效率较高,而他在稠密群组下的效率较高。起初我没太在意,不过到了大概周四晚上以后,我开始研究其中的差异,结果发现了 qgvs 的磅礴杀机:若直接按照 JML 的规格来书写,当群组里很单纯地有 1111 个人时,即使他们互不为熟人,我的 qgvs 耗时也是惊人的;而 jbt 则由人出发遍历其熟人然后查询其熟人是否在群里,在上述情况下耗时就很短。在意识到这个问题后,我立即做了优化:若某人熟人多,则遍历群组;若熟人少,则遍历熟人。优化后我就起飞了,并且以此构造出了一个蛮有效的 hack 思路,这思路/数据不知道经过各种途径传给了多少人:)
另外,群组内维护一个 ageSum 是必要的,否则 qgav 会超时(至于是否维护各项平方之和 age²Sum,实际测了一下,起码对于我的架构来说应该是不用,维护它反而会更慢;类似地,对于 qgvs,如果直接在 atg、dfg 时维护 value总和,实测在 10w+ 条加人/删人指令下会变得极慢,且在低指令数下也并未比直接遍历求和快——由此可见“优化”也不能想当然地优化,必须要结合实践测试才行)
第三次作业
1、最短路径
我采用的是堆优化的 Dijkstra
算法。不得不说,这次百度百科真是超神了,以文末的模板为参考可以光速写完本次作业中的 sim()。不过其中还有2个优化的空间:
- 同 qgvs,不要直接遍历网络内所有未求得最小距离的人,而是从遍历某人的熟人出发再判断其是否已求出最小距离(这个优化方法算是本单元的特性,若储存方式换为邻接矩阵则不能直接按此法实现)
- 可以在方法内维护一个存有所有距离的记录表;对于 poll() 出的新结点,只有当新的距离值小于记录值时才加入优先队列并更新记录,以减少不必要的开销(这是上述百度百科中的通用代码所欠缺的)
2、移除技巧
众所周知,直接用 for each 边遍历边删除是不行的,一般应该用 Iterator 来一边遍历一边删除。很有意思的是,在我用 Iterator 进行书写后,IDEA 立即标黄提示我优化,于是我便参悟了这样的写法:
public int deleteColdEmoji(int limit) {
emojiIdToHeat.values().removeIf(heat -> heat < limit);
messages.values().removeIf(message -> (message instanceof EmojiMessage &&
!containsEmojiId(((EmojiMessage) message).getEmojiId())));
return emojiIdToHeat.size();
}
我查看源码后,发现其底层倒也就是用 Iterator 来实现的;但是这样用 removeIf 与 lambda 表达式的写法实在是很优雅
二、测试
这个单元我总算是自己搭了个评测机了!(前两个单元我真就随手搓了点数据然后随便测测就完事)
数据构造
随机数据
这个单元的数据构造不能太随意,需要控制指令比例、指令先后顺序、异常产生等要素。所以随机数据的构造分为两大环节:1、构筑单条指令;2、控制各随机指令的顺序与比例
第一次作业的数据生成器是由 jbt 提供给我的,他完成了各单条指令的构建与简单的全指令生成;然后我写了稍微更强一点的指令顺序、比例控制的内容。其中,对已添加的 person、group、acquaintance 等都进行了存取与维护,并依概率来决定是否产生异常(与 jml 的各个分支条件相对应),如下例:
class Data:
def __init__(self):
self.people = []
self.people_acquaintances = {}
self.groups = []
self.group_people = {}
self.messages = [] #这是第二次作业引入的
self.emoji_library = [] #这是第三次作业引入的
def ap(self):
if random.random() < 0.90 or len(self.people) == 0:
id_ = random.randint(1, 2500)
if id_ not in self.people:
self.people.append(id_)
self.people_acquaintances[id_] = []
else:
id_ = random.choice(self.people)
name = [random.choice(alphabet) for _ in range(random.randint(4, 10))]
name = ''.join(name)
age = random.randint(0, 200)
return 'ap ' + str(id_) + ' ' + name + ' ' + str(age) + '\n'
值得一提的是,为了充分验证 qci() 与 qbs(),我适量控制了 ap() 与 ar() 的比例,使得图的稠密度较为适中
第二次作业的数据生成器由我自己从头写了一个(因为 jbt 上次作业里单条指令的构建写得有点儿烂,还是自己来写比较安心,上面的python代码就是我自己写的版本)。为了充分检验 qgvs、qlc,我设置指令数量如下:ar() > ap() >> ag(),即图偏稠密且群组内人数较多
第三次作业的数据生成是最讲究的:
点此查看整体控制
def create_hw11():
data = Data()
s = ''
# 初始化、完备性
for i in range(20):
for _ in range(100):
s += data.ap()
for _ in range(3):
s += data.ag()
for _ in range(80):
s += data.atg()
for _ in range(150):
s += data.ar()
for _ in range(20):
s += data.am()
for _ in range(50):
s += data.arem()
for _ in range(50):
s += data.anm()
for _ in range(10):
s += data.sei()
for _ in range(150):
s += data.aem()
for _ in range(180):
s += data.sim()
if random.random() < 0.3:
s += data.qm()
s += data.cn()
s += data.qrm()
s += data.qp()
if random.random() < 0.03:
s += data.dce(_ / 30)
s += data.sei()
s += data.sei()
for _ in range(80):
s += data.sm()
if random.random() < 0.3:
s += data.qm()
s += data.cn()
s += data.qrm()
s += data.qp()
if random.random() < 0.03:
s += data.dce(_ / 30)
s += data.sei()
s += data.sei()
for _ in range(100):
s += data.qsv()
return s
- 基础指令的编写直接继承了第二次作业并进行了扩充。其中最让我感觉头疼的就是 aem() 的 emojiId 的生成与 dce() 的 limit 值的划定。其根本原因是数据构造器无法模拟出 dce() 对于表情包的删除(要想模拟也可以,用python再把本次作业的部分内容写一遍~),所以难以控制相关参数。因此对于 emojiId,我采用的是在保证有少量 sei() 指令的情况下令 emojiId = random.choice(self.emoji_library[-6:]);对于 dce(),我根据总的 aem() 数量进行了动态控制,整体使 limit 较小
- 对于整体数据集的构建,我完全没有沿用第二次作业的顺序与比例,而只挑了与第三次作业新增的相关的指令来重新构建数据集(当然,最后测试时还是带着上次的数据集测了一下,测试时 hw10 与 hw11 的数据比例为1:4)
- 各指令的先后顺序很重要。比如尽量先 am() 再 sm(),先 sei() 再 aem(),先 sim() 再 qm()
- 还是要注意图的疏密程度。这次我将 ap() 与 ar() 的比例控制在了1:1.5,整体相对稀疏,便于验证 sim()
手搓数据
随机数据主要是整体检验,要论“定点爆破”还是得自己搓:
第一、二次作业手搓数据时我还是很认真的,特化性强,每一个数据基本只含几个针对性指令,对易错点/易超时的地方进行了精心设计,实测比随机生成的数据更靠谱
然而第三次作业嘛......看文件名也看得出来,我没咋手搓,基本是仅将自动评测时使周边同学们出错的数据部分记录下来了而已(名字已全部匿名替换为xxx)——这是因为第三次作业针对性手搓数据的难度在我看来实在较大,并且感觉没啥可专门构造超时的爆破点(不像 qgvs);同时自动评测机的效率也已经比较高了,基本不超过 5 组数据就能测出大家的 bug
正确性检验
这个单元采用对拍检验,简单写个对拍器即可(别一帮人全寄就行)
时间测试
评测机除了正确性检验以外,还给出了运行的总时长。出乎我事先预料的是,时间反馈信息在本单元里为我提供了最大的帮助——当我发现我在某些数据下明显偏慢时,我就能立马知道我有的地方没有优化到位,然后要做的就是进一步的优化了
三、bug分析
自身bug
强测、互测都全部通过了,自测也没有出过bug(但在优化上做了很多很多个测试/改进版本)
他人bug(互测前)
帮小伙伴们评测时测出了挺多bug,凭印象记录一下(这里的bug专指正确性问题,至于优化不到位在此就不提了):
第一次作业:几乎没人有错(然而由于自动数据构造上出了瑕疵,直到最后10min我临时手搓的数据才测出了我室友的一个bug(┯_┯))
第二次作业:周边约半数的同学在第一次写 qlc(最小生成树) 的时候都有些奇奇怪怪的错误,不过问题不大()
第三次作业:周边朋友几乎都被我hack中了起码一次,bug主要还是集中在以下2种:
-
sim(最短路径)的各种bug
-
clean_notices() 出错。周围大部分人都是在这里出了bug。他们几乎都是没有调用 iterator.remove 而是直接对实例对象进行remove(),那么据说就会在类似这种情况下出错:
若某个 person 存有 id 相同的 message1 与 message2,其中1是普通消息,2是notice消息。由于重写后的 equals 的判断条件仅仅是 id 相等,所以在直接对实例对象进行 remove 时,可能明明想移除 message2 却误移除了 message1,酿成悲剧
互测bug
第一次作业:全房没有一个人有bug
第二次作业:早上9点我抛出 qgvs 以 ctle 炸了2个人(也不知道我在群里冒了个泡以后,qgvs() 到底 hack 了多少人),然后就没有测出其他 bug 了(但是互测结束后发现有一个同学被另一个人以 RUNTIME_ERROR 给hack中了一次,这其实说明了我的数据构造并不够全面)
第三次作业:唯一的bug是有一个同学 qgvs() 优化了但优化得不够彻底而依然 ctle,我怀疑上次作业他们房里 qgvs 的构造可能不够强
总的说来,这个单元大家 bug 都蛮少的,毕竟 jml 很明确,每个函数都分别写好的话就是不应该有bug才对,正确性bug基本都集中在第三次作业的 cn() 里了;然而在性能上必须得好好优化一番才能避免 ctle
四、功能拓展
任务需求
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过 Advertiser 来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
如此 Network 可以支持市场营销,并能查询某种商品的销售额和销售路径等
分析解读
Person 就站一边吃瓜去:)
三者的逻辑
- 对象产生的先后顺序。任务需求明显是一个供给侧的市场,所以应该先有产品,再有广告,进而再有消费,因此诞生的顺序应为:Producer -> Advertiser -> Customer
- 发送请求的传递顺序:Customer -> Advertiser -> Producer
- 从属关系:根据题意,Advertiser应当从属于Producer。若有不同的 Producer 生产同种商品,Advertiser 只能为其对应的一个或多个 Producer 进行宣传
Network新增操作
加人:加入Advertiser/Producer/Customer/Person
加广告:加入Advertiser不指定其广告内容,当进行该操作时再建立其与Producer及产品的联系
购买产品:Customer花钱买商品,Producer、Advertiser获利
查询某种商品的销售额:查询该种商品在网络中的销售总额(注意:即使是同种商品,经由不同 Advertiser 售出的话,其售价亦可能不同)
查询某种商品的销售路径:应当对所有包含该商品的 Advertiser 进行查询并反馈,最后应当得到一个集合
简化设计
为了简化操作,我们不妨进行这样的规定(同时后续可进行再扩展):
-
题目明确了只要求假设出现了几种不同的Person,故暂时不考虑增加新的 message 类,而直接在 Person 间进行沟通
-
一个Producer仅生产一种产品,产品的name与Producer保持一致(允许Prodecer重名), 产品成本 与Producer的age 保持一致
-
一个Advertiser仅对一个Producer负责(但是相同name的产品可能会由多个Advertiser销售),产品的售价为 产品成本 与 Advertiser的age(产品利润) 之和
-
Customer 在购买时指定的是某个Advertiser(而非具体产品——否则难以指定其究竟是买的哪位 Advertiser 的产品),且仅购买一件产品
-
购买产品时,Customer的money减少产品对应的售价,Producer的money增加产品对应的成本,Advertiser的money增加产品对应的利润(这里的“成本”、“利润”与其字面义有很大的出入)
-
题目中的“持续向外发送产品广告“与”选择和自己偏好匹配的产品来购买“暂不予以实现:1、所谓”持续向外“是不必要的,因为Network内是透明的,所有Customer均能看到所有的Advertiser;2、仅凭”偏好匹配“是无法买到商品的,偏好≠一定要买、想买某种商品≠会从某个Advertiser那里买商品,如果仅凭偏好来匹配可能会匹配到较多结果,难以对结果的唯一性进行约束
接口方法
(仅展示Network内新增的属性、方法)
/*@ public instance model non_null String[] procuctList;
@ public instance model non_null int[] procuctSaleVolumeList;
@*/
/*@ public normal_behavior
@ requires contains(id1) && contains(id2) && (getPerson(id1) instanceof Advertiser) &&
@ (getPerson(id2) instanceof Producer) && !((Advertiser)getPerson(id1)).onSale &&
@ !(\exists int i; 0 <= i && i < procuctList.length; procuctList[i].equals(getPerson(id2).name));
@ assignable getPerson(id1).producer, getPerson(id1).sale, procuctList, procuctSaleVolumeList;
@ ensures ((Advertiser)getPerson(id1)).producer == getPerson(id2);
@ ensures ((Advertiser)getPerson(id1)).sale == true;
@ ensures procuctList.length == \old(procuctList.length) + 1;
@ ensures procuctSaleVolumeList.length == \old(procuctSaleVolumeList.length) + 1;
@ ensures (\exists int i; 0 <= i && i < procuctList.length;
@ procuctList[i] == getPerson(id2).name && procuctList[i] == 0);
@ ensures (\forall int i; 0 <= i && i < \old(procuctList.length);
@ (\exists int j; 0 <= j && j < procuctList.length; procuctList[j] == (\old(procuctList[i])))) &&
@ procuctSaleVolumeList[j] == \old(procuctSaleVolumeList[i])));
@ also
@ public normal_behavior
@ requires contains(id1) && contains(id2) && (getPerson(id1) instanceof Advertiser) &&
@ (getPerson(id2) instanceof Producer) && !((Advertiser)getPerson(id1)).onSale &&
@ (\exists int i; 0 <= i && i < procuctList.length; procuctList[i].equals(getPerson(id2).name));
@ assignable ((Advertiser)getPerson(id1)).producer, ((Advertiser)getPerson(id1)).sale
@ ensures ((Advertiser)getPerson(id1)).producer == getPerson(id2);
@ ensures ((Advertiser)getPerson(id1)).sale == true;
@ also
@ public exceptional_behavior
@ assignable \nothing;
@ requires !contains(id1) || !contains(id2) || getPerson(id1).onSale;
@ signals (PersonIdNotFoundException e) !contains(id1);
@ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2);
@ signals (WrongIdentityException e) contains(id1) && contains(id2) &&
@ !(getPerson(id1) instanceof Advertiser);
@ signals (WrongIdentityException e) contains(id1) && contains(id2) &&
@ (getPerson(id1) instanceof Advertiser) && !(getPerson(id2) instanceof Producer);
@ signals (ProductDuplicatedException e) contains(id1) && contains(id2) &&
@ (getPerson(id1) instanceof Advertiser) && (getPerson(id2) instanceof Producer) &&
@ ((Advertiser)getPerson(id1)).onSale;
@*/
public void addAdvertisement(int id1, int id2) throws
PersonIdNotFoundException, WrongIdentityException, ProductDuplicatedException;
// id1表示Advertiser的id
// id2表示Producer的id
// 方法所表达的含义即为:让Advertiser为Producer进行销售
/*@ public normal_behavior
@ requires contains(id1) && contains(id2) && (getPerson(id1) instanceof Customer) &&
@ (getPerson(id2) instanceof Advertiser) && ((Advertiser)getPerson(id2)).onSale;
@ assignable getPerson(id1).money, getPerson(id2).money, ((Advertiser)getPerson(id2)).producer.money,
@ procuctSaleVolumeList;
@ ensures getPerson(id1).money == \old(getPerson(id1).money) - getPerson(id2).age -
@ ((Advertiser)getPerson(id2)).producer.age;
@ ensures getPerson(id2).money == \old(getPerson(id2).money) + getPerson(id2).age;
@ ensures ((Advertiser)getPerson(id2)).producer.money ==
@ \old(((Advertiser)getPerson(id2)).producer.money) +
@ ((Advertiser)getPerson(id2)).producer.age;
@ ensures (\exists int i; 0 <= i && i < procuctList.length &&
@ procuctList[i] == ((Advertiser)getPerson(id2)).producer.name;
@ procuctSaleVolumeList[i] == \old(procuctSaleVolumeList[i]) + getPerson(id2).age +
@ ((Advertiser)getPerson(id2)).producer.age);
@ also
@ public exceptional_behavior
@ assignable \nothing;
@ requires !contains(id1) || !contains(id2) || !getPerson(id1).onSale;
@ signals (PersonIdNotFoundException e) !contains(id1);
@ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2);
@ signals (WrongIdentityException e) contains(id1) && contains(id2) &&
@ !(getPerson(id1) instanceof Customer);
@ signals (WrongIdentityException e) contains(id1) && contains(id2) &&
@ (getPerson(id1) instanceof Customer) && !(getPerson(id2) instanceof Advertiser);
@ signals (NotOnSaleException e) contains(id1) && contains(id2) &&
@ (getPerson(id1) instanceof Customer) && (getPerson(id2) instanceof Advertiser) &&
@ !((Advertiser)getPerson(id2)).onSale;
@*/
public void purchase(int id1, int id2) throws
PersonIdNotFoundException, WrongIdentityException, NotOnSaleException;
// id1表示Customer的id
// id2表示Advertiser的id
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < procuctList.length; procuctList[i] == name);
@ ensures (\exists int i; 0 <= i && i < procuctList.length; procuctList[i] == name &&
@ \result == procuctSaleVolumeList[i]);
@ also
@ public exceptional_behavior
@ requires !(\exists int i; 0 <= i && i < procuctList.length; procuctList[i] == id);
@ signals_only ProductNotFoundException;
@*/
public /*@ pure @*/ int queryProductSaleVolume(String name) throws ProductNotFoundException;
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < procuctList.length; procuctList[i] == name);
@ ensures \forall int i; 0 <= i && i < people.length && (people[i] instanceof Advertiser) &&
@ ((Advertiser)people[i]).producer.name == name; \result.contains(people[i].name);
@ also
@ public exceptional_behavior
@ requires !(\exists int i; 0 <= i && i < procuctList.length; procuctList[i] == name);
@ signals_only ProductNotFoundException;
@*/
public /*@ pure @*/ Set<String> queryProductSaleWays(String name) throws ProductNotFoundException;
// Set的每个元素为:Advertiser.name
五、心得体会
JML
解读&书写
在这里简述一下我解读 jml 的方法:
- 先关注有哪些情况,即前置条件,梳理出各种行为
- 知道了有哪些情况以后,结合方法名,可以尝试着直接对方法所实现的内容进行概括性的揣测(比如前文中的 addToGroup,说白了就是向群组中加人)
- 在有大体的了解的基础上再细读 jml 的具体内容就会很轻松了
- 也有些难以一次读懂的描述,例如最小生成树的那个描述——我觉得自己在纸上造一个简单的例子,然后依照着 jml 走一遍会是比较生动的理解方式
书写的话,顺序略作调动即可:明确要干什么 -> 理清各种情况 -> 对各个情况分别进行书写
感受
解读 jml 感觉这种语言描述确实无比清晰明确;但一想到如果要自己来书写,就立马头疼不已——课程组的老师、助教们真的辛苦了,最小生成树与最短路径的写法着实令人大开眼界
其实我感觉,与其花大力气写一个大篇幅又间接的 jml,不如用日常语言尽可能描述清楚并给出一些样例,我认为这样明显更直观且更省力——我也由此很好奇在现实里到底有哪些地方在使用 jml,也很好奇 Unit1、2、4 应该怎样用 jml 进行书写(当然,这都是建立在阅读对象是人的前提下;若是机器,自然另当别论——但我还是感觉书写 jml 实在是很费力气)
特别鸣谢
首先是 jbt 同学,感谢他在凌晨一直跟我唠嗑,让我在大半夜凹优化时一点儿也不困
这个单元进行本地测评时对拍几乎是必要的,所以我收到了很多同学的jar包,也在帮大家 debug/测时间 时进行了交流——在此感谢与我进行交流的所有同学,如此交流互鉴式的学习使彼此都收获颇丰(前两个单元的作业完全是单打独斗,这个单元的交流收获了更多的乐趣)