BUAAOO_UnitThree

目录

一、JML语言

  • 理论基础
  • 工具链

二、SMT Solver部署与验证

三、JMLUnitNG

四、本单元架构设计

五、Bug及修复情况

六、心得体会

引言

  本单元作业主要聚焦于JML规格下的代码完成与测试,考察了我们对于JML规格的阅读和理解能力,同时还考察了一些基本的数据结构和算法知识,要求我们能够使用JUnit等途径对自己的代码进行测试。

一、JML语言

 理论基础

1.JML表达式

  JML表达式中主要由原子表达式、量化表达式、集合表达式和操作符。

  • 原子表达式
1.\old(expr):表示一个表达式expr在相应方法执行前的取值,该表达式涉及到评估expr中的对象是否发生变化。
如果是引用(如hashmap),对象没改变,但进行了插入或删除操作。v和odd(v)也有相同的取值。

2.\not_assigned(x,y,...):用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,
否则返回
false
用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值。 3.\not_modified(x,y,...):该表达式限制括号中的变量在方法执行期间的取值未发生变化。 4.\nonnullelements(container):表示container对象中存储的对象不会有null。 5.\type(type):返回类型type对应的类型(Class),如type(
boolean)为Boolean.TYPE。TYPE是JML采用的缩略表示,
等同于Java中的 java.lang.Class。 6.\typeof(expr):该表达式返回expr对应的准确类型。如\typeof(
false)为Boolean.TYPE。
  • 量化表达式
1.\forall:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

2.\exists:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

3.\sum:返回给定范围内的表达式的和。

4.\product:返回给定范围内的表达式的连乘结果。

5.\max:返回给定范围内的表达式的最大值。

6.\min:返回给定范围内的表达式的最小值。

7.\num_of:返回指定变量中满足相应条件的取值个数。可以写成(\num_of T x; R(x);P(x)),
其中T为变量x的类型,R(x)为x的取值范围;P(x)定义了x需要满足的约束条件。从逻辑上来看,该表达式也等价于
(\sum T x;R(x)&&P(x);1)。
  •  集合表达式
集合构造表达式的一般形式为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合中x的范围,
通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。
new JMLObjectSet {Integer i | s.contains(i) && 0 < i.intValue() } 表示构造一个JMLObjectSet对象,其中包含的元素类型为Integer,该集合中的所有元素都在容器集合s中出现
(注:该容器集合指Java程序中构建的容器,比如ArrayList),且整数值大于0。
  •  操作符
1.E1<:E2子类型操作符:如果类型E1是类型E2的子类型(sub type)或相同类型,则该表达式的结果为真,否则为假。
任意一个类X,都必然满足X.TYPE<:Object.TYPE。 2.b_expr1<==>b_expr2或b_expr1<=!=>b_expr2等价关系操作符:其中b_expr1和b_expr2都是布尔表达式。 3.b_expr1==>b_expr2或b_expr1<==b_expr2推理操作符:相当于离散的->,只有(1,0)是false。 4.\nothing或\everthing变量引用操作符:表示当前作用域访问的所有变量。前者空集,后者全集。
变量引用操作符经常在assignable句子中使用,如 assignable \nothing表示当前作用域下每个变量都不可以
在方法执行过程中被赋值。

2.方法规格

定义了前置条件、后置条件和副作用。

  • 前置条件
对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。
requires P;其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。
多个分开的requires是并列关系都要满足,或关系用requires P1
||P2;
  • 后置条件
对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。其中ensures是JML关键词,表达的意思是
“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。并列关系和或关系与前置相同。 ensures P;
  • 副作用
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。

从方法规格的角度,必须要明确给出副作用范围。
JML提供了副作用约束子句,使用关键词assignable(表示可赋值)或者modifiable(可修改)。虽然二者有细微的差异,在大部分情况下,二者可交换使用。
副作用约束子句共有两种形态,
1. 用JML关键词来概括,不指明具体的变量;
2. 指明具体的变量列表。

3.类型规格

  类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。
  从面向对象角度来看,类或接口包含数据成员和方法成员的声明及或实现。不失一般性,一个类型的成员要么是静态成员(static member),要么是实例成员(instance member)。
  一个类的静态方法不可以访问这个类的非静态成员变量(即实例变量)。静态成员可以直接通过类型来引用,而实例成员只能通过类型的实例化对象来引用。因此,在设计和表示类型规格时需要加以区分。

  •   invariant P  

  不变式(invariant)是要求在所有可见状态下都必须满足的特性,其中invariant为关键词, P为谓词。对于类型规格而言,可见状态(visible state)是一个特别重要的概念。

