面向对象第三单元总结

第三单元总结

JML语言的理论与应用

(一)理论基础

The Java Modeling Language (JML) is a behavioral interface specification language that can be used to specify the behavior of Java modules. It combines the design by contract approach of Eiffel and the model-based specification approach of the Larch family of interface specification languages, with some elements of the refinement calculus, using Hoare style pre and postconditions and invariants, that follows the design by contract paradigm. Specifications are written as Java annotation comments to the source files, which hence can be compiled with any Java compiler.

由上可知,JML是一种在Java代码注释中规定代码实现者可依赖的初始条件、如何提供满足要求的结果以及其对于变量的影响(副作用)的结构性语言,具体语法内容可参看指导书。

通过两则实例可以更好地理解:

/*@ 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要求结果为对某数求和(sum),遍历整个people数组,如果某一点与前面所有(forall)点都不满足isCircle则加一,由isCircle定义可知此方法为求连通块数目,可以使用并查集。

/*@ 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;

上面为某方法的前置条件部分,可以看到,其要求groups中存在id2people中存在id1,且id2里不能有id1元素,与之相反的条件可以被定义在了exceptional_behavior中,而最后一个id2里元素不能超过1111的限制,其他情况却不能满足,如果在代码中不体现,仅认为这是前置条件,是方法调用者需满足的话,会造成许多测试点错误。所以前置条件也应当完整的反应在代码里。

(二)应用工具链

以下两种工具是常被使用(并不)的:

OpenJML

使用SMT Solver来对检查程序实现是否满足所设计的规格。

下载地址

JMLUnitNG

根据规格自动化生成测试样例,进行单元测试。

官网地址

SMT Solver部署

SMT(Satisfiability Modulo Theories)问题是在特定理论下判定一阶逻辑公式可满足性问题,其判定算法被称为SMT求解器。

OpenJML

安装方法可以查看这篇博客。

通过一个例子可以理解其作用:

public class test {
    /*@ public normal_behaviour
      @ ensures \result == in*in*in;
    */
    public int power3_A(int in) {
        int i, out_a;
        out_a = in;
        for (i = 0; i < 2; i++)
            out_a = out_a * in;
        return out_a;
    }

    /*@ public normal_behaviour
      @ ensures \result == in*in*in
    */
    public int power3_B(int in) {
        int out_b;
        out_b = (in * in) * in;
        return out_b;
    }
}

执行openjml test.java命令得到以下输出:

警告: An internal JML error occurred, possibly recoverable.  Please report the bug with as much informa
tion as you can.
  Reason: Last resort loading of extensions
警告: Could not locate the internal runtime classes - you may need to add the jmlruntime.jar explicitly
 on the command-line using -classpath.
警告: An internal JML error occurred, possibly recoverable.  Please report the bug with as much information as you can.
  Reason: Last resort loading of extensions
test.java:15: 错误: The expression is invalid or not terminated by a semicolon
    */
    ^
test.java:13: 警告: There is no point to a specification case having more visibility than its method
    /*@ public normal_behaviour
        ^
1 个错误
3 个警告
请按任意键继续. . .

可以看出,SMT的作用是检查JML书写格式、逻辑是否正确,上例的错误在于第二个方法第二行JML最后没有分号。

JMLUnitNG部署

*此应用的安装与使用参考

对于Group类进行测试报错过多。遂使用如下代码(test类)测试:

public class test {
    /*@ public normal_behaviour
      @ ensures \result == in*in*in;
    */
    public static int power3_A(int in) {
        int i, out_a;
        out_a = in;
        for (i = 0; i < 2; i++)
            out_a = out_a * in;
        return out_a;
    }

    /*@ public normal_behaviour
      @ ensures \result == in*in*in;
    */
    public static int power3_B(int in) {
        int out_b;
        out_b = (in * in) * in;
        return out_b;
    }

    /*@ public normal_behaviour
      @ requires in >= 0;
      @ ensures in == \result*\result;
      @ also
      @ public exceptional_behavior
      @ requires in < 0;
      @ signals_only RuntimeException;
    */
    public static double extraction(double in) {
        if (in > 0) {
            return Math.sqrt(in);
        } else {
            throw new RuntimeException();
        }
    }

    public static void main(String args[]) {
        System.out.println(power3_A(-2));
        System.out.println(power3_B(-2));
        System.out.println(power3_A(2));
        System.out.println(power3_B(2));
        System.out.println(extraction(4));
        System.out.println(extraction(-4));
    }
}

依次使用如下命令进行自动化测试:

java -jar jmlunitng.jar test.java
javac -cp jmlunitng.jar *.java
java -jar openjml.jar -rac test.java
java -cp jmlunitng.jar test_JML_Test

前两部分别生成.java文件和.class文件:

面向对象第三单元总结_第1张图片

第三步得到相应的JML_TEST程序文件,然后第四步运行即可看到自动生成的样例以及给出的结果,根据测试样例可以看出,JMLUnitNG只对边界数据进行了测试:

面向对象第三单元总结_第2张图片

已经检测出了上述代码中比较明显的越界错误。

作业代码架构设计

Homework 9

第一次作业十分简单,我直接强测0分。

是的,我总共花了一个小时便照着JML写完了代码,提交上去发现没问题就睡大觉去了。直接使用ArrayList存储people,方法实现的逻辑也与JML完全一致。

