面向对象程序设计第三单元总结
第三单元博客作业
一、自测过程
在自测之间,我一般会反复阅读自己的代码,静态地看一下可能的问题。
- 在第一次作业的时候,尝试使用了
JUnit
来构造数据,但是发现构造的时候非常的麻烦,基本上是手动构造。 - 之后就开始使用对拍的方法来测试了……效果似乎还不错。
二、本单元的架构设计
在本单元中,所谓架构主要是指如何保存信息,JML只是指出了一种抽象的概念框架,但是在实际上可以有不同的选择,在不同的实际实现之间选择最适合本单元测试的,这是一种权衡。
例如在一个NetWork
中,需要去用id
去索引 MyPerson
,MyNetwork
等元素。在JML提供的抽象概念中,使用了一个数组来表示这样的概念,由于Id
并不是顺序设置的,因此有可能输入了一个很大的数,使用数组实现就不太实际了。此时我们想到可以使用ArrayList
,可是这个数据结构在索引的时候需要顺序查询,时间复杂度还是偏高。
描述这样的键值对关系,一个更好的办法是使用HashMap
来进行处理,由于哈希表的性质,查询变得非常的快,并且可以提供比数组更好的功能。
综上所述,在本单元中,对于所有的需要索引来查询的对象,都使用HashMap
来保存。
具体而言:
-
MyNetwork中:
people
保存该网络中的所有id => Person
键值对groups
保存该网络中的所有id => group
键值对messages
保存该网络中的所有id => Message
键值对emojiHeat
保存该网络中的emojiId => emojiHeat
键值对
-
MyPerson中:
edges
保存该人与其他人的所有边关系,仍然以键值对的方式存储Integer => edges
在以上的大多数使用HashMap
来保存图的架构中,还有一个例外:对于每个人收到的Message
,由于我们在输出的时候需要按照下标来输出,因此必须真的顺序存储,所以使用了一个ArrayList<Message>
来进行存储。
在存储之后,就是维护策略:
对于大多数“添加”的行为,可以使用HashMap
简单的put
操作来执行维护,只需要更新id
在哈希表中对应的结果就可以了。
但有少数比较Trick的地方,比如在关于Group的getValueSum
的时候,需要进行一些特殊的保存,这些将在下一节中提到。
三、性能问题和修复情况
在本单元中,大多数查询对性能的要求都不高,但是有一些是例外的。
3.1 group::getValueSum
这里的问题是,如果像JML的描述一样使用二重循环来扫描的话,理论的时间复杂度就会达到O(N^2)
,即使强测没有数据卡这个点,互测肯定会。
我的方法是,语气让少数指令的时间复杂度达到平方,不如让所有指令的时间复杂度均摊为线性O(N)
.
实际上,对于每个Person
而言,维护了一个哈希表
private final HashMap<Integer, Integer> groupSum;
这个哈希表的意义在于,建立一个从groupId
,到“该Group中,所有与本人相连的边的权重的总和”,在添加边的时候,时间复杂度从O(1)
变成了O(# Groups)
,但是在执行求组内边权重和的时候,只需要将组内所有的Person
,在该Group
下的groupSum.get(groupId)
求和即可。
3.2 并查集
在本次作业中,需要判断某些Person(以下等同于点)之间是否有边相连,如果点在同一个联通块中就算做一个Block,并且还需要计算所有Block的数量。
这是一个图上分组的问题,用并查集(再加上路径优化)就很合适。
3.3 最小生成树
在第二次作业中,queryLeastConnection
需要求“在某个点所在的Block中的最小生成树”,这里执行一个Kruskal算法就可以了。
3.4 Dijkstra算法的堆优化
似乎在时间复杂度上,并不需要堆优化……
总之,在Dijkstra算法中,每次需要寻找一个与当钱“求出了最短距离”的点集合中,距离最近的那一个点,这里本身是用扫描进行的,但是可以通过使用一个堆来更快地确定这个最小值。
四、Network的扩展
我的想法是,Advertiser自己可以拥有一个广告,然后还拥有一些目标人群,每次调用发送方法,广告者就把自己的广告发送给目标人群。
Class Advertiser extends Person {
// public instance model non_null AdMessage ad;
// public instance model non_null Customer[] customers;
/** 将某个人加入到广告发送组中 */
public void addPeson(Person p);
/** 将自己的广告发送给广告发送组中的每个人*/
public void sendAds();
}
/*
JML of function sendAds():
@ assignable customers[*].getmessages()
@ ensures customers.length == \old(customer.length)
@ ensures(\forall int i; 0 <= i && i < customers.length;customers[i].getmessages().size() == \old(customers[i].getmessages().size()) + 1))
@ ensures(\forall int i; 0 <= i && i < customers.length;
(\forall int j; 0 <= j && j < \old(customers[i].getmessages().size());
\old(customers[i].getmessages().get(j)) == customers[i].getmessages().get(j+1)))
@ ensures(\forall int i; 0 <= i && i < customers.length;
customers[i].getmessages.get(0) == ad)
*/
对于生产商,它应该能够动态地添加商品,还可以查询商品的销售额,对于销售路径,可以用一个Map来表示:即每个广告商提供了多少销售量。
Class Producer extends Person {
private ArrayList<int> productId;
private ArrayList<int> productSale;
/** 生产商可以增加自己能生产的商品*/
public void addProduct(int id);
/** 生产商可以查询某种商品的销售额*/
public void querySale(int id) throws noProductException;
/** 生产商可以查询某种商品的销售路径*/
public Map<Advertiser, int> querySalePath(int);
/** 生产商将自己接收到的最早一笔订单进行处理,更新元数据*/
public void processOrder();
}
/*
JML of function querySale
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < productId.size(); id == productId[i])
@ ensures (\exist int i; 0 <= i && i < productId.size(); id == productId[i] && \result == productSale[i])
@ also
@ public exceptional_behavior
@ requires (\forall int i; 0 <= i && i < productId.size(); id != productId[i])
@ signals noProductException;
*/
对于消费者,它应该能够拥有自己的偏好,还可以选择收到的广告进行购买。
Class Customer extends Person {
private ArrayList<int> perference;
private OrderMessage order;
/** 消费者的偏好是否和广告匹配*/
public void boolean(AdMessage);
/** 消费者可以添加自己的偏好*/
public void addPreference();
/** 消费者选择自己收到的广告中,最新的一条符合自己偏好的广告并进行购买*/
public void buy() throws noAdvertisementException;
}
/*
JML of function buy()
@ public normal_behavior
@ assgiable messages
@ requires (\exists int i; 0 <= i && i < getmessages().size(); getmessages().get(i) instanceof AdMessage && preference.match(get.messages().get(i)))
@ ensures getmessages.size() == \old(getmessages.size()) - 1
@ ensures (\exist int i; 0 <= i && i < \old(getmessages.size());
\old(getmessages.get(i) instanceof AdMessage)
&&
preference.match(get.messages().get(i)))
&&
(\forall int j; 0 <= j && j < \old(getmessages.size()); (\old(getmessages.get(j)) instanceof AdMessage && preference.match(get.messages().get(j)))) ==> (i <= j))
&&
(\forall int j; 0 <= j && j < \old(getmessage.size()); (j != i) ==> (\exists int k; 0 <= k && k < getmessage.size(); getmessages.get(k) == \old(getmessages.get(j)))
&&
(\forall int j; 0 <= j && j < getmessage.size(); getmessages.get(j) != getmessages.get(i))
&&
getmessages.get(i).producer.getmessages().size() == \old(getmessages.get(i).producer.getmessages().size() + 1
&&
(\forall int j; 0 <= j && j < \old(getmessages.get(i).producer.getmessages().size());
\old(getmessages.get(i).producer.getmessages().get(i)) == getmessages.get(i).producer.getmessages().get(i+1))
&&
getmessages.get(i).producer.getmessages().get(0) == order
)
@ also
@ public exceptional_behavior
@ requires (\forall int i; 0 <= i && i < getmessages().size(); !(getmessages().get(i) instanceof AdMessage)))
@ signals noAdvertisementException;
*/
五、学习体会
一开始,我以为JML和普通的Docs是很像的,在学习之后发现,Docs是要简洁很多,而JML可以形式化地给出函数和类的执行要求。
很多时候,在类似的情景,比如OS中看不清楚出题人的意图的时候,真的很想在函数上看到类似于JML的文档,这也说明了(我的理解中)JML的好处:它提供了一个非常清晰的前件、后件的描述。
但是这也是一种权衡,在简洁性和清晰度之间的选择。例如,在最小生成树的那个函数中,JML占了数十行,仅仅是阅读理解JML的意思就需要很久,但是普通的Docs却可以直接描述出来。
我自己在课外了解中发现,JML的使用并不像想象中的广泛,或许在实际的工业产品中,代码的各个变量、组件之间远远比现在的还要复杂,如果要想把所有的函数的效果全部用JML描写的话,会带来巨大的复杂性,这似乎就有点得不偿失了。
总结起来:在本单元中,我们了解了这样一种新的,可以执行规模化设计的语言,可以提高代码的逻辑严密性和可维护性。但在写自己的程序的时候、或是工作中,什么时候需要使用JML,还是值得三思。