BUAA北航OO第三单元总结

OO第三单元总结

第三单元的内容主要是契约式编程,根据官方给定的JML进行代码的编写。由于JML给出了每个方法的前置条件、后置条件、副作用,实现难度和前两个单元比起来可谓是低了很多,但规格是规格,实现是实现,如果实现完全按照官方JML来写一定是不行的,否则很容易TLE(比如第十次作业的qgvs)

架构设计

UML类图

HW9

HW9

HW10

HW10

HW11

HW11

可以看出都是课程组给好的架构,只是为了维护连通块建立了一个Disjoint类维护并查集

架构设计

本单元作业是模拟一个社交网络,通过阅读JML我们可以知道每个person是社交网络图中的一个点,而person之间的relation则是图中的边,而person类中的acquaintance属性则是一个点与之连通的点,Network中的addRelation指令则是在两个person之间添加两条有向边,people属性则是图结构的邻接表

由此可见,JML已经帮我们描绘了一个天然的带权无向图因此图结构不需要特别维护,用到图结构的时候我们只需要使用JML要求我们维护的people邻接表即可。

算法技巧

1.并查集

qci指令要求我们频繁查询图中两个点的连通性,而并查集天生就是为了这种要求而存在的,只需要在ap中为并查集添加节点、ar中维护并查集连通性,即可实现qci的O(1)查询

但是仁慈的课程组并没有要求我们实现删除两个人之间的关系的指令,否则原版并查集将无法使用。我在做作业时曾搜索了一些资料,发现也不是不行,只是需要维护一个虚源点,但如果出题出到这份上似乎就违背OO的本意了,所以我觉得往后的OO第三单元也不会出现这条指令。。。

但值得注意的是,并查集有按秩合并和路径压缩(启发式合并)两种写法(两者也可以一起用,但路径压缩会破坏按秩合并的规则性),其中路径压缩的递归写法可以构造数据爆栈,即图结构退化成链结构时。比如这样的数据

ap 1 1 200
ap 2 2 200
ap 3 3 200
ap 4 4 200
.....
ap 5000 5000 200
ar 1 2 1000
ar 2 3 1000
ar 3 4 1000
ar 4 5 1000
ar 5 6 1000
ar 6 7 1000
ar 7 8 1000
......
ar 4999 5000 1000
qci 1 5000

但需要1w条指令才能爆栈,而互测的指令数被限制为5000条(否则想必又是一场腥风血雨)

2.堆优化的prim算法(最小生成树)

第十次作业要求我们实现qlc指令,即给出从一个点出发的最小生成树,最小生成树的算法有prim和克鲁斯卡尔

其中注意到我们的作业加一个点需要一条指令ap,加一条边需要在加点的基础上再加边,平均下来需要指令数更多,因此我们作业的图一般情况下是稀疏图,而稀疏图使用克鲁斯卡尔比较好,有O(mlog(m))的复杂度

但我最终选用的是堆优化的prim算法,O(mlog(n))复杂度,一方面是考虑到其与克鲁斯卡尔复杂度差不多,并不会超时;另一方面是考虑到使用克鲁斯卡尔还需要额外多建立一个类,再维护边的信息,比较麻烦

堆优化的prim只需要采用java自带的PriorityQueue以及使用new int[]{node, dist}在qlc的方法中维护到每个点的最短路,并不需要建立额外的类,比较方便

实现重点:前面说到JML已经帮我们维护了一个天然的邻接表存储的图形结构(这也是堆优化的prim算法容易实现的一个原因:邻接表存储),我们只需要从给定的id开始,对当前id的person(点)的熟人列表(边)进行遍历,将满足条件的熟人的id加入到堆中即可,无需维护额外的数据结构

3.堆优化的迪杰斯特拉算法(最短路)

第十一次作业的sim指令要求我们将查询两个person之间的最短路,最短路的算法有很多(迪杰斯特拉、floyd、spfa、Bellman等),我选择其中性能较好(spfa已死)又比较容易实现的堆优化的迪杰斯特拉算法,复杂度为O(nlog(n))

由于堆优化的prim算法和堆优化的迪杰斯特拉极为相似(这也是最小生成树使用堆优化prim的好处之一😋),所以我直接把最小生成树的算法的代码复制过来,修改了几行就完成了,实现的重点也与上文所说的堆优化的prim算法一致

