Unit3-窝窝社交圈

全文共4909字,推荐阅读时间15~20分钟。

文章共分五个部分:

  • JML总结

  • 作业分析

  • 评测相关

  • 重构策略

  • 课程体验感受

JML总结

定义

  • JML是一种对Java程序进行规格化设计的表示语言
  • JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL)

注释结构

  • JML和Javadoc的注释方式相同
    • 每行都以@起头
    • 行注释://@ annotation
    • 块注释:/*@ annotation @*/
//@ public model non_null int[] elements

/*@ public normal_behavior
  @ ensures \result == elements.length;
  @*/
  • model:表示elements数组是规格层次的描述,不是JML语句所在类的组成部分,也不要求该类声明这一变量。
  • non_nullelemants数组对象的引用不能是null
  • 作用域:和Java相同,JML也有自己的作用域,此处是public.
  • 规格中每个子句需要以分号结尾
  • 接口中声明规格变量时,要求明确变量的类别。
    • 静态
    • 实例
// inside an interface
//@ public static model non_null int[] elements
//@ public instance model non_null int[] elements 

表达式

  • JML断言中,不可以使用带有赋值语义的操作符。例如,++,--,+=等。

原子表达式

  • \result:表示一个非void的方法的返回值

  • \old(expr):表达式expr在执行相应方法前的取值

    • expr是引用时,表示的是指向的对象(地址)有没有发生变化,而不是对象本身有没有发生变化。因此,当对象发生变化时,expr只要依然指向它,就没有发生变化。
    • map.size() = \old(map).size() != \old(map.size())
  • \not_assigned(x,y,...):括号中变量如果在执行过程中赋值则返回true,否则为false.

  • \not_modified(x,y,...):类似not_assigned(),追踪变量值在执行过程中是否发生变化

    相比\not_assigned(x,y,...)使用较多,通常用在前置条件和后置条件中,断言变量在进入函数前后是否相等。

  • \nonnullelements(container):断言container对象中存储的对象没有null

    container != null && (\forall int i; 0<=i && i < container.length; container[i]!=null)
    
  • \type(type):返回基本类型type对应的引用类型,例如type(boolean)的值为Boolean.TYPE.

    TYPE等价于java.lang.Class

  • \typeof(expr):返回表达式expr对应的准确类型,例如\typeof(false)的值为Boolean.TYPE.

量化表达式

  • \forall:全称量词修饰的逻辑表达式,表示给定范围内的元素如果都满足相应需求则为真。
    • (\forall int i,j; 0<=i && i < j && j < 10; a[i] < a[j]),当a是严格单增的数组时该表达式为真。
  • \exists:存在量词修饰的逻辑表达式,表示如果存在给定范围内的元素满足相应需求则为真。
    • (\exists int i; 0 <= i && i < 10; a[i] < 0),当数组中有小于0的元素时表达式为真。
  • \sum:返回给定范围内指定的求和表达式的
    • (\sum int i; 0 <= i && i < 5; i)\(0+1+2+3+4\)
    • (\sum int i; 0 <= i && i < 5; i * i)\(0^2+1^2+2^2+3^2+4^2\)
  • \product:返回给定范围内指定的求积表达式的
  • \max:返回给定范围内指定表达式的最大值
    • (\max int i; 0 <= i && i < 5; i)
  • \min:返回给定范围内指定表达式的最小值
  • \num_of:返回给定范围内满足相应条件的表达式个数
    • (\num_of int x; 0 < x && x <=20; x % 2 ==0)

集合表达式

JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。

  • new ST {T x | R(x) && P(x)},其中R(x)对应集合中x的范围,P(x)对应x取值的约束。

    • new JMLObjectSet {Integer i | s.contains(i) && 0 < i.intValue() }

    s指java程序中构造的容器,如ArrayList.

操作符

\(JML操作符=Java操作符+子类型关系+等价关系+推理+变量引用\)

  • 子类型关系操作符:E1<:E2,当E1是E2的子类型时,表达式结果为真。(包括两者是相同类型)

  • 等价关系操作符:b_expr1<==>b_expr2b_expr1<=!=>b_expr2,其中表达式均为布尔表达式

    <==><=!=>的优先级低于==!=

  • 推理操作符:b_expr1==>b_expr2b_expr1<==b_expr2,和离散中的谓词表达式的真值判断相同。

  • 变量引用操作符:JML规格语句可以直接引用Java代码或者JML规格中定义的变量,使用\nothing,\everything来概括作用域,常出现在assignable句子中。

    • assignable \nothing表示当前作用域下每个变量可以在方法执行过程中被赋值。

模型域(model)

理解为规格中的成员变量

//@ public model instance JMLObject elemQueue;