然而交上去强测是全错!可见我对于JML契约式编程的理解是十分肤浅的,JML只规定了代码的实现条件与实现结果,并未限制方法的实现方式,为了追求与JML逻辑一致而编写执行效率低下的代码是错误的。课程组提供的测试点狠狠地打了我的脸。其中最为严重的错误便是isCircle方法,我使用了递归调用,也即如果与终点相连则返回真,否则对于其相连的点调用此方法。这看似编码十分方便,还能够应付一些情况,但是复杂度远高于\(O(n^2)\),而且还错了!由于没有存放已访问的队列,所以可能死循环,直接爆掉实在正常,最后换成BFS顺利完成。

Homework 10

第二次作业依旧十分简单,我直接强测0分。

是的,我故技重施,重蹈覆辙,三番五次地爆零。这次还是性能,我当时仍旧没有吸取错误经验,还在按着JML的双重循环傻乎乎地写。助教没有容忍懒人,出现了循环的地方基本上都被卡掉,所以必须要动脑筋。如何才能优化性能呢?这就引出了两种数据结构ArrayListHashMap的性能差距。简单来讲就是:

//ArrayList各方法复杂度
add(E e);
//添加元素到末尾,平均时间复杂度为O(1)。
add(int index, E element);
//添加元素到指定位置,平均时间复杂度为O(n)。
get(int index);
//获取指定索引位置的元素,时间复杂度为O(1)。
remove(int index);
//删除指定索引位置的元素,时间复杂度为O(n)。
remove(Object o);
//删除指定元素值的元素,时间复杂度为O(n)。
contains(Object o);
//遍历,时间复杂度为O(n)。
//HashMap各方法复杂度
put(K key,V value);//O(1)
get(K key);//O(1)~ O(n)
containsKey(K key);//O(1)~ O(n)

可以看出,对于MyNetwork中需要遍历数组选取某个元素的方法直接使用HashMapget() containsKey()即可十分方便快捷地得到结果 ;对于需要双重循环的getRelationSum()getValueSum()getConflictSum()则可以考虑每次添加人或关系时更新,这样查询只是一个O(1)的操作。

HashMap还有扩容机制什么的有优化的余地,在此不赘述。

Homework 11

第三次作业巨难(并不),我果然进入了互测!

实际上,到了第三次作业我已经忘记前面有些方法的含义了,比如这个isCircle,我不知为甚,理解出需是基本路径的要求,这就导致这道题变得极为复杂,直到我看了讨论区大佬的这篇博客发现我的复杂度怎么和别人不一样?!于是悔改之。

面向对象第三单元总结_第3张图片

如果没有要求基本路径,那么queryBlockSum只需要使用并查集,查询社交网络中连通子图的个数即可。而对于queryMinPath方法,则使用了Dijkstra算法,并使用优先队列来优化性能。而queryStrongLinked方法则使用遍历割点的方式,根据门杰定理,可以依次将路径上的点删去,再用bfs广度优先搜索寻找两点间的一条路径,如果两点依然连通,说明两点是点双连通的。这其中需要特别注意的是如果终点和起点直接相连则需要特判,因为就算不是强相连遍历删除所有其他点,这两点仍是相连的(不过先用dfs深度优先遍历找到两点间的一条路径,然后依次将路径上的点删去是可以不用特判的)。

Bug修复与测试

Homework 9

对于isCircle方法,改变了此前采用递归调用的方法。采用双队列的方式,将所有可达的id放入队列循环访问其acquaintance,添加到可达队列中,并且被访问过的id专门组成一个队列,不会再次被访问。

同时修改了addRelation方法中的一处复制代码导致的笔误(只添加了单边的关系)。

修改完善了isLinked方法,将等于自己的情况添加上了。

Homework 10

CPU超时问题。最初以为只是被卡掉了双重循环,将MyGroup的数据改正为HashMap后,将计算RelationValue总和分摊到了每次添加人或添加关系里。提交后发现超时依旧。
于是将所有的循环都去掉,把数据结构从ArrayList全部换成HashMap之后便满足了时间的条件。事实证明,不能以为满足正确性就能过测试。

还有一点bug是addtoGroup方法里面,没有判断Group的人数有无小于1111。修改后通过测试。

Homework 11

本次作业的bug仍然以CPU超时为主,主要在于对于并查集的处理并不标准,不是按照树形结构构造,而是按照以HashSet为单位的集合,将其组织成ArrayList,在将关系(两点所在集合)合并是时需要遍历整个所有并查集组成的ArrayList,这样在一些测试点上,如果先添加所有的人,再添加关系,会使得其复杂度非常高。

所以使用了最常规的方法,并且其中运用了内部类来节约时间。其次,我之前使用了initialCapacity这个参数,在运行当中我发现,使用这个参数不恰当反而会降低性能,故而删去。

测试

使用python构造数据集,因为有明确的命令条数和格式要求,所以比较方便实现,注意需要用数组存住生成的person来构造环。需要使用他人的代码进行对拍,同时使用脚本比较文件(fc命令)。

心得体会

OO课,你呀,总是能给我出点新花样,让我痛不欲生。我感觉我这门课在疫情的艺术加工下已经完全废掉了。各种碰壁,各种踩坑,各种爆零。哎,不管是简单的还是难的我都能如愿以不偿的不得分。只能说重视不够,不能怨天尤人。这一单元对于JML的理解还是比较深入的,从单纯的照着写,变成三思而后写,思考如何才能在保证正确性的情况下尽量提升性能,这其中会反复与同学交流讨论参看各种博客、资料,从知识的广度和深度上都得到了比较长足的进步。谢谢茄子。

面向对象第三单元总结_第4张图片

你可能感兴趣的:(面向对象第三单元总结)