容器选择

由于要实现快速存取,大部分容器都采用的是哈希表

1.NetWork

private HashMap<Integer, Person> people;
private HashMap<Integer, Group> groups;
private HashMap<Integer, Message> messages;
private HashMap<Integer, Integer> emojiIdMap;
private DisjointSet disjointSet;

利用HashMap实现O(1)存取(哈希冲突退化成红黑树变成log(n))

其中的people实现了邻接表

2.Group

private HashMap<Integer, Person> people;

3.Person

private LinkedList<Message> messages;
private HashMap<Integer, Person> acquaintance;
private HashMap<Integer, Integer> values;

其中的messages使用LinkedList维护消息的有序性,便于取最近的前4个消息

其中的acquaintance实现了邻接表

4.Message

未使用容器

5.Exception

private static HashMap<Integer, Integer> messageExceptionCounter = new HashMap<>();
private static int totalExceptionCounter = 0;

messageExceptionCounter存储id与异常次数映射

totalExceptionCounter存储异常总次数

性能问题和修复情况

本次作业中在最短路、最小生成树、连通块这些大算法中并没有出现问题,但在一个不起眼的qgvs中出现了性能问题,详见分析自己的bug

同时在本地测试时还遇到了System.out.printf和System.out.println的性能差异(导致de了一晚上的bug),详见

基于JML规格的数据生成及自动化测试

​ 我在本单元如前两个单元一样搭建了评测机,对自己的程序进行正确性检验(之所以叫正确性检验,是因为一些极限数据是很难由评测机自动生成的,只能通过手工构造)。本单元的评测机搭建比较像第一单元,主要分为数据生成和自动化测试,其中难点在数据生成(与第一单元一样),自动化测试只需要和小伙伴对拍即可,无需像第二单元一样搭建起一套完整的评测逻辑。

在本单元中我并没有学习Junit的使用,一方面是翻看往届博客的时候发现这东西貌似风评不太好。。。一方面是评测机的数据也是基于JML来写的,也能做到覆盖性测试,另一方面,值得注意的是,无论是评测机还是JML,都只能保证程序输出结果的正确性,但无法保证程序的时间性能,而我又比较想写评测机,于是就放弃了Junit

数据生成

由于三次作业的数据生成都较为相似,只是根据指令的迭代开发,于是这次的数据生成便放在一块讲了。

这次的数据生成与前两个单元有些不同,前两个单元主要是根据指导书的输入要求来进行数据生成,而第三单元的数据生成则是对着官方包的源码接口方法的JML一步一步生成,其中不仅需要构造正常的数据,还需要根据JML的public exceptional_behavior进行异常数据的生成。其实写数据生成时就相当于把作业又重新做了一遍。

生成数据的策略是覆盖到每一条指令的正常行为和异常行为,确保在大量数据的轰炸下能全面地根据JML规格触发对应的正常行为和异常行为。

数据初始化

由于需要触发一些“人\组\消息ID已经存在或者不存在”的异常,于是选择先初始化一个常量池,从池中选取对应的ID添加到对应的ID列表中,方便指令异常行为的编写,同时为了某些指令人与人、人与组直接连接的前置条件,采用字典来映射人与人、人与组直接连接的状态。同时在第三次作业中的sim指令需要我们对isCircle的两个人之间发送消息,于是采用并查集维护人与人之间的连通块

father = {} #维护并查集
noPersonIdList = [i for i in range(-4000, 4000)] #选取Person的ID
personIdList = []
noGroupIdList = [i for i in range(-1500, 1500)] #选取组的ID
groupIdList = []
messageIdList = []
noMessageIdList = [i for i in range(-2000, 2000)] #选取消息ID
emojiIdList = []
noEmojiIdList = [i for i in range(10001)] #选取表情ID
linkedPersonGroup = []
personLinkedDic = collections.defaultdict(list) #人与人直接连接的关系图
groupLinkedDic = collections.defaultdict(list) #组与人连接的关系

异常概率

在data.py开头设置每条指令的异常概率,方便调整