创建了一个公开的、类型为JMLObject的、名叫elemQueue模型实例

  • instance:虽然这个模型是在接口中被定义出来的,但是每个实现接口的类都有各自的模型域。
  • 声明的模型域和Java代码无关,不可以在Java代码中被引用。
  • model为Java代码提供了一个建议,如果Java代码中实现了相应的变量,那么就可以使用JML进行检查,但是如果没有实现,则只是提供一个建议

模型域的取值(represents)

用于沟通JML和Java

//@ private represents jmlHeap <- javaHeap;

实现了jmlHeap和javaHeap的同步,每次永不需要手动操作。

方法规格

基本概念

  • 前置条件:对方法输入参数的限制
  • 后置条件:对方法执行结果的限制
  • 副作用:方法在执行过程中对输入对象或者this对象进行了修改(赋值等操作)

前置条件(pre-condition)

  • 通过requires子句表示
    • requires P;,其中P是逻辑表达式(可以包含逻辑连接词如&&,||,!

后置条件(post-condition)

  • 通过ensures子句表示
    • ensures P;,其中P是逻辑表达式

副作用(side-effects)

  • 约束副作用范围:assignable,modifiable(没有\
    • assignable elements,max,min,多个变量时使用,隔开。
  • JML不允许在约束语句中指定规格中声明的变量数据
  • 二者可以替换,但是通常使用assignable.
  • 纯粹访问:pure

    public /*@ pure @*/ int getCredits();
    

    方法规格描述可以省略\assignable

类型规格

对Java程序中定义的数据类型设计的限制规则,通常是对类、接口设计的约束规则。

不变式限制(invariant)

不变式是要求在所有可见状态下都要满足的特性。

  • invariant Pinvariant是关键词,P是谓词。

  • 可见状态(visible state):凡是会修改成员变量(静态+实例)的方法的执行期间,对象都处于不可见状态。

    这些方法在执行时,对象状态不稳定,因此不可见。

public class Path {
    private /*@ spec_public @*/ ArrayList seq_nodes;
    private /*@ spec_public @*/ Integer start_node;
    private /*@ spec_public @*/ Integer end_node;
    /*@ invariant seq_nodes !=null &&
      @ seq_nodes[0] == start_node &&
      @ seq_nodes[seq_nodes.length - 1] == end_node &&
      @ seq_nodes.length >= 2;
      @*/
}
  • spec_public表示private属性的成员变量在规格中对调用者可见,在Java程序中的可见性不受影响。
  • invariant语句最后以;结尾
  • 不变式中可以直接引用pure形态的方法

  • 静态不变式(static invariant):只约束静态成员变量

  • 实例不变式(instance invariant):约束静态+非静态成员变量

  • 实例

    • 前置条件调用pure方法

      /*@ requires c >= 0;
        @ ensures getCredits() == \old(getCredits()) + c;
        @*/
      public void addCredits(int c);
      
    • 使用量化表达式进行限制

      /*@ ensures !contains(elem);
        @ ensures (\forall int e; e != elem; contains(e) <==> \old(contains(e)));
        @ ensures \old(contains(elem)) ==> size == \old(size) - 1;
        @ ensures !\old(contains(elem)) ==> size == \old(size);
        @*/
      public void remove(int elem);
      
    • signals子句&signals_only子句

      public abstract class Student {
          // public model non_null int[] credits;
          /*@ public normal_behavior
            @ requires z >= 0 && z <= 100;
            @ assignable \nothing;
            @ ensures \result == credits.length;
            
            @ also
            @ public exceptional_behavior
            @ requires z < 0;
            @ assignable \nothing;
            @ signals_only IllegalArgumentException;
            
            @ also
            @ public exceptional_behavior
            @ requires z > 100;
            @ assignable \nothing;
            @ signals (OverFlowException e) true;
            @*/
          public abstract int recordCredit(int z) throws IllegalArgumentException,OverFlowException;
      }
      
      • normal_behavior/exceptional_behavior:后面不用带;

      • also使用情景:

        • 子类覆写父类方法后,进行规格补充时。
        • 分隔正常功能和异常功能

        also各个块的前置条件不能有重合,因为各部分不是串行的。

      • signals (Exception e) b_expr:当设置b_expr为true时,方法需要抛出相应的异常。

        在异常功能中,ensures语句一样可以使用。

      • signals_only Exception:不关心什么条件下会抛出,只要方法抛出即可。

状态变化约束(constraint)

状态变化约束是在限制状态变化的方式,在前序可见状态当前可见状态中生效。

public class ServiceCounter {
    private /*@spec_public@*/ long counter;
    //@ invariant counter >= 0;
    //@ constraint counter == \old(counter) + 1;
}

constraint限制可以在每个改变counter的方法的后置条件中实现,但是相比直接在变量本身添加限制会更繁琐。

  • 静态状态约束(static constraint)
  • 实例状态约束(instance constraint)

方法规格&类型规格

  • 当一个类是不变类时,构造方法的后置条件和成员变量的不定式选一个实现即可。

JML FAQ

  • JML只对纯方法支持断言确认

  • 当方法修改引用的具体域时,需要指明是哪个域。

    /*@ assignable obj.x
      @*/
    public void foo(Bar obj){
        // TODO
    }
    
  • JML==equals的区别

    两者的区别和Java中的区别相同

  • exceptional_behavior情况下,可以定义方法的前置条件、副作用和异常抛出,但不能定义方法的后置条件。

    使用signals时,可以不需要定义exceptional_behavior的前置条件。

  • JML中是否可以引用Java中的变量

    • JML无论何时都可以调用Java中的pure方法,但是并不是随时都能够引用变量。
    • 如果Java中的变量在JML对应方法的参数列表方法体中出现,那么就可以引用。
    • /*@ spec_public @*/的作用就是让不满足第二条的变量能够被调用者JML引用。
    • 如果第三条也不满足,即JML对应的方法以及其调用的方法中都没有出现需要的变量,则不能够引用相应的变量。

    引用变量则可以使用其方法

JML工具链

SMT solver

Z3 Theorem Prover is a cross-platform satisfiability modulo theories (SMT) solver by Microsoft.

​ --Wikipedia

因为openjml开发较早,所以导致很多我们现在使用的JML语法无法被识别,因此实现了基本的四则运算进行验证。

方法如下:

public class Test {

	// @ensures \result == a + b;
	public static int add(int a, int b) {
		return a + b;
	}

	// @ensures \result == a - b;
	public static int sub(int a, int b) {
		return a - b;
	}

	// @ensures \result == a * b;
	public static int mult(int a, int b) {
		return a * b;
	}

	// @ensures \result == a / b;
	public static int div(int a, int b) {
		return a / b;
	}
}

SMT solver反馈结果如下:

java -jar .\openjml.jar -esc -prover .\z3-4.7.1.exe -exec .\z3-4.7.1.exe .\Test.java

错误: An error while executing a proof script for add: (error "Error writing to solver: (set-logic ALL) java.io.IOException: 管道正在被关闭。")
错误: An error while executing a proof script for div: (error "Error writing to solver: (set-option :AUTO_CONFIG false) java.io.IOException: 管道正在被关闭。")
错误: An error while executing a proof script for mult: (error "Error writing to solver: (set-option :smt.MBQI false) java.io.IOException: 管道正在被关闭。")
错误: An error while executing a proof script for sub: (error "Error writing to solver: (declare-sort REF 0) java.io.IOException: 管道正在被关闭。")
4 个错误

JMLUnitNG部署与测试

由于JMLUnitNG支持的JML语法较少,与现在我们使用的大部分语法并不兼容,因此将Group中的部分方法及其规格进行简化后进行了自动生成数据并测试的试验。

依次键入以下命令:

java -jar jmlunitng-1_4.jar Group.java
javac -cp jmlunitng-1_4.jar *.java
openjml -rac Group.java
java -cp jmlunitng-1_4.jar Group_JML_Test

add方法得到如下反馈结果

[TestNG] Running:
 Command line suite
Passed: racEnabled()
Passed: constructor Group()
Failed: static add(-2147483648, -2147483648)
Passed: static add(0, -2147483648)
Passed: static add(2147483647, -2147483648)
Passed: static add(-2147483648, 0)
Passed: static add(0, 0)
Passed: static add(2147483647, 0)
Passed: static add(-2147483648, 2147483647)
Passed: static add(0, 2147483647)
Failed: static add(2147483647, 2147483647)
===============================================
Command line suite
Total tests run: 11, Failures: 2, Skips: 0
===============================================

可以看到自动生成的数据均为边界数据,对程序的极限条件检查还是很到位的。

作业分析

Unit3要求我们模拟现实生活中的社交网络,迭代主要是增加不同的功能。本单元的作业分析有独特的地方——因为三次作业都采用了一致的架构,因此以第三次作业的UML图为例即可了解整个单元迭代开发的过程。

Unit3-窝窝社交圈_第1张图片

从UML图可以很直接地看出来整个系统的模拟思路:通过分包策略,让网络(graph)、人(vertex)、算法(algo)之间实现“透明调度”。如此清晰的架构得益于课程组的精心设计,让我真真切切感受到了JML规范在Java语言中的强大。

这里说一点题外话,今天在知乎上看到一个问题:

如果让你来创建一种全新的语言,你会希望它具有什么特征?

高赞回答中提到了

能够通过一种程序员和用户都能看懂的格式抽象出所有运行流程的逻辑语义。

这不就是JML最擅长的吗?这样看来,JML早已悄悄地跑到了大多数语言前面。

第一次作业

第一次作业要求我们实现一个支持可达性判断的图模拟社交网络。

UML图如下

Unit3-窝窝社交圈_第2张图片
  • 代码结构

结构大致可以分为三层:graph->vertex->algo.在算法层面不用考虑每个人具体的情况,只需要知道这是个点,并且图是无向的,那么就可以在接口一致的情况下调用封装好的算法了。(这和Java自带的数据结构非常相似)

  • 复杂度分析
Unit3-窝窝社交圈_第3张图片 Unit3-窝窝社交圈_第4张图片

从反馈结果可以看出,因为课程组为我们提供的良好架构,因此复杂度都体现在算法层面,而不是系统本身的逻辑架构上。这么说或许有些抽象,进一步阐释就是说,复杂度的升高并不是因为架构的复杂,而是来自算法对逻辑和数据结构要求的提高。

第二次作业

第二次作业要求我们实现可以增加子图,并在子图中进行查询的社交网络模拟系统。

UML图如下

Unit3-窝窝社交圈_第5张图片
  • 代码结构

代码架构基本延续了第一次的设计风格,虽然加入了SocialGroup这个子图结构,但是可以发现UML图的三层结构几乎没有变化。

  • 复杂度分析
Unit3-窝窝社交圈_第6张图片

Unit3-窝窝社交圈_第7张图片

从反馈结果可以看出,这次的复杂度依然集中体现在算法中,而不是架构上,JML的威力足够让人吃惊。

第三次作业

第三次作业要求我们进一步实现可以进行高级查询操作的社交网络模拟系统。

UML图如下

Unit3-窝窝社交圈_第8张图片
  • 代码结构

代码架构基本延续了第一次的设计风格,但是出于查询要求需要,增加了一些新的算法。

  • 复杂度分析
Unit3-窝窝社交圈_第9张图片

从反馈结果可以看到,由于queryStrongLinked的出现,导致了最高复杂度的“易主”,因为采用的是Menger定理的枚举思想,因此循环较多,在圈复杂度上体现得尤为明显。

评测相关

重难点聚焦

从三次开发和互测的过程来看,问题和上一单元相同,主要都在CPU时间上。但是这一单元出现这个问题的原因不是来自大家的陌生,就我自己而言,是对某些算法的陌生和复杂度的估计不准确。

在第三次作业中,因为是根据Menger定理枚举获得分离集进行queryStrongLinked的判断,虽然采用了数组和BFS代替普通的数据结构和递归进行加速,但是还是出现了超时的情况。讲道理复杂度和标程的\(O(n^2)\)相比应该是会小一点的,可能因为标程在其他的部分做的更加完善,所以能够不超时。

之后将算法改为了Tarjan来进行判断,时间复杂度确实下降不少。

权当这是个教训吧,还是要时刻牢记:

要射下太阳的人,他的箭头总会比太阳高一些的。

Hack所用的策略

主要有两种方式:

  • 使用完成作业时有意义的样例进行测试
  • 利用评测机自己生成数据

评测机简要介绍

这是评测机的working directory

image-20200318121023438

  • center:存放评测的核心控制代码,用于组织编译->运行->反馈功能

  • data:存放自动生成的数据

  • download_data:存放测试中出现问题的数据,可以用于回归测试。

  • factory:存放数据生成代码

  • output:存放各个测试代码的输出

  • result:存放各个测试代码的结果

  • ruler:存放标程

  • src:存放源码

  • filter.py:用于格式化数据以提交

重构策略

本单元的一大特点就是规格化编程,同时得益于课程组的精心设计,在架构上不需要我们进行过多的自主设计。课程组已经为我们设计好了相当完备和精巧的架构,因此本单元的重构问题其实是被巧妙地规避了,在这里衷心地向老师和助教们的辛苦构思表示感谢。

课程体验感受

这一单元的迭代开发让我接触到了Java工程的超现实主义开发方法:面向JML规格编程。请原谅我给它取了一个略显中二的名字,但是它的与众不同的确让人无法抗拒。

我们很多时候都会面临读别人写的代码的情况——无论是在此基础上继续开发,还是借鉴思想另起炉灶,对代码执行逻辑的准确理解都是相当重要的。因此,养成良好的阅读习惯不仅有助于我们去理解别人,更有助于我们规范自己的风格。这一单元的学习在我看来是非常有必要的,因为它让大多数人第一次不是仅仅为了完成功能而写代码,而是开始为了搭建一个系统并构建一个良好的可扩展的生态,这是让我醉心于OO的一大原因,也是OO的魅力所在。

最后,再次感谢一路上陪伴的老师、助教、同学,谢谢大家!

你可能感兴趣的:(Unit3-窝窝社交圈)