面向對象第三單元總結 - JML(Java Modeling Language)
- 面向對象第三單元總結 - JML(Java Modeling Language)
- 一、JML 理論基礎 與 應用工具鏈
- 1.1 JML 理論基礎
- 1.2 JML應用工具鏈
- 1.2.1 OpenJML
- 1.2.2 JMLUnitNG/JMLUnit
- 二、JMLUnitNG/JMLUnit部署
- 三、架構設計
- 3.1 作業1
- 3.2 作業2
- 3.3 作業3
- 四、BUG分析
- 五、感想
- 一、JML 理論基礎 與 應用工具鏈
一、JML 理論基礎 與 應用工具鏈
1.1 JML 理論基礎
JML(Java Modeling Language)是用於對Java程序進行規格化設計的一種表示語言。JML是一種行爲接口規格語言(Behavior Interface Specification Language, BISL),基於Larch方法構建。BISL提供了對方法和類型的規格定義手段。所謂接口即一個方法或類型外部可見的内容。
以上文字摘自課程組下發的JML(Level 0)使用手冊。
1.2 JML應用工具鏈
1.2.1 OpenJML
OpenJML is a program verification tool for Java programs that allows you to check the specifications of programs annotated in the Java Modeling Language.
OpenJML是可用於Java程序的程序驗證工具,它可以檢查使用JML(Java Modeling Language)語言進行注釋的程序的正確性。它支持靜態的檢查,也支持運行時檢查。此外它還集成了一些SMT Solvers,便於對程序進行更深層次的驗證。
它的官網是http://www.openjml.org/
1.2.2 JMLUnitNG/JMLUnit
JMLUnitNG is an automated unit test generation tool for JML-annotated Java code, including code using Java 1.5+ features such as generics, enumerated types, and enhanced for loops. Like the original JMLUnit, it uses JML assertions as test oracles. It improves upon the original JMLUnit by allowing easy customization of data to be used for each method parameter of a class under test, as well as by using Java reflection to automatically generate test data of non-primitive types.
JMLUnitNG是JMLUnit的進階版本,全稱為JMLUnit Next Generation。JMLUnitNG是用於帶有JML註釋的Java代碼的自動化單元測試生成工具,包括使用Java 1.5+特性(例如泛型,枚舉類型和增強的for循環)的代碼。 像原始的JMLUnit一樣,它使用JML斷言作爲測試。 它通過允許對要測試的類的每個方法參數輕鬆地自定義數據,以及使用Java反射自動生成非原始類型的測試數據,對原始JMLUnit進行了改進。
JMLUnitNG最近更新於2014年,年代久遠,用起來也不是很方便。
它的官網是http://insttech.secretninjaformalmethods.org/software/jmlunitng/
二、JMLUnitNG/JMLUnit部署
JMLUnitNG的部署恐怕是最難的地方了。說實話,看到大家在使用過程中各種報錯,我甚至想像某學長“遂放棄”一樣結束這一部分。但是後來我自己嘗試了一下,發現還是可以正常使用的。正常使用之前需要先做如下幾個工作:
-
使用jdk8。
-
將整個文件樹複製到工作目錄下test文件夾,並將所有的java文件開頭加上
package test;
或在已有package前加上test.
(如package test.com.oocourse .spec3.main;
)。 -
將Group.java中所有的JML代碼複製到MyGroup.java中相對應的地方,還需要將所有的
@Override
刪掉。 -
將JML中的變量名與Java程序中的變量名修改,使其不會重名。我使用了VSCode對於Java代碼的批量重命名功能,將Java代碼中的變量名進行了修改。修改JML變量名有點麻煩。
-
在構造方法中,對HashMap的new操作顯式指出HashMap的鍵值對類型。如
this.peopleMap = new HashMap
。如果不這樣做的話,在使用JMLUnitNG時會有如下報錯(以及一大堆警告,此處略去):(); C:\Users\NBao\jml>java -jar jmlunitng.jar test\MyGroup.java JMLUnitNG exited because of an irrecoverable error: org.jmlspecs.jmlunitng.JMLUnitNGError: Encountered 2 compilation errors: C:\Users\NBao\jml\test\MyGroup.java:23: error: illegal start of type this.peopleMap = new HashMap<>(); ^ warning: ...
最後我的MyGroup.java是這樣的内容:
package test;
import test.com.oocourse.spec3.main.Group;
import test.com.oocourse.spec3.main.Person;
import java.math.BigInteger;
import java.util.HashMap;
public class MyGroup implements Group {
/*@ public instance model non_null int id;
@ public instance model non_null Person[] people;
@*/
private final int idNum;
private final HashMap peopleMap;
private int relationSum;
private int valueSum;
private BigInteger conflictSum;
private int ageSum;
private int ageSumSquare;
public MyGroup(int id) {
this.idNum = id;
this.peopleMap = new HashMap();
this.relationSum = 0;
this.valueSum = 0;
this.conflictSum = BigInteger.ZERO;
this.ageSum = 0;
this.ageSumSquare = 0;
}
//@ ensures \result == id;
public /*@pure@*/ int getId() {
return idNum;
}
/*@ also
@ public normal_behavior
@ requires obj != null && obj instanceof Group;
@ assignable \nothing;
@ ensures \result == (((Group) obj).getId() == id);
@ also
@ public normal_behavior
@ requires obj == null || !(obj instanceof Group);
@ assignable \nothing;
@ ensures \result == false;
@*/
public /*@pure@*/ boolean equals(Object obj) {
if (!(obj instanceof Group)) {
return false;
}
return ((Group) obj).getId() == idNum;
}
public void addPerson(Person person) {
for (Person p : peopleMap.values()) {
if (p.isLinked(person)) {
relationSum += 2;
valueSum += 2 * p.queryValue(person);
}
}
relationSum++;
conflictSum = conflictSum.xor(person.getCharacter());
ageSum += person.getAge();
ageSumSquare += person.getAge() * person.getAge();
peopleMap.put(person.getId(), person);
}
//@ ensures \result == (\exists int i; 0 <= i && i < people.length; people[i].equals(person));
public /*@pure@*/ boolean hasPerson(Person person) {
return peopleMap.containsKey(person.getId());
}
/*@ ensures \result == (\sum int i; 0 <= i && i < people.length;
@ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1));
@*/
public /*@pure@*/ int getRelationSum() {
return relationSum;
}
/*@ 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])));
@*/
public /*@pure@*/ int getValueSum() {
return valueSum;
}
/*@ public normal_behavior
@ requires people.length > 0;
@ ensures (\exists BigInteger[] temp;
@ temp.length == people.length && temp[0] == people[0].getCharacter();
@ (\forall int i; 1 <= i && i < temp.length;
@ temp[i] == temp[i-1].xor(people[i].getCharacter())) &&
@ \result == temp[temp.length - 1]);
@ also
@ public normal_behavior
@ requires people.length == 0;
@ ensures \result == BigInteger.ZERO;
@*/
public /*@pure@*/ BigInteger getConflictSum() {
return conflictSum;
}
/*@ ensures \result == (people.length == 0? 0 :
@ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
@*/
public /*@pure@*/ int getAgeMean() {
return peopleMap.size() == 0 ? 0 : ageSum / peopleMap.size();
}
/*@ ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length;
@ (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) /
@ people.length));
@*/
public /*@pure@*/ int getAgeVar() {
if (peopleMap.size() == 0) {
return 0;
}
int mean = getAgeMean();
int n = peopleMap.size();
return (ageSumSquare + n * mean * mean - 2 * mean * ageSum) / n;
}
public void delPerson(Person person) {
peopleMap.remove(person.getId());
for (Person p : peopleMap.values()) {
if (p.isLinked(person)) {
relationSum -= 2;
valueSum -= 2 * p.queryValue(person);
}
}
relationSum--;
conflictSum = conflictSum.xor(person.getCharacter());
ageSum -= person.getAge();
ageSumSquare -= person.getAge() * person.getAge();
}
public int getSize() {
return peopleMap.size();
}
public void updateLink(int value) {
relationSum += 2;
valueSum += 2 * value;
}
}
此時的目錄樹是這樣的:
C:\Users\NBao\jml>tree /f
Folder PATH listing
Volume serial number is 44BA-9DE9
C:.
│ jmlunitng.jar
│ openjml.jar
│
└─test
│ Dijkstra.java
│ Main.java
│ MyGroup.java
│ MyNetwork.java
│ MyPerson.java
│ Tarjan.java
│ UnionFindSet.java
│
└─com
└─oocourse
└─spec3
├─exceptions
│ EqualGroupIdException.java
│ EqualPersonIdException.java
│ EqualRelationException.java
│ GroupIdNotFoundException.java
│ PersonIdNotFoundException.java
│ RelationNotFoundException.java
│
└─main
Group.java
Network.java
Person.java
Runner.java
依次執行以下四條指令:
java -jar jmlunitng.jar test/MyGroup.java
javac -cp jmlunitng.jar test/*.java
java -jar openjml.jar -rac test/MyGroup.java
java -cp jmlunitng.jar test.MyGroup_JML_Test
得到下面的結果:
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. 著作權所有,並保留一切權利。
C:\Users\NBao>cd jml
C:\Users\NBao\jml>java -jar jmlunitng.jar test\MyGroup.java
C:\Users\NBao\jml>javac -cp jmlunitng.jar test\*.java
C:\Users\NBao\jml>java -jar openjml.jar -rac test\MyGroup.java
test\MyGroup.java:3: error: package test.com.oocourse.spec3.main does not exist
import test.com.oocourse.spec3.main.Group;
^
test\MyGroup.java:4: error: package test.com.oocourse.spec3.main does not exist
import test.com.oocourse.spec3.main.Person;
^
test\MyGroup.java:9: error: cannot find symbol
public class MyGroup implements Group {
^
symbol: class Group
test\MyGroup.java:11: error: cannot find symbol
@ public instance model non_null Person[] people;
^
symbol: class Person
location: class MyGroup
test\MyGroup.java:14: error: cannot find symbol
private final HashMap peopleMap;
^
symbol: class Person
location: class MyGroup
test\MyGroup.java:54: error: cannot find symbol
public void addPerson(Person person) {
^
symbol: class Person
location: class MyGroup
test\MyGroup.java:70: error: cannot find symbol
public /*@pure@*/ boolean hasPerson(Person person) {
^
symbol: class Person
location: class MyGroup
test\MyGroup.java:125: error: cannot find symbol
public void delPerson(Person person) {
^
symbol: class Person
location: class MyGroup
test\MyGroup.java:23: error: cannot find symbol
this.peopleMap = new HashMap();
^
symbol: class Person
location: class MyGroup
test\MyGroup.java:38: error: cannot find symbol
@ requires obj != null && obj instanceof Group;
^
symbol: class Group
location: class MyGroup
test\MyGroup.java:40: error: cannot find symbol
@ ensures \result == (((Group) obj).getId() == id);
^
symbol: class Group
location: class MyGroup
test\MyGroup.java:43: error: cannot find symbol
@ requires obj == null || !(obj instanceof Group);
^
symbol: class Group
location: class MyGroup
test\MyGroup.java:48: error: cannot find symbol
if (!(obj instanceof Group)) {
^
symbol: class Group
location: class MyGroup
test\MyGroup.java:51: error: cannot find symbol
return ((Group) obj).getId() == idNum;
^
symbol: class Group
location: class MyGroup
test\MyGroup.java:55: error: cannot find symbol
for (Person p : peopleMap.values()) {
^
symbol: class Person
location: class MyGroup
The operation symbol ++ for type java.lang.Object could not be resolved
org.jmlspecs.openjml.JmlInternalError: The operation symbol ++ for type java.lang.Object could not be resolved
at org.jmlspecs.openjml.JmlTreeUtils.findOpSymbol(JmlTreeUtils.java:291)
at org.jmlspecs.openjml.JmlTreeUtils.findOpSymbol(JmlTreeUtils.java:282)
at org.jmlspecs.openjml.JmlTreeUtils.makeUnary(JmlTreeUtils.java:739)
at com.sun.tools.javac.comp.JmlAttr.createRacExpr(JmlAttr.java:4465)
at org.jmlspecs.openjml.ext.QuantifiedExpressions$QuantifiedExpression.typecheck(QuantifiedExpressions.java:214)
at com.sun.tools.javac.comp.JmlAttr.visitJmlQuantifiedExpr(JmlAttr.java:4070)
at org.jmlspecs.openjml.JmlTree$JmlQuantifiedExpr.accept(JmlTree.java:2685)
at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577)
at com.sun.tools.javac.comp.Attr.visitParens(Attr.java:2995)
at com.sun.tools.javac.tree.JCTree$JCParens.accept(JCTree.java:1661)
at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577)
at com.sun.tools.javac.comp.Attr.attribExpr(Attr.java:619)
at com.sun.tools.javac.comp.JmlAttr.attribExpr(JmlAttr.java:6209)
at com.sun.tools.javac.comp.JmlAttr.visitJmlMethodClauseExpr(JmlAttr.java:3117)
at org.jmlspecs.openjml.JmlTree$JmlMethodClauseExpr.accept(JmlTree.java:2332)
at com.sun.tools.javac.comp.JmlAttr.visitJmlSpecificationCase(JmlAttr.java:3361)
at org.jmlspecs.openjml.JmlTree$JmlSpecificationCase.accept(JmlTree.java:2837)
at com.sun.tools.javac.comp.JmlAttr.visitJmlMethodSpecs(JmlAttr.java:3423)
at org.jmlspecs.openjml.JmlTree$JmlMethodSpecs.accept(JmlTree.java:2539)
at com.sun.tools.javac.comp.JmlAttr.visitBlock(JmlAttr.java:706)
at com.sun.tools.javac.comp.JmlAttr.visitJmlBlock(JmlAttr.java:3804)
at org.jmlspecs.openjml.JmlTree$JmlBlock.accept(JmlTree.java:1333)
at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577)
at com.sun.tools.javac.comp.Attr.attribStat(Attr.java:646)
at com.sun.tools.javac.comp.JmlAttr.attribStat(JmlAttr.java:558)
at com.sun.tools.javac.comp.Attr.visitMethodDef(Attr.java:1015)
at com.sun.tools.javac.comp.JmlAttr.visitMethodDef(JmlAttr.java:1112)
at com.sun.tools.javac.comp.JmlAttr.visitJmlMethodDecl(JmlAttr.java:6053)
at org.jmlspecs.openjml.JmlTree$JmlMethodDecl.accept(JmlTree.java:1261)
at com.sun.tools.javac.comp.Attr.attribTree(Attr.java:577)
at com.sun.tools.javac.comp.Attr.attribStat(Attr.java:646)
at com.sun.tools.javac.comp.JmlAttr.attribStat(JmlAttr.java:558)
at com.sun.tools.javac.comp.Attr.attribClassBody(Attr.java:4378)
at com.sun.tools.javac.comp.JmlAttr.attribClassBody(JmlAttr.java:536)
at com.sun.tools.javac.comp.Attr.attribClass(Attr.java:4286)
at com.sun.tools.javac.comp.JmlAttr.attribClass(JmlAttr.java:414)
at com.sun.tools.javac.comp.Attr.attribClass(Attr.java:4215)
at com.sun.tools.javac.comp.Attr.attrib(Attr.java:4190)
at com.sun.tools.javac.main.JavaCompiler.attribute(JavaCompiler.java:1258)
at com.sun.tools.javac.main.JmlCompiler.attribute(JmlCompiler.java:479)
at com.sun.tools.javac.main.JavaCompiler.compile2(JavaCompiler.java:898)
at com.sun.tools.javac.main.JmlCompiler.compile2(JmlCompiler.java:712)
at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:867)
at com.sun.tools.javac.main.Main.compile(Main.java:553)
at com.sun.tools.javac.main.Main.compile(Main.java:410)
at org.jmlspecs.openjml.Main.compile(Main.java:581)
at com.sun.tools.javac.main.Main.compile(Main.java:399)
at com.sun.tools.javac.main.Main.compile(Main.java:390)
at org.jmlspecs.openjml.Main.execute(Main.java:417)
at org.jmlspecs.openjml.Main.execute(Main.java:375)
at org.jmlspecs.openjml.Main.execute(Main.java:362)
at org.jmlspecs.openjml.Main.main(Main.java:334)
test\MyGroup.java:128: error: cannot find symbol
for (Person p : peopleMap.values()) {
^
symbol: class Person
location: class MyGroup
16 errors
C:\Users\NBao\jml>java -cp jmlunitng.jar test.MyGroup_JML_Test
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(java.lang.Object@4fccd51b)
Passed: <>.equals(java.lang.Object@60215eee)
Passed: <>.equals(java.lang.Object@65e579dc)
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getSize()
Passed: <>.getSize()
Passed: <>.getSize()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Failed: <>.hasPerson(null)
Failed: <>.hasPerson(null)
Failed: <>.hasPerson(null)
Passed: <>.updateLink(-2147483648)
Passed: <>.updateLink(-2147483648)
Passed: <>.updateLink(-2147483648)
Passed: <>.updateLink(0)
Passed: <>.updateLink(0)
Passed: <>.updateLink(0)
Passed: <>.updateLink(2147483647)
Passed: <>.updateLink(2147483647)
Passed: <>.updateLink(2147483647)
===============================================
Command line suite
Total tests run: 49, Failures: 10, Skips: 0
===============================================
此處運行了49個測試用例,39個通過,10個失敗。這其中JMLUnitNG創建了三個MyGroup對象,其id分別爲0、-2147483648、2147483647;之後JMLUnitNG分別調用了這三個MyGroup對象的所有方法進行測試。
可以看到,當方法所需參數爲int類型時,JMLUnitNG會將0、最大整數和最小整數傳入;當方法所需參數爲Object子類時,JMLUnitNG會將null傳入。這説明JMLUnitNG還是很注重邊界條件的測試。不過由於JML規格中的這些方法是與其他方法相互配合的,所以諸如addPerson、delPerson、hasPerson這些方法其實是可以保證傳入的對象不是null的,因此這些使用null進行的測試也沒有特別大的意義。
總體來説,JMLUnitNG不是很好用,其自動生成的測試隨機性不强,檢測到代碼中真正的遺漏與疏忽的可能性也不是很大。
三、架構設計
3.1 作業1
剛開始接觸這一單元的作業時,我以爲這一單元的作業就是要嚴格按照JML規格的要求來寫代碼,以至於我甚至嘗試使用靜態數組作爲容器實現其功能。後來我意識到實際上規格就是用來表述一種抽象關係,在保證符合規格要求的前提下,使用什麼容器都可以。爲了最簡單地和JML規格相對應,我採用了ArrayList來與規格中的數組相對應,這樣在寫代碼的時候可以直接把規格類似地借鑒下來,最為便捷。現在來看,那個時候我對於JML規格的理解還是TOO YOUNG TOO SIMPLE、SOMETIMES NAIVE。
3.2 作業2
作業2的最高指令數達到了100000條,這很恐怖。對於Group中需要查詢的字段,我採取了緩存的方式,避免每次都遍歷。另外我發現,對於MyNetwork中要求實現的大部分函數中,在不存在Person ID的時候,就會直接抛出異常。這種情況下,如果采用ArrayList,就需要對整個列表遍歷一遍,時間複雜度很高。因此,我在作業2開展了一項“掃黑除惡”專項行動——將所有的ArrayList這樣的黑惡勢力替換成HashMap。替換成HashMap的好處在於,查找的複雜度由O(n)變成了O(1)。不過,這就需要我把查找相關的所有代碼推倒重寫。
如何判斷重寫的對不對呢?這就需要JUnit大顯神通了。在重寫之前,我構造了覆蓋全面的JUnit測試用例。在重寫之後,再次執行JUnit測試,如果原來的樣例都可以通過,就説明重寫大致沒有問題(這取決於測試用例的水平)。説實話,重寫之後的程序還真的有一個函數沒有通過測試用例。後來發現,抛出異常的前提條件的boolean語句前面少寫了一個非運算符!
。JUnit真是一個好幫手。
3.3 作業3
作業3,虛假的JML——按照規格寫代碼;真實的JML——數據結構與算法。説實話這次作業需要考慮性能,新增加的這幾個函數要想保證性能較佳,確實需要研究一下算法。這次作業中使用了堆優化的Dijkstra算法、Tarjan算法、并查集數據結構。這些具體的實現我都單獨開了一個類,把複雜的代碼分散,提高代碼可讀性。相對來説我還是比較滿意這次的架構的。
四、BUG分析
三次作業中,作業3的代碼中出現了一個小小的問題。我在周六晚上截止提交之後發現了這一BUG,但是已經無能爲力,哭/(ㄒoㄒ)/~~ 。毫無疑問,在互測中,我被Hack了。
這個BUG是Tarjan算法實現上的一個小小的BUG,和dfs到割點之後、下一個即將遍歷的節點順序有關係。當這個割點與要判斷是否StrongLinked的兩個節點都强連通時,便可能出現判斷失誤的問題。
算法的具體問題有點過於細節,説實話也不應該作爲JML單元的關注點……這次BUG的主要原因還是因爲我對Tarjan算法對於無向圖的處理理解得不透徹,下學期學算法的時候要好好學習一下。
五、感想
我認爲本單元僅僅想要拿到作業分數並不算難,但本單元真正需要思考的卻應當是設計與架構方面的問題。對於我的三次作業,前兩次作業由於較爲簡單,因此我的代碼中只有繼承了三個接口的MyPerson、MyNetwork、MyGroup。第三次作業需要較爲複雜的算法,因此我將那些重要的算法都單獨分出了一個類,如Dijkstra、Tarjan、UnionFindSet這樣三個類。這樣將複雜的算法單獨分離出來,便於相關算法的維護,也避免了將所有代碼堆到MyNetwork中,對於這樣的做法我還是比較滿意的。不過,對於更深層次的構造問題我沒有考慮更多,畢竟實現這些算法就夠我花上兩三天了。
這個單元我對於規格與代碼的區別有了一個較爲清晰的認識,也認識到方法的行爲是可以從函數中抽象出來單獨表示的。雖然之後寫JML規格的機會很少,但是在之後寫代碼的時候至少會在腦海過一遍相關的行爲抽象,也會有意識地控制自己寫的方法,使其行爲的抽象都能較爲簡單地描述出來。事實上,如果某個方法很難用規格描述,那麽就説明這個方法的實現很有可能有較大問題。因此,這一單元的學習對於我之後寫代碼的影響還是比較大的。