addPersonExp = 0.3
addRelationExp = 0.3
queryValueExp = 0.3
isCircleExp = 0.3
addGroupExp = 0.3
addToGroupExp = 0.3
queryGroupPeopleSumExp = 0.3
queryGroupValueSumExp = 0.3
queryGroupAgeVarExp = 0.3
addMessageExp = 0.3
messageTypeZero = 0.5
messageTypeOne = 0.1
sendMessageExp = 0.3
querySocialValueExp = 0.3
queryReceivedMessagesExp = 0.3
queryLeastConnectionExp = 0.3
addRedEnvelopeMessageExp = 0.1
cleanNoticesExp = 0.3
addEmojiMessageExp = 0.3
storeEmojiIdExp = 0.3
queryPopularityExp = 0.3
queryMoneyExp = 0.3

指令编写

为确保覆盖到JML的所有情况,所以这一部分需要考虑的因素很多,最好的办法就是对着JML来写每条指令的生成函数(相当于重做一遍作业,但可以让人对JML更熟悉,减少出错的可能)。

拿am指令来做个生成示例

展开代码
def addMessage(returnType=0):
    p = random.random()
    if p < addMessageExp:
        p1 = random.random()
        if p1 > 0.5:
            if not messageIdList:
                return addMessage()
            messageId = random.choice(messageIdList)
            if personIdList:
                id1 = random.choice(personIdList)
            else:
                id1 = random.choice(noPersonIdList)
            id2 = id1
        else:
            if not noMessageIdList:
                return sendMessage()
            messageId = random.choice(noMessageIdList)
            noMessageIdList.remove(messageId)
            messageIdList.append(messageId)
            if personIdList:
                id1 = random.choice(personIdList)
            else:
                id1 = random.choice(noPersonIdList)
            id2 = id1
        messageType = random.randint(0, 1)
        socialValue = random.randint(-1000, 1000)
    else:
        p2 = random.random()
        if p2 > messageTypeZero:
            messageType = 0
            messageId = random.choice(noMessageIdList)
            noMessageIdList.remove(messageId)
            messageIdList.append(messageId)
            if len(personIdList) < 3:
                return addPerson()
            p3 = random.random()
            if p3 > sendMessageExp:
                if not personLinkedDic:
                    return addRelation()
                id1 = random.choice(linkedPersonGroup)
            else:
                id1 = random.choice(personIdList)
            if id1 in linkedPersonGroup: #Disjoint Union
                id2 = random.choice(linkedPersonGroup)
                while find(id2) != find(id1):
                    id2 = random.choice(linkedPersonGroup)
            else:
                id2 = random.choice(personIdList)
            while id2 == id1:
                id2 = random.choice(personIdList)
        else:
            messageType = 1
            if not noMessageIdList:
                return sendMessage()
            messageId = random.choice(noMessageIdList)
            noMessageIdList.remove(messageId)
            messageIdList.append(messageId)
            if len(groupIdList) < 1:
                return addGroup()
            if not personIdList:
                return addPerson()
            if not groupLinkedDic:
                return addToGroup()
            if random.random() > sendMessageExp:
                id2 = random.choice(list(groupLinkedDic.keys()))
                id1 = random.choice(groupLinkedDic[id2])
            else:
                id2 = random.choice(groupIdList)
                id1 = random.choice(personIdList)
        socialValue = random.randint(-1000, 1000)
    if returnType == 0:
        return "am {} {} {} {} {}".format(messageId, socialValue, messageType, id1, id2)
    else:
        return [messageId, messageType, id1, id2]

数据生成

数据生成使用了类似C语言中函数指针的语法

functionList = [addPerson, addRelation, queryValue, queryPeopleSum, queryCircle, queryBlockSum, addGroup, addToGroup,delFromGroup, queryGroupValueSum, queryGroupAgeVar, addMessage, sendMessage, queryReceivedMessages,queryLeastConnection, addRedEnvelopeMessage, addNoticeMessage, clearNotices, addEmojiMessage,storeEmojiId, queryPopularity, deleteColdEmoji, queryMoney, sendIndirectMessage]

将所有函数放到一个列表中并制定函数数量functionNum,然后从函数列表中随机选择functionNum个函数

functionNum = 24

def generDataSpecial():
    global functionList
    funcList = random.sample(functionList, functionNum)
    #funcList = [addPerson, addRelation, addMessage, addRedEnvelopeMessage, addEmojiMessage, addNoticeMessage, storeEmojiId, deleteColdEmoji, queryMoney, sendIndirectMessage]
    #funcList = [addPerson, addRelation, addEmojiMessage, storeEmojiId, sendIndirectMessage, deleteColdEmoji,sendMessage]
    s = useFuncToGengerData(funcList)
    return s

