BUAA OO Unit3总结
写在前面
第三单元JML规格的学习告一段落,我认为这单元总体的内容并不困难,但如果想要获得理想的成绩,仍然需要刻苦地研究相关的单源最短路径算法,图论的知识也是必不可少的。
这一单元主要考察了JML规格语言的理解,按照规格编写相关程序,因此在代码的设计和架构上并没有前两个单元那么高的要求。但由于JML的实际应用价值不高,并且上机实验的体感不是很好,我认为这单元的教学内容仍然需要进一步进行改良。
第九次作业
作业要求
社交网络的整体框架官方已经给出了JML表述并提供了相应接口。同学们需要阅读JML规格,依据规格实现自己的类和方法。
具体来说,各位同学需要新建两个类 MyPerson
、MyNetwork
(仅举例,具体类名可自行定义并配置),并实现相应的接口方法,每个方法的代码实现需要严格满足给出的JML规格定义。
- 阅读指导书中关于异常类行为的描述,通过继承官方提供的各抽象异常类,实现自己的异常类。抽象异常类已在官方包内给出,这一部分没有提供 JML 规格,各位同学需要仔细阅读指导书中关于异常类的详细描述,结合样例理解其行为,然后继承这些抽象类实现自己的异常类,使其
print()
方法能够正确输出指定的信息。 - 为了检验大家对于规格的理解,请同学们为部分方法编写OK测试,检查前置条件requires、后置条件ensures和pure类方法等限制。
作业分析
本次作业注重对方法规格的训练,对于自主架构的能力要求并不是很高。我们需要根据给出的官方Network
类和Person
类中的方法规格,来实现满足功能的MyNetwork
类和MyPerson
类。
除此之外,还需要自行补充异常处理函数,用来处理出现了各种异常的情况(这部分在指导书中有明确的规定)。
在本次作业中,JML规格的理解没有很难的地方,而让人感到害怕的是指导书中处处透露的杀气。
比如:
公测和互测都将使用指令的形式模拟容器的各种状态,从而测试各个类、接口的实现正确性,即是否满足 JML 规格的定义或者指导书描述。可以认为,只要所要求的三个类的具体实现严格满足 JML,同时异常类的实现符合指导书描述,就能保证正确性,但是不保证满足时间限制。
再比如:
程序的最大运行 cpu 时间为 10s,虽然保证强测数据有梯度,但是还是请注意时间复杂度的控制。
于是这次作业的考点就十分明确了:如何保证在满足规格设计的情况下,利用各种算法确保不出现TLE
的情况。
我们可以先来分析一下官方给出的哪些方法的复杂度可能会出现问题。
首先,Person
类中全部是pure
方法,涉及到的遍历查找复杂度也不超过O(n)
,不会出现超时的可能(根据指导书,n<10000,因此O(n)
的复杂度一定是可以满足条件的)
但我认为针对课程组给出的规格,还可以进行进一步的优化。
在容器的选择上,官方给出的规格是利用两个ArrayList
来存储熟人ID和关系强度,或许可以转化为HashMap
来实现由ID查找强度的存储结构,复杂度由O(n)
降低到了O(1)
,在查找时便可以进一步压缩耗时。
我们再来看看Network
类中的方法规格,发现了如下几个可能出现问题的情况:
isCircle
方法:
/*@ 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;
这是一个用来查找两个人之间是否有联通路径的函数,规格并没有要求一定要使用什么方法实现。
那么我最先想到的应该是剪枝处理的DFS
算法,但实际效率并不高,并且实现起来也不太方便。这时我看到群里有人在讨论并查集算法,于是也自己去研究了一下,发现并查集算法的查找效率比DFS
要高上不少。
为了更好的符合“面向对象”的设计思想,我们可以将并查集封装成类,在类中用HashMap
来存储结点的父子关系,用find()
、merge()
、add()
等方法封装并查集的相关操作。
import java.util.HashMap;
public class DisjointSet {
private final HashMap<Integer, Integer> pre;
private final HashMap<Integer, Integer> rank;
public DisjointSet() {
this.pre = new HashMap<>();
this.rank = new HashMap<>();
}
public void add(int id) {...}
public int find(int id) {...}
public void merge(int id1, int id2) {...}
}
连通块个数相关方法:
/*@ 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();
/*@ ensures \result ==
@ (\sum int i; 0 <= i && i < people.length;
@ (\sum int j; i < j && j < people.length;
@ (\sum int k; j < k && k < people.length
@ && getPerson(people[i].getId()).isLinked(getPerson(people[j].getId()))
@ && getPerson(people[j].getId()).isLinked(getPerson(people[k].getId()))
@ && getPerson(people[k].getId()).isLinked(getPerson(people[i].getId()));
@ 1)));
@*/
public /*@ pure @*/ int queryTripleSum();
在这两个方法的处理上,我犯了比较严重的错误。规格给出了三重循环的查找方法,我也没有多想,就直接套用了,但忽视了关于CPU时长的问题。强测数据条数上限10000,三重循环就直接寄了。
互测与强测
本次作业,我受到5次hack,并成功hack他人5次。强测得分60分。
出现bug的原因都是由于对于qbs
和qts
指令的处理,均采用复杂度为O(n3)
的不良算法,进而导致了TLE
的产生。
BUG修复
将相关变量如blockSum
和tripleSum
的静态维护转化为动态维护即可解决TLE
的问题。
具体而言,由于blockSum
和tripleSum
的变化只会在出现ap
、ar
时产生,所以我们不妨把对它们的维护放在接收这两个指令的时候。
for (Person person : p1.getAcquaintance().keySet()) {
if (person.isLinked(p2)) {
tripleSum++;
}
}
if (disjointSet.find(id1) != disjointSet.find(id2)) {
blockSum--;
}
维护逻辑也比较简单:对于blockSum
,由于在ap
时加入的人一定是一个孤立节点,因此恒加一;而在ar
时,需要通过构建的并查集判断两个节点有无公共父亲,若有,意味着两个块之间可以融合,因此需要减一。
而对于tripleSum
,如果在二者相连之前,已经有第三方作为中介,那么在二者相连后,一定能和该第三方产生一个三元闭环,tripleSum
在这种情况下加一即可维护。
第十次作业
作业要求
本次作业的程序主干逻辑我们均已经实现,只需要同学们完成剩下的部分,即:
- 通过实现官方提供的接口
Person
、Network
、Message
和Group
,来实现自己的Person
、Network
、Message
和Group
类。 - 阅读指导书中关于异常类行为的描述,通过继承官方提供的各抽象异常类,实现自己的异常类。
Person
、Network
、Group
和Message
类的接口定义源代码和对应的 JML 规格都已在接口源代码文件中给出,各位同学需要准确理解 JML 规格,然后使用 Java 来实现相应的接口,并保证代码实现严格符合对应的 JML 规格。具体来说,各位同学需要新建四个类 MyPerson
、 MyNetwork
、 MyGroup
、MyMessage
并实现相应的接口方法,每个方法的代码实现需要严格满足给出的 JML 规格定义。
抽象异常类已在官方包内给出,这一部分没有提供 JML 规格,各位同学需要仔细阅读指导书中关于异常类的详细描述,结合样例理解其行为,然后继承这些抽象类实现自己的异常类,使其 print()
方法能够正确输出指定的信息。
当然,还需要同学们在主类中通过调用官方包的 Runner
类,并载入自己实现的 Person
、Network
、Group
和Message
类 ,来使得程序完整可运行,具体形式下文中有提示。
作业分析
乍一看:哎呀这和上次作业有什么区别,不就是再多加几个方法嘛,不就是按规格新建两个类嘛,太简单了。
分析后:mr
我要杀你一千遍还不够!
这次作业属实让人难受不已,最难以处理的是modifyRelation
中,当queryValue + value < 0
的时候,需要我们从社交网络中把两个人的关系移除。这个移除不要紧,牵一发而动全身啊。你们两位的关系美美破裂了,可是qbs
和qts
有话说了,qgvs
和qgav
也不乐意了,qba
和qcs
争着抢镜头,属于是花团锦簇了。
当然,这些数据的维护麻烦是麻烦,但细心点认真点总能解决。可是关系图该怎么办,上次作业的压缩并查集彻底用不了了(它无法处理删边的请求)。
于是我抛弃了查找性能无敌的并查集,转而退化为BFS
虔诚的信徒。有一说一,BFS
算法的确很好理解,写起来也很方便,但它的劣势在于查找关系和删除关系的时候,都会各自引用一个复杂度O(n)
的方法,大大大大增加了程序的运行时间。
public boolean bfs(int id1, int id2) {
MyPerson p1 = (MyPerson) getPerson(id1);
MyPerson p2 = (MyPerson) getPerson(id2);
if (p1.getAcquaintance().isEmpty()) {
passedList.clear();
return false;
}
ArrayList<Person> aqcList
= new ArrayList<>(p1.getAcquaintance().keySet());
ArrayList<Person> queue = new ArrayList<>();
passedList.add(p1);
if (p1.isLinked(p2)) {
passedList.clear();
return true;
}
for (int i = 0; i < aqcList.size(); i++) {
if (passedList.contains(aqcList.get(i))) {
continue;
}
queue.add(aqcList.get(i));
if (aqcList.get(i).isLinked(p2)) {
passedList.clear();
return true;
}
if (i == aqcList.size() - 1 && !queue.isEmpty()) {
int id = queue.get(0).getId();
queue.remove(0);
passedList.add(getPerson(id));
bfs(id, id2);
}
}
passedList.clear();
return false;
}
这里我没有为BFS
新创建一个方法类,而是基于已有的增加节点和增加关系的算法进行的查找。有一点需要注意:在社交关系图中,节点之间的路径都是双向的,如果不进行特殊处理,一定会导致无限的循环递归,造成程序错误。我的方法是创建一个passedList
集合,每当一个节点作为父节点查找结束,就将它放入这个集合中,在查找别的节点时,直接跳过这个已经查找过的节点,以此来修正一个单向的树结构(严格来说不是一个树)。
bfs
的结果返回一个布尔类型值,用来表示是否联通。所以不难看出,在涉及到连通性检查的所有方法中都需要调用bfs
进行,时间成本大大增加。但也有优点:逻辑简单(
除此之外,新增了qba
和qcs
指令,和qbs
和qts
异曲同工,承袭二者的维护方法,我也采用了动态维护的方法,可以将其复杂度由O(n2)
减少到O(n)
,目前来看是最优方法。下面是相关的方法。
@Override
public int queryCoupleSum() {
int coupleSum = 0;
for (Integer index : people.keySet()) {
MyPerson person = (MyPerson) people.get(index);
if (person.getHasCouple()) {
coupleSum++;
}
}
return coupleSum / 2;
}
public void checkCouple() {
hasCouple = false;
ArrayList<Person> tmpAcquaintance
= new ArrayList<>(acquaintance.keySet());
for (Person item : tmpAcquaintance) {
MyPerson person = (MyPerson) item;
if (bestAcquaintance.equals(person)
&& person.getBestAcquaintance().equals(this)) {
hasCouple = true;
break;
}
}
}
public void checkBestAcquaintance(int value, Person person) {
if (value > bestValue) {
bestAcquaintance = person;
bestValue = value;
}
}
但这个比较繁琐,因为需要维护的地方确实比较多,细心一些尽量面面俱到。
其他的指令并不复杂,只要按照规格规规矩矩地写就好。(必须que一下OKTest
)
互测与强测
本次作业,我受到2次hack,并成功hack他人9次。强测得分41.6667分。
出现bug的原因是由于对于bestAcquaintance
的维护没有考虑周到,导致了qba
和qcs
指令无法得到正确的取值。除此之外,我的BFS
算法也出现了问题。由于我直接在MyNetwork
类中实现BFS
,因此在每次调用BFS
时,我应该在全局中维护等待队列queue
,而不是在递归的过程中直接new一个新的等待队列。这导致了我的BFS
产生了致命错误,于是qbs
和qci
指令也会出现错误。(说的好听结果自己中招了)
BUG修复
研究后,我认为不如直接在qba
和qcs
发生时再进行查找bestAcquaintance
和coupleSum
,这样虽然时间复杂度会出现问题,但可以保证不出现TLE
,并且成功修复了这个问题。
BFS
的修复也如上所示,我修复了相关的内容,也解决了该问题。
第十一次作业
作业要求
本次作业的程序主干逻辑我们均已经实现,只需要同学们完成剩下的部分,即:
- 通过实现官方提供的接口
Person
、Network
和Group
,来实现自己的Person
、Network
和Group
类。 - 通过实现官方提供的接口
Message
、EmojiMessage
、NoticeMessage
和RedEnvelopeMessage
,来实现自己的Message
、EmojiMessage
、NoticeMessage
和RedEnvelopeMessage
类。 - 阅读指导书中关于异常类行为的描述,通过继承官方提供的各抽象异常类,实现自己的异常类。
Person
、Network
和 Group
类的接口定义源代码和对应的 JML 规格都已在接口源代码文件中给出,各位同学需要准确理解 JML 规格,然后使用 Java 来实现相应的接口,并保证代码实现严格符合对应的 JML 规格。具体来说,各位同学需要新建三个类 MyPerson
、MyNetwork
和 MyGroup
(仅举例,具体类名可自行定义并配置),并实现相应的接口方法,每个方法的代码实现需要严格满足给出的 JML 规格定义。
继承自 Message
的接口有 RedEnvelopeMessage
、NoticeMessage
、EmojiMessage
,接口定义源代码和对应的 JML 规格都已在接口源代码文件中给出,同样需要各位同学准确理解 JML 规格,然后使用 Java 来实现相应的接口,并保证代码实现严格符合对应的 JML 规格。
抽象异常类已在官方包内给出,这一部分没有提供 JML 规格,各位同学需要仔细阅读指导书中关于异常类的详细描述,结合样例理解其行为,然后继承这些抽象类实现自己的异常类,使其 print()
方法能够正确输出指定的信息。
当然,还需要同学们在主类中通过调用官方包的 Runner
类,并载入自己实现的 Person
、Network
、Group
和各个消息类,来使得程序完整可运行,具体形式下文中有提示。
作业分析
经过了三次作业的洗礼,我意识到了这个单元的作业具有一个共同的特征:在一次作业中加入一个难度较大的指令,其他的指令基本上不存在算法难点。这次为我们带来困扰的是qlm
指令。
通俗来讲,qlm
指令就是在已有的关系图系统中,找出经过特定点的最小环。看到这个要求,我菊花一紧两眼一黑,一年前数据结构图论部分的不好回忆涌上心头。于是看到指导书中要我们掌握单源最短路径算法,第一个想到的就是迪杰斯特拉算法。不过对于如何将迪杰斯特拉算法应用到求最小环这个问题,我考虑了很久也没有得到合理的答案。
这时,评论区的XXX大佬(再次)拯救了我:
提供一种使用最短路径和删边法查找最短环的方法:
对于起点 i i i ,假如点 j j j 与 i i i 直接相连,则删掉此边后查询 i i i 和 j j j 之间是否仍然存在路径。若存在路径,则一个可能的最短环值就是 v i , j + D i j k s t r a i , j v_{i,j} + Dijkstra_{i, j} vi,j+Dijkstrai,j
重复此操作遍历所有与起点直接相连的点即可得到最短环值。
醍醐灌顶,直接开写:
import com.oocourse.spec3.main.Person;
import java.util.ArrayList;
import java.util.HashMap;
public class Dijkstra {
private final HashMap<Person, Integer> leastValue = new HashMap<>();
private final ArrayList<Person> passedList = new ArrayList<>();
private int nearest = 11451400;
private Person nearestPerson;
public Dijkstra() {}
public void findDijkstra(MyPerson person, HashMap<Integer, Person> people) {
ArrayList<Integer> peopleId = new ArrayList<>(people.keySet());
for (Integer integer : peopleId) {
MyPerson p1 = (MyPerson) people.get(integer);
if (person.isLinked(p1) && !person.equals(p1)) {
leastValue.put(p1, person.getValue().get(p1));
if (person.getValue().get(p1) < nearest) {
nearestPerson = p1;
nearest = person.getValue().get(p1);
}
} else {
leastValue.put(p1, 11451400);
if (nearestPerson == null) {
nearestPerson = p1;
}
}
}
passedList.add(person);
leastValue.put(person, 0);
do {
MyPerson p1 = (MyPerson) nearestPerson;
int tmpNearest = 11451400;
for (Integer integer : peopleId) {
MyPerson p2 = (MyPerson) people.get(integer);
if (p1.isLinked(p2) && !p1.equals(p2)
&& !passedList.contains(p2)) {
if (leastValue.get(p2) > nearest + p1.getValue().get(p2)) {
leastValue.put(p2, nearest + p1.getValue().get(p2));
}
if (leastValue.get(p2) < tmpNearest) {
nearestPerson = p2;
tmpNearest = leastValue.get(p2);
}
}
}
passedList.add(p1);
nearest = tmpNearest;
if (nearestPerson == p1) {
tmpNearest = 11451400;
for (Integer integer : peopleId) {
MyPerson tmpPerson = (MyPerson) people.get(integer);
if (tmpNearest > leastValue.get(tmpPerson)) {
nearestPerson = tmpPerson;
tmpNearest = leastValue.get(tmpPerson);
}
}
nearest = tmpNearest;
}
} while (passedList.size() != peopleId.size());
}
public HashMap<Person, Integer> getLeastValue() {
return this.leastValue;
}
}
利用HashMap<Integer, Integer>
存储人的ID和到源点的最短路径长度。需要注意的是这里传入的关系图应该是经过删边处理后的图,而由于删除的关系随人的变化而变化,因此在遍历查找最短路径的过程中需要每次都跑一遍,以此来更新每个包含目标的环值。
public int queryLeastMoments(int id) throws
PersonIdNotFoundException, PathNotFoundException {
if (!people.containsKey(id)) {
throw new MyPersonIdNotFoundException(id);
}
MyPerson person = (MyPerson) getPerson(id);
int tmpValue;
int leastMoment = 11451400;
ArrayList<Integer> personId = new ArrayList<>(people.keySet());
for (Integer integer : personId) {
MyPerson p1 = (MyPerson) getPerson(integer);
if (person.isLinked(p1) && !person.equals(p1)) {
tmpValue = person.getValue().get(p1);
person.getAcquaintance().remove(p1);
person.getValue().remove(p1);
p1.getAcquaintance().remove(person);
p1.getValue().remove(person);
Dijkstra dij = new Dijkstra();
dij.findDijkstra(person, people);
Bfs bfs = new Bfs();
if (bfs.bfs(person, p1)) {
if (dij.getLeastValue().get(p1) + tmpValue < leastMoment) {
leastMoment = dij.getLeastValue().get(p1) + tmpValue;
}
}
person.addAcquaintance(p1);
person.addValue(p1, tmpValue);
p1.addAcquaintance(person);
p1.addValue(person, tmpValue);
}
}
if (leastMoment < 11451400) {
return leastMoment;
}
throw new MyPathNotFoundException(id);
}
这样的写法没什么问题,但这样的写法没什么问题是不太可能的。没什么问题是指正确性,没什么问题不太可能指的是O(n)
套O(n2)
的运行复杂度可能满足不了15s的时间要求。
互测与强测
本次作业,我受到0次hack,并成功hack他人0次。强测得分 63.6364分。
房内一片祥和,一刀没出,有点难绷。
强测的主要错误点在于tle
了很多关于qlm
的指令,因为复杂度维护出现了很大的问题。只要稍微多一点qlm
指令,我就会t掉。本地测试中,我的程序在万量级稠密图的前提下,完成一条 qlm
的平均时长在0.2s-0.3s之间,这显然不符合要求。
BUG修复
如上述,在不必要的地方删去对bestAcquaintance
的动态维护,就可以大致满足测评需要的时间。
目前的dijkstra
算法也有问题。在上述代码的前提下,复杂度最差可能会出现O(n3)
的情况。听说使用堆优化和动态维护可以减少运行的时长但我摆了(
测试方法
关于黑箱测试和白箱测试
顾名思义,黑箱测试就是将程序看作一个整体测试,而白箱测试就是将程序具体语句展开进行测试,并在源代码层面上进行修改。
这两种测试方式在这个单元的测试中缺一不可,黑箱测试整体体现在利用评测机对程序的压缩文件与输入数据放在一起进行运行,测试出程序整体的正确性;而白箱测试是对于算法等具体代码进行的细致化测试,用于修改程序的错误或漏洞。
不太清楚具体的理解这两种测试方法是要从什么方面进行,但这两种测试方法对于我们的作业而言都是非常重要,必不可少的方法。前者是进行快速广度测试的必要手段,后者是修改程序的重要测试依据。
关于回归测试
我们的BUG修复实际上就是一种回归测试。我认为在大型工程中回归测试是很有必要的,因为接口和方法之间的关系可能会错综复杂,修改一个错误可能会导致别的方法受到牵连。不过在我们第三单元的作业中,我的错误基本都是出现在算法方面和细节处理方面的问题,并不涉及到很复杂的逻辑联系,因此我没有特地进行回归测试。不过在我们课程的BUG修复环节中,确实运用到了回归测试的思想,毕竟如果修复漏洞导致产生了新的漏洞,相当于白忙活(。
关于测试工具
很遗憾我没有使用课程组推荐的Junit工具测试,因为安装之后有点不知所措,不是很会用,所以放弃了,只好找人对拍。好在有很多大佬分享了自己的评测机,有人栽树,我也乘乘凉。(逃)
而OK测试并没有直接的评测机可以使用,于是便四处求索,寻找别人的数据,多方交流多方查询,尝试数百种不同的输入情况均不出现错误后,才放心自己的程序没有显性问题(实际上对于OK测试,自己的心里还是很虚的,毕竟没办法覆盖所有情况,数据的构造也相当困难)。
关于测试数据的构造
首先我通过编写程序,对每个指令进行单独测试,随后进行了随机指令综合的测试,最后对时间复杂度较高的单一指令进行了压力测试(如qci qcs qlm
等用到较为复杂的算法的指令)。
学习心得
关于JML
首先,我想对本单元的学习核心内容JML进行一番评价。JML规格的出发点是良好的,它规定了某些方法和类的标准格式,并且列出了前置条件、后置条件、副作用等等一系列限制,帮助我们程序编写者条理更加清晰地分析某个方法或类需要实现的目的,并且具有很强很强的逻辑严谨性,可谓事无巨细,某些很显然的条件也会作为ensures
条件出现在规格中。这好吗?这很好。对于多人协作的程序而言,JML规格无疑大大降低了对于方法或接口的理解偏差,有助于提高协作内容的准确性。但是JML规格一个问题,我相信经历了本单元三次作业的同学都会有或多或少同样的看法,那就是JML规格的可读性实在有待提高。对于某些逻辑稍微复杂的方法,看到JML规格写了那么一大坨,多少有点抵触的情绪在里面,缩进风格也并不读者友好,某些ensures
语句更是重量级,那个括号看的人眼睛花。这好吗?这不好。我估计这也是市场主流不再认可JML的主要原因,因为它的效率实在不算太好。第六次上机实验更是给我幼小的心灵带来了极大的创伤
所以JML就是一种没有什么意义的东西吗?我认为不是,JML规格的编写思路基本上也是我们在编写某个方法时需要考虑的逻辑。对于某个方法,我们当然也需要考虑这个方法的副作用、前后条件等等方面的问题。所以,如果你问我JML有没有实用价值,我认为是没有的;但如果你问我JML有没有什么值得学习的方面,那我会说JML在一定程度上培养了我的编程思想,它教会我在编写过程中应该从哪些角度考虑问题,梳理逻辑。
其次是对于面向对象编程的一些体会。虽然说这单元的作业没有什么架构上的问题,但是三次作业的迭代性非常强,有一个好的架构对于后续的作业是有事半功倍的效果的,这也是贯穿了面向对象整个课程的思想。
关于课程的建议
对于JML的学习可以保留,但可以将单元的顺序进行一定的调整,例如将本单元放在第一单元进行学习,同时将对数学逻辑和图论知识的要求适当降低。这样既可以使初来乍到的学生体会到面向对象的迭代性思想,又可以起到不错的学习效果。在第三单元教学JML相关的内容,作业的要求一定不会太过简单,因此作业中方法的JML规格编写起来也相当复杂。在三次作业中,均出现了助教将官方包内容进行修改的现象,这也恰好证实了我所说的观点。
因此,将JML单元提前,顺便降低任务难度、简化JML代码,可能可以获得更好的教学效果、实现教学目的。