OO第三单元总结
OO第三单元总结
一、作业分析
1、第一次作业
(1)作业要求简述
第一次作业要求通过实现官方接口 Person、Network、Group,来实现自己的 MyPerson、MyNetwork、MyGroup类,并最后能实现以下指令:
指令 | 简写 |
---|---|
add_person | ap |
add_relation | ar |
query_value | qv |
query_people_sum | qps |
query_circle | qci |
query_block_sum | qbs |
add_group | ag |
add_to_group | atg |
del_from_group | dfg |
(2)个人实现过程
第一次作业的难点在于query_circle、query_block_sum这两个指令的实现,根本上在于一个问题:判断无向图中两个节点之间是否存在路径。
由于我是在这一次作业刚布置就开始写了,所以没有注意到后来的讨论区里各位同学的建议(也没有考虑到强测数据的暴力),直接就采用了大一下学期数据结构中学到的方法:DFS遍历。具体实现在此不赘述,DFS递归实现大家都懂。但是,架构设计的偷懒带来的就是面对大量数据测试时的不堪一击,性能直接炸裂……
之后,我便开始思考怎么进行性能上的优化:DFS肯定不能用。我也想到了,对people新开一个属性动态储存其关联节点,这样只需要在加人、加关系的时候维护一下,而在执行query_circle、query_block_sum这两个指令时直接通过该属性即可迅速完成。
但是,当时的我是开了一个列表,储存了该people的所有直接相关的结点,这就导致在维护时操作极其繁琐,最终性能与DFS相差无几,甚至更加恶劣。
最后,我在了解了并查集后意识到,我仅仅需要储存该people的最高父节点即可完成本次作业的指令的要求:毕竟我只需要判断某两个节点是否有路径即可,至于路径是怎么样的,我不需要管。
2、第二次作业
(1)作业要求简述
第二次作业要求通过实现官方接口 Person、Network、Group、Message,来实现自己的 MyPerson、MyNetwork、MyGroup、MyMessage类,并最后能实现以下指令:
指令 | 简写 |
---|---|
add_person | ap |
add_relation | ar |
query_value | qv |
query_people_sum | qps |
query_circle | qci |
query_block_sum | qbs |
add_group | ag |
add_to_group | atg |
del_from_group | dfg |
query_group_people_sum | qgps |
query_group_value_sum | qgvs |
query_group_age_var | qgav |
add_message | am |
send_message | sm |
query_social_value | qsv |
query_received_messages | qrm |
query_least_connection | qlc |
(2)个人实现过程
第二次作业的难点在于2个问题:**最小生成树问题 **以及 如何保证面对大量qgvs、qgav指令时不超时。
对于最小生成树问题,由于我第一次作业最终采用了并查集,这对于Prim算法的实现提供了非常良好的基础,因此这一难点不足为惧。
真正恶心的还是性能问题:如果节点、关系足够多,qgvs、qgav指令也足够多,就非常容易超时。
最终,我选择再开两个新属性valueSum、ageSum来储存qgvs、qgav指令应该返回的值,而这两个属性在加人、加关系等操作时动态维护。虽然增加了维护这一成本,但是在执行qgvs、qgav指令时复杂度为O(1)。
3、第三次作业
(1)作业要求简述
第三次作业要求通过实现官方接口 Person、Network、Group、Message、EmojiMessage、NoticeMessage、RedEnvelopeMessage,来实现自己的 MyPerson、MyNetwork、MyGroup、MyMessage、MyEmojiMessage、MyNoticeMessage、MyRedEnvelopeMessage类,并最后能实现以下指令:
指令 | 简写 |
---|---|
add_person | ap |
add_relation | ar |
query_value | qv |
query_people_sum | qps |
query_circle | qci |
query_block_sum | qbs |
add_group | ag |
add_to_group | atg |
del_from_group | dfg |
query_group_people_sum | qgps |
query_group_value_sum | qgvs |
query_group_age_var | qgav |
add_message | am |
send_message | sm |
query_social_value | qsv |
query_received_messages | qrm |
query_least_connection | qlc |
add_red_envelope_message | arem |
add_notice_message | anm |
clean_notices | cn |
add_emoji_message | aem |
store_emoji_id | sei |
query_popularity | qp |
delete_cold_emoji | dce |
query_money | qm |
send_indirect_message | sim |
(2)个人实现过程
第三次作业的难点在于一个问题:无向图的最短路径问题。
在算法方面,我没有选择迪杰斯特拉算法,而是选择了弗洛伊德算法。
我知道从单次复杂度来看,弗洛伊德比迪杰斯特拉复杂很多,但是,考虑到上次作业的测试点中,很多测试点的结构是“构造图+查询”,对于这种结构,弗洛伊德算法维护的图在面对大量查询时,其优势就体现出来了:虽然从单次求最短路径上来看成本更高,但是一旦操作变多,其边际成本是O(1)的。
4、异常实现
在这里,我对三次作业的异常实现进行统一阐述。
以MyEqualPersonIdException为例:
package com.oocourse.spec1.exceptions;
import java.util.HashMap;
public class MyEqualPersonIdException extends EqualPersonIdException {
private static int sum = 0;
private int id;
private static HashMap<Integer, Integer> map = new HashMap<>();
public MyEqualPersonIdException(int id) {
this.id = id;
if (map.containsKey(id)) {
map.put(id, map.get(id) + 1);
}
else {
map.put(id, 1);
}
}
@Override
public void print() {
sum++;
System.out.println("epi-" + sum + ", " + id + "-" + map.get(id));
}
}
我增加了两个静态属性:
静态属性sum存储的值为本类异常出现的总次数;
静态属性map存储的是一个hashmap,键为发生本类异常的id,值为该id发生异常的次数;
每发生一次异常,sum就会自增一次,而MyEqualPersonIdException(int id)这一方法则会查询map中是否包含该id,有则值加一,没有则map增加一个键为id,值为1的元素。
通过以上机制,就实现了计数功能。
二、测试数据准备
由于本单元是根据JML规格完成作业,大多数指令都较为简单,每次作业都只有少数几条指令涉及到算法、可能出错,因此我选择偷点懒:直接到洛谷上找考察相关图论算法的题目,将其测试点构造的图通过我自己写的C语言程序改造成本单元的指令的格式,再手动测试即可。至于改造的C语言程序无非就是做一些字符串变换。
但是这难以对性能进行测试,这就导致了我在性能方面的不足。
三、Network扩展
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
Person:作为吃瓜群众,可用原先就已实现了的MyPerson来代替;
Advertiser:实现Person接口,新增1个属性 ArrayList<AdvertiseMessage> advertiseMessages存储产品广告;新增2个方法,分别为向消费者发送广告、向生产商发送购买消息PurchaseMessage2;
Producer:实现Person接口,新增1个方法接收PurchaseMessage并生产和发送产品
Customer:实现Person接口,新增2个属性ArrayList<Interger> hobby存储自己的爱好产品的id,ArrayList<AdvertiseMessage> advertiseMessages存储自己收到的广告;新增3个方法,向广告商发送购买消息PurchaseMessage1,清除不喜欢的广告,购买产品;
AdvertiseMessage:实现Message接口,新增1个属性存储产品id;
PurchaseMessage1:实现Message接口,新增1个属性存储产品id;
PurchaseMessage2:实现Message接口,新增1个属性存储产品id;
NetWork:新增属性存储当前的产品及销售额(可用hashmap)、广告消息、购买消息;新增Producer生产产品的方法,Customer选择产品的方法,查询产品销售额的方法,查询产品销售路径的方法。
Exception:新增ProduceIdNotFoundException,没有相应id的产品时触发;AdvertiseMessageIdNotFoundException,没有相应id的广告时触发;PurchaseMessage1IdNotFoundException、PurchaseMessage2IdNotFoundException,没有相应id的购买消息时触发。
Customer清除不喜欢的产品的广告:
/*@ public normal_behavior
@ requires contains(personId);
@ assignable getPerson(personId).advertiseMessages
@ ensures (\forall int i; 0 <= i && i < \old(getPerson(personId).advertiseMessages.length);
@ (\old(getPerson(personId).hobby.contains(getPerson(personId).advertiseMessages[i].produceId))
@ ==> (\exists int j; 0 <= j && j < getPerson(personId).advertiseMessages.length;
@ getPerson(personId).advertiseMessages[j] == \old(getPerson(personId).advertiseMessages[i]))));
@ ensures (\forall int i; 0 <= i && i < getPerson(personId).advertiseMessages.length;
@ (\exists int j; 0 <= j && j < \old(getPerson(personId).advertiseMessages.length);
@ getPerson(personId).advertiseMessages[i] == \old(getPerson(personId).advertiseMessages[j])));
@ ensures getPerson(personId).advertiseMessages.length ==
@ (\num_of int i; 0 <= i && i < \old(getPerson(personId).advertiseMessages.length);
@ getPerson(personId).hobby.contains(getPerson(personId).advertiseMessages[i].produceId));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId);
@*/
public void deleteHateAdvertisements(int personId) throws PersonIdNotFoundException;
查询产品销售额:
/*@ public normal_behavior
@ requires containsProduce(id);
@ ensures (\exists int i; 0 <= i && i < produces.length; produces[i].getId() == id &&
@ \result == produces[i].getNum());
@ also
@ public exceptional_behavior
@ signals (ProduceIdNotFoundException e) !containsProduce(id);
@*/
public /*@ pure @*/ Person getProduceNum(int id) throws ProduceIdNotFoundException;
Advertiser向Customer发送广告:
/*@ public normal_behavior
@ requires containsAdvertiseMessage(id) &&
@ containsCustomer(getAdvertiseMessage(id).getPerson2());
@ assignable getAdvertiseMessage(id).getPerson2().advertiseMessages;
@ ensures !containsAdvertiseMessage(id) && advertiseMessages.length == \old(advertiseMessages.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(advertiseMessages.length) && \old(advertiseMessages[i].getId()) != id;
@ (\exists int j; 0 <= j && j < advertiseMessages.length; advertiseMessages[j].equals(\old(advertiseMessages[i]))));
@ ensures (\forall int i; 0 <= i && i < \old(getAdvertiseMessage(id).getPerson2().getAdvertiseMessages().size());
@ \old(getAdvertiseMessage(id)).getPerson2().getAdvertiseMessages().get(i+1) == \old(getAdvertiseMessage(id).getPerson2().getAdvertiseMessages().get(i)));
@ ensures \old(getAdvertiseMessage(id)).getPerson2().getAdvertiseMessages().get(0).equals(\old(getAdvertiseMessage(id)));
@ ensures \old(getAdvertiseMessage(id)).getPerson2().getAdvertiseMessages().size() == \old(getAdvertiseMessage(id).getPerson2().getAdvertiseMessages().size()) + 1;
@ also
@ public exceptional_behavior
@ signals (AdvertiseMessageIdNotFoundException e) !containsAdvertiseMessage(id);
@ signals (PersonIdNotFoundException e) containsAdvertiseMessage(id) &&
@ !containsCustomer(getAdvertiseMessage(id).getPerson2());
@*/
public void sendAdvertise(int id) throws
AdvertiseMessageIdNotFoundException, PersonIdNotFoundException;
四、单元学习体会
我有以下几点体会:
第一,JML规格对于编写代码有着很好的简化作用。这一单元的作业思维难度比前两个单元低了不少,大多数情况只需要仔细阅读、理解JML规格就可以完成代码的编写,这让我非常直观地感受到JML规格带来的好处。
第二,JML规格写起来有点反人类。JML好用归好用,但是难写是真的难写。有时候用自然语言非常容易描述的东西,用JML却极难描述;除此之外,JML中有一些ensure语句基本上已经是约定了,但是在形式化语言里还是要写,有些冗余。
第三,好的数据结构、好的算法对于功能的实现、性能的优化有着举足轻重的地位。比如第一次作业的并查集,相较于我最初使用的DFS,在性能方面强了无数倍。
我认为,以后在构思算法、结构的时候,应该先明确最终目的是什么、不可缺少的东西是什么,根据这些来构思:就比如isCircle里,我们只需要判断两个节点是否相连,即两个节点是否在一棵树上,而不用关注这两个节点之间是怎么相连的,所以我们就只需要知道所有节点所在树的根结点就可以得出结论,而不用遍历树,而这就是并查集的核心思想。