然后再利用被选择的函数进行数据生成

def useFuncToGengerData(functions: list):
    s = ''
    for i in range(20000):
        s += random.choice(functions)()
        s += '\n'
    return s

这样做的好处是可以对指定数量的指令进行数据生成,同时能够手动指定需要进行测试的指令

这种数据生成模式在第三次作业中使用,反观前两次数据生成模式,都是采用p = random.random(),再根据p的大小选择对应的指令

展开代码
    p = random.random()
            if 0.95 < p:
                context = addPerson()
            elif 0.90 < p <= 0.95:
                context = addRelation()
            elif 0.80 < p <= 0.85:
                context = addGroup()
            elif 0.75 < p <= 0.80:
                context = queryValue()
            elif 0.70 < p <= 0.75:
                context = addToGroup()
            elif 0.65 < p <= 0.70:
                context = delFromGroup()
            elif 0.60 < p <= 0.65:
                context = queryCircle()
            elif 0.55 < p <= 0.60:
                context = queryBlockSum()
            elif 0.50 < p <= 0.55:
                context = queryPeopleSum()
            elif 0.45 < p <= 0.5:
                context = queryGroupValueSum()
            elif 0.4 < p <= 0.45:
                context = queryGroupAgeVar()
            elif 0.35 < p <= 0.4:
                context = addMessage()
            elif 0.3 < p <= 0.35:
                context = sendMessage()
            elif 0.25 < p <= 0.3:
                context = querySocialValue()
            elif 0.2 < p <= 0.25:
                context = queryReceivedMessages()
            elif 0.15 < p <= 0.20:
                context = queryLeastConnection()
            elif 0.1 < p <= 0.15:
                context = addMessage()
            elif 0.05 < p <= 0.1:
                context = sendMessage()
            else:
                context = queryLeastConnection()
        else:
            p = random.random()
            if 0.65 < p:
                context = addMessage()
            elif 0.35 < p:
                context = sendMessage()
            else:
                context = addGroup()

这样不仅不美观,而且不利于评测机的迭代开发,而换乘成第三次的数据生成的模式后,一下就清爽了很多,后续需要添加指令也只需要在函数列表里面添加就可以了。由此可见,OO的思想不仅在作业里需要用到,评测机的编写也可以用到OO的思想。

但美中不足的是,我只能让生成的指令呈均匀分布,而无法对特定指令调高生成概率

自动化评测

就是利用python的subprocess库获取jar包的输出,然后对每个测试的jar包的输出进行比较,如果有错,则将输入和出错的地方写入到error文件中,方便查看

for jar in jarList:
    cmd = "java -jar {}".format(jar + '.jar')
    proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                            encoding='utf-8')
    stdout, stderr = proc.communicate(stdinData)

自己与他人的bug分析

由于搭建了自己的评测机并在本地进行了充分的测试,所以三次作业中没有出现正确性bug,但出现了一个tle的bug。这也能看出评测机的缺点:难以生成极端测试用例,但这一点Junit也一样,所以极限数据还是需要自己手搓的。

分析自己的bug

在第二次作业的互测中被hack了一次qgvs的ctle的bug

先来看看qgvs的JML

/*@ ensures \result == (\sum int i; 0 <= i && i < people.length; 
@          (\sum int j; 0 <= j && j < people.length && 
@           people[i].isLinked(people[j]); people[i].queryValue(people[j])));
@*/

其实就是O(n**2)查询组内价值总和,可以在添加时O(n)维护,也可以每次双重循环遍历

双重遍历有两种写法,一种是双重循环遍历组内的人,为O(n**2)的复杂度,另一种是一重循环遍历组内的人,然后第二重循环遍历这个人的熟人,这种方法是O(N+E)复杂度。

很明显前一种在图极为稠密(完全图)的情况下才较第二种方法的复杂度有优势,第二种在稀疏图中的复杂度远低于第一种。而根据我们作业来看,加点(ap)需要一条指令,加边(ar)又需要一条指令,因此在总指令数一定的情况下,我们的图大部分情况下都是稀疏的,因此选择第二种O(N+E)的方法就能保证复杂度了