4.其他

  •  (/*@ pure @ */) 

  指不会对对象的状态进行任何改变,也不需要提供输入参数,这样的方法无需描述前置条件,也不会有任何副作用,且执行一定会正常结束。

  有些前置条件可以引用pure方法的返回结果;

  •  forall和exists 

  前置条件或后置条件需要对不止一个变量进行约束,往往是需要对一个容器中的所有元素进行约束。

  •  public normal_behavior和public exception_behavior 

  为了有效地区分方法的正常功能行为和异常行为。如果一个方法没有异常处理行为,不必使用这两个关键词。
  public,指相应的规格在所在包范围内的所有其他规格处都可见。
  also ,这里指除了正常功能规格外,还有一个异常功能规格。

  同一个方法的正常功能前置条件和异常功能前置条件一定不重叠。

  •  signals (***Exception e) b_expr 

  强调满足某个条件抛出相应异常。
  当 b_expr 为 true 时,方法会抛出括号中给出的相应异常e。注意一定要在方法声明中明确指出(使用Java的 throws 表达式),且必须确保signals子句中给出的异常类型一定等同于方法声明中给出的异常类型,或者是后者的子类型。
   signals_only (***Exception e) 
  强调满足前置条件抛出相应异常。

JML工具链

  • openJML 检查JML语法
  • JUnit 单元测试
  • JMLUnitNG 根据规格生成自动测试

二、SMT Solver部署和验证

OpenJML验证

举个例子,要检查整个src下的规格,得到结果如下:

./src/Group.java:56: error: incomparable types: int and INT#1
    /*@ ensures \result == (people.length == 0? 0 :
                        ^
  where INT#1 is an intersection type:
    INT#1 extends Number,Comparable
./src/Group.java:61: error: incomparable types: int and INT#1
    /*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length;
                        ^
  where INT#1 is an intersection type:
    INT#1 extends Number,Comparable
2 errors

OpenJML总体来说在实际使用中用处不大,而且支持的语法也不多,且总是会报一些神秘错误。

三、JMLUnitNG

用它来测试Group接口之后,得到的测试结果如下:

[TestNG] Running:
  Command line suite

Passed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <<MyGroup@4fccd51b>>.addPerson(null)
Failed: <<MyGroup@44e81672>>.addPerson(null)
Failed: <<MyGroup@4ca8195f>>.addPerson(null)
Failed: <<MyGroup@65e579dc>>.delPerson(null)
Failed: <<MyGroup@61baa894>>.delPerson(null)
Failed: <<MyGroup@b065c63>>.delPerson(null)
Passed: <<MyGroup@768debd>>.equals(null)
Passed: <<MyGroup@7d4792a8>>.equals(null)
Passed: <<MyGroup@490d6c15>>.equals(null)
Passed: <<MyGroup@66133adc>>.equals(java.lang.Object@24273305)
Passed: <<MyGroup@42f30e01>>.equals(java.lang.Object@7bfcd12c)
Passed: <<MyGroup@5b1d2887>>.equals(java.lang.Object@46f5f779)
Passed: <<MyGroup@1c2c22f3>>.getAgeMean()
Passed: <<MyGroup@18e8568>>.getAgeMean()
Passed: <<MyGroup@5a42bbf4>>.getAgeMean()
Passed: <<MyGroup@270421f5>>.getAgeVar()
Passed: <<MyGroup@52d455b8>>.getAgeVar()
Passed: <<MyGroup@4f4a7090>>.getAgeVar()
Passed: <<MyGroup@18ef96>>.getConflictSum()
Passed: <<MyGroup@6956de9>>.getConflictSum()
Passed: <<MyGroup@769c9116>>.getConflictSum()
Passed: <<MyGroup@6aceb1a5>>.getId()
Passed: <<MyGroup@2d6d8735>>.getId()
Passed: <<MyGroup@ba4d54>>.getId()
Passed: <<MyGroup@de0a01f>>.getRelationSum()
Passed: <<MyGroup@4c75cab9>>.getRelationSum()
Passed: <<MyGroup@1ef7fe8e>>.getRelationSum()
Passed: <<MyGroup@2471cca7>>.getValueSum()
Passed: <<MyGroup@5fe5c6f>>.getValueSum()
Passed: <<MyGroup@6979e8c>>.getValueSum()
Failed: <<MyGroup@763d9750>>.hasPerson(null)
Failed: <<MyGroup@5c0369c4>>.hasPerson(null)
Failed: <<MyGroup@2be94b0f>>.hasPerson(null)

===============================================
Command line suite
Total tests run: 40, Failures: 11, Skips: 0
===============================================

可以发现,这个工具主要针对的是一些边界数据,在实际使用中的用处不大。

四、本单元架构设计

第一次作业

  • 在这次作业中,我采用的是并查集的方法来解决isCircle的判断问题,判断两人是否联通,只需判断两人的祖先是否相同,我在addrelation中进行了判断两人是否有相同祖先,若不是,则默认将第二个合并到第一个,单个点的祖先是他自己。
  • 第一次作业对于容器的访存性能要求不高,使用ArrayList可以过关。
  • 第一次作业的难度不大,难点主要在找到合适的算法解决isCircle的判断,我的整个设计架构图如下(无官方包):

  

第二次作业

  • 第二次作业中,新增了MyGroup这个类,将person划分到了组别中,MyGroup类里主要是一些针对age、value的计算过程,在这次作业中,尤其要注意数据的存放途径,同时考虑到计算的方法比较多,应当想到要使用缓存数据的方法,而不是求到某个值,就从头进行遍历计算,举个简单的例子如下,我们在求几种属性的总和时,可以在addperson方法里进行缓存:
    @Override
    public void addPerson(Person person) {
        relationSum++;
        ageSum += person.getAge();
        conflict = conflict.xor(person.getCharacter());
        for (Person p : people.values()) {
            if (person.isLinked(p)) {
                relationSum += 2;
                valueSum += 2 * person.queryValue(p);
            }
        }

        people.put(person.getId(),person);

    }

  不过要考虑到addrelation指令引起的变化,因此需要添加方法并正确调用:

public void addrelation(int value) {
        relationSum += 2;
        valueSum += 2 * value;
    }
  • 第二次作业应当使用性能更好的容器hashmap,当算法不够优化时,使用arraylist有极大的可能会TLE。
  • 我的第二次作业架构图如下(无官方包):

  

第三次作业

  • 第三次作业中,增加了几个难度比较大的方法,如判断两点是否强连通、求出两点最短路径、求连通块数等,我们在实现过程中,除了要根据JML规格理解方法的目的,还要注意与离散数学中的知识相联系,从理论上简化方法的实现步骤。
  • 同时,由于qsl明确指出不超过20条,因此我们不用太过注重算法的最优化,而应当保证结果的正确性,在实现过程中,我先是尝试2次dfs但最终由于方法不正确而放弃,想实现tarjan算法又被难度和实现起来容易产生的错误太多劝退,最终采用的是相对暴力的一个算法,即去掉首尾路径上的任一点后再判断首尾是否仍然联通,最终得到了正确结果;在此,提供一个简单的暴力方法,同时要注意到有几个特殊情况需要考虑;
  1. 首先,判断两个点是否iscircle,不是则直接返回false;
  2. 若两点直接link了,则删除这条边再判断是否iscircle,不是则直接返回false;
    public boolean isStrongLinked(int id1,int id2) throws PersonIdNotFoundException {
        Set<Integer> tmpset = getPeople();
        for (Integer i : tmpset) {
            visited = new HashSet<>();
            if (i != id1 && i != id2 && isCircle(id1,i)) {
                visited = new HashSet<>();
                visited.add(i);
                if (!isCircle(id1,id2)) {
                    visited = new HashSet<>();
                    return false;
                }
                visited.remove(i);
            }
        }
        visited = new HashSet<>();
        return true;
    }
  • 求最短路径我使用到的是堆优化迪杰斯特拉方法,不过由于实现过程中处处更新,而没有存储已得结果,导致T了三个点,该算法还存在优化的空间;
  • 求连通块数如果能用数学的思维去思考就会容易很多,不需要每次都遍历一遍,只需在adperson时+1,addrelation时若!iscircle则-1即可实现该值的缓存与维护。
  • 我的第三次作业整体设计如图:

  

五、Bug及修复情况

  • 第一次作业中由于自己使用了数组存visit,导致RE,想来是一个非常制杖的错误,同时也反映了我在做第一次作业时过于浮躁,没有做足测试,也没有想清楚数据的范围,debug时先是修改了数据容器,又换了并查集写法,最终通过;
  • 第二次作业中由于算法太弱,没有用缓存数据的方法,同时使用了效率较低的arraylist,导致双重循环TLE,debug时换用了数据缓存维护,通过;
  • 第三次作业TLE了三个点,互测没有被hack,需要优化一下qmp算法;

六、心得体会

  • 这一单元是我失误最多的一个单元,可能在心理上也有些过于放松了,做第一次作业的时候(就这)没有想到检查什么的,第二次作业也没太考虑时间限制,甚至看到有同学讨论维护数据这种思路也没有去实践,因此出问题也确实可以说是自找的罪有应得,对我来说是很重要的一个教训;
  • 本单元由于没有涉及到手写JML规格,只要求我们能读懂规格代码,因此对于规格的手写还不是很熟悉;
  • 这一单元的后两次作业都拜托了同学帮我一起对拍,也帮助我纠正了很多自己第一次写时没有考虑到或测试出的问题,在此表示感谢。
  • 我在学习这个单元之后的最大感触是,规格中给出的仅仅是条件要求,而不能作为我们写代码的一个指导,如果我们在实现的过程中只模仿规格一步一步写是极有可能出大问题的,比如第一次作业中规格的描述都是数组,但我们在实现的时候就不能用数组来写,因此,我们在读懂规格之后一定要加上自己的理解和思考,才能更好地完成目标。

猜你喜欢

转载自www.cnblogs.com/HildaChen-/p/12934578.html
今日推荐