但是很不幸,我选择的是第一种。因为这个方法是在第一次作业中出现的,而第一次作业的指令总数最多被限制在1000条,因此我觉得O(n**2)的方法也应该不会超时。第二次作业的时候已经忘了这个地方,于是就悲剧ε(┬┬﹏┬┬)3了。

他人的bug

第一次作业

房间有且只有一个老哥被hack到了,原因是qci没有用并查集维护,复杂度是O(n**2),被别人hack烂了

第二次作业

房间里除了我还有一位老哥qgvs是O(n**2)复杂度,也被hack烂了

房间里还有一位是并查集维护出错,但是本地的评测机没有测出来,可能是数据强度不够OAO

第三次作业

房间里有一个qm出错,评测机跑第一条数据就发现了

有趣的bug(JAVA里printf和pringln的区别)

在第二次作业里,我除了使用了自己的评测机测试外,还用了别人的评测机。在使用别人评测机的过程中,我发现在别人的稀疏图数据下我的程序会跑十几秒,而一起对拍的其他人大都只要几秒,于是我便去寻找原因。

我从堆优化的prim的写法debug到路径压缩的并查集,最后连在prim中使用new int[]的速度都考虑到了,但是我的程序的速度仍然和原来一样,正当我百思不得其解之际,突然隐隐约约地想到了似乎我异常输出用的是System.out.printf,其他人似乎用的是System.out.println,在学java语法的时候似乎了解过他们之间的速度有差异,于是上网找资料,最后发现果不其然,原因就出在printf比println慢上,printf更换成了println以后就快了10s多

但我网上搜索了很久,并没有找到二者速度差异大的原因,希望有知道的大佬可以评论区告诉我😄

下面给出一个JAVA快速输入输出模板


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
 //import java.io.*;
public class 快速输入输出 {
 
	public static void main(String[] args) throws IOException {
		// TODO 自动生成的方法存根
		BufferedReader bur1=new BufferedReader(new InputStreamReader(System.in));
		//想输入一个数(数组长度)
		String c=bur1.readLine();
		int b=Integer.valueOf(c);
		//想输入一个数组
		String aString=bur1.readLine();
		String a[]=aString.split(" ");
		int v[]=new int[b];
		for (int i = 0; i < a.length; i++) {
			 v[i]=Integer.valueOf(a[i]);
		}
		//为了获得最高效率,可考虑将 OutputStreamWriter 包装到 BufferedWriter 中,以避免频繁调用转换器。
		//Writer writer=new OutputStreamWriter(System.out);
		BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(System.out));
		for (int i = 0; i < a.length; i++) {
			 writer.write(a[i]);
		}
		writer.write("\r");//
		writer.write("hello");
		writer.flush();//释放资源
	}
 
}

Network扩展

概述

首先可以看出Advertiser、Producer、Customer都是Person的子类,ap、ar的方法不需要改变

Advertiser需要发送的广告可以看成一种message,在addAdvertisement中添加,message类型为2,只含有person1(为对应的Advertiser)

public void sendAdvertisement(int id) throws MessageIdNotFoundException, noAdvertisementException; //发送id对应的广告,如果消息type不为2(不是广告),就抛出对应异常,否则向与Advertiser有关系的人(除了producer之外)发送广告消息

Producer通过Advertiser销售产品,即通过addAdvertisement其中的person1为对应的Advertiser),message类型为2,且Producer与对应的Advertiser之间要有关系

public void addAdvertisement(Message message) throws EqualMessageIdException, EqualPersonIdException;

Customer购买产品直接通过Advertiser给相应Producer发一个购买消息,可分解成Customer添加购买消息、Advertiser向对应的Producer发送购买消息

public void addPurchaseMessage(Message message) throws EqualMessageIdException, noAdvertiserException, noCustomerException, noProducerException; //添加购买消息

public void sendPurchaseMessage(Message message) throws MessageIdNotFoundException; //发送购买消息

JML规格

添加广告信息

/*@ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 2 &&
      @          getMessage(id).getPerson1() instancedof Advertiser;
      @ assignable messages;
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson1().acquaintance[i]);
      @          \old(getMessage(id).getPerson1().acquaintance[i].getMessages().get(i+1) == \old(getMessage(id).getPerson1().acquaintance[i].getMessages().get(i)));
      @ ensures \old(getMessage(id).getPerson1().acquaintance[i].getMessages().get(0).equals(\old(getMessage(id)));
      @ ensures \old(getMessage(id)).getPerson1().acquaintance[i].getMessages().size() == \old(getMessage(id).getPerson1().acquaintance[i].getMessages().size()) + 1;
  	  @ also
      @ public exceptional_behavior
      @ signals (noAdvertisementException e) getMessage(id).getType() != 2;
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (noAdvertiserException e) containsMessage(id) && getMessage(id).getType() == 2 &&
      @          !(getMessage(id).getPerson1() instanceof Advertiser);
      @*/   
public void sendAdvertisement(int id) throws MessageIdNotFoundException, noAdvertisementException, PersonIdNotFoundException, noAdvertiserException;

发送广告信息

 /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
      @ message.getType() == 2 && (message.getPerson1() instanceof Advertiser);
      @ assignable messages;
      @ ensures messages.length == \old(messages.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
      @          (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
      @ also
      @ public exceptional_behavior
      @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message));
      @ signals (noAdvertiserException e) !(\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message)) && !(messages[i].getPerson1() 			  @										instanceof Advertiser);
      @*/
public void addAdvertisement(Message message) throws EqualMessageIdException, noAdvertiserException;

添加采购信息

 /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
      @ message.getType() == 3 && (message.getPerson1() instanceof Advertiser) && (message.getPerson2() 		  @ instanceof Customer) && (message.getPerson3() instanceof Producer);
      @ assignable messages;
      @ ensures messages.length == \old(messages.length) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
      @          (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
      @ also
      @ public exceptional_behavior
      @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message));
      @ signals (noAdvertiserException e) !(\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message)) && !(messages[i].getPerson1() 			  @										instanceof Advertiser);
      @ signals (noCustomerException e) !(\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message)) && !(messages[i].getPerson1() 			  @										instanceof noCustomerException);
      @ signals (noProducerException e) !(\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message)) && !(messages[i].getPerson() 			  @										instanceof noProducerException);
      @*/
public void addPurchaseMessage(Message message) throws EqualMessageIdException, noAdvertiserException, noCustomerException, noProducerException; //添加购买消息

心得体会

首先要感谢课程组的仁慈,这个单元的难度比起前两个单元的确是低了很多,完成每周OO任务的时间也短了很多(虽然写评测机的时间好像还是差不多?),能有更多时间去女娲补天我的离散和概统🤣

这一单元的互测也是OO最后一个单元的互测,虽然继第一单元的HW2后又被刀了一次,但是HW9-HW11都有刀到人,也都搭建了自己的评测机,再加上从第一次作业到第十一次作业,除了博客作业外每次都有刀到人,也算是为以前从他人口中“恶名昭彰~”的OO互测(圣杯战争)落下了圆满的帷幕😄

其实这一单元所接触到的契约式编程对我们并不陌生,现在看来,似乎OS的课下任务就是契约式编程,大部分函数前都给出了前置条件和后置条件,甚至有的还有副作用🤪所以在写这一单元的oo给我的感觉就像在写os一样

经过这一单元的学习,我读了很多JML,觉得这东西有优点也有缺点。优点自然不必说,只要把JML写好了就能清晰地向编写代码的人传递需求,没有自然语言的二义性,哪怕编写者一开始对JML理解有误,也可以通过反复阅读JML解决。但缺点也是很明显的,那就是JML有点太复杂了,比如求一个点到另一个点的最短路,短短几个字便可以说清楚的东西用JML来描述却要十几行甚至几十行,对编写JML的人也是一个挑战。使用JML能够清晰地传递需求固然是好事,但是编写JML花费的时间会不会比需求者和编写者之间直接沟通花费的时间多呢?我认为这也是一个trade off。

最后预测一下明年OO第三单元的题目,这一单元似乎已经与数据结构和算法绑定了🤣留给课程组简单的图论算法不多了。盲猜一下明年可能有"带有负边的最短路"、"判断负环"、"二分图最大匹配"、"维护二叉搜索树(来个手写AVL和红黑树)"、"维护哈夫曼树"😆

posted @ 2022-05-31 09:21  繁华丶人间  阅读(157)  评论(2编辑  收藏  举报