BUAA_OO_UNIT3 总结
一、JML语言基础梳理
-
JML(Java Modeling Language,Java建模语言),在Java代码种增加了一些符号,这些符号用来标志一个方法是干什么的,但是不关心它的具体实现。通过使用JML,在实现代码前,我们可以描述一个方法的预期功能,而尽可能地忽略实现,从而把过程性思考一直延迟到方法设计的层面。
-
如果仅仅是描述方法的功能,那么自然语言一样可以做到,但是,使用JML语言的好处是,相比于容易产生歧义的自然语言,以前置条件、副作用、异常行为、作用域、后置条件等为标准规格的JML规格语言能减少歧义的产生。
-
一段简易的jml代码示例如下:
/*@ public normal_behavior(注:一般行为)
@ requires class != null;(注:前置条件)
@ assignable \nothing;(注副作用)
@ ensures \result = (class == nowclass)(注:后置条件、\result-返回值)
@ also
@ public exceptional_behavior(注:异常行为)
@ signals (ClassNotValid e) class == null;(注:抛出异常)
*/
- 当然,是语言就会有问题,但是,严格的JML语言避免了本身的歧义,一旦出现问题,就很容易能找到是JML规格的描述问题还是代码的实现问题。对用户而言,能提早发现用户代码对类地错误使用,还能给用户提供和代码配套的JML规格文档;而对于程序员,能够准确地知道代码需要实现的功能,高效地寻找和修正程序的bug(对比代码和规格便知),还能在代码升级时降低引入bug的风险。
二、架构与设计分析
1.数据结构
- 根据JML的描述,很容易选择JML描述中的列表等数据结构,这样的结果就是正确性得到了保证,然而程序的效率相当低下(
或许这也是jml的一个坑?)。 - 有此思量,在进行数据结构的选择时,我尽可能使用索引(即Map)结构:
JML描述(以Person为例)
/*@ public instance model int id;
@ public instance model non_null String name;
@ public instance model int age;
@ public instance model non_null Person[] acquaintance;
@ public instance model non_null int[] value;
@ public instance model int money;
@ public instance model int socialValue;
@ public instance model non_null Message[] messages;
@*/
代码实现
private final int id;
private final String name;
private final int age;
// Person - Value
private final HashMap<Person,Integer> linkedPersonMap;
private final Deque<Message> messageList;
private int money;
private int socialValue;
2.算法考量
1.isCircle
jml描述
点击查看代码
/*@ 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;
显然是查询两个节点是否连通,为了效率考量,我选择并查集实现;同时,为了代码复用与编写方便,我将并查集结构进行了封装
public class DisJointSet {
private final HashMap<PersonPro, PersonPro> fatherMap; // 子节点->父节点 映射
private final HashMap<PersonPro, Integer> rankMap; // 优先级集合
private int blockSum; // 保存block数,方便进行查询
public DisJointSet() {
this.fatherMap = new HashMap<>();
this.rankMap = new HashMap<>();
this.blockSum = 0;
}
public void addPerson(PersonPro personPro) {
this.fatherMap.put(personPro,personPro);
this.rankMap.put(personPro, 0);
this.blockSum++; // 每增加一个Person,便会增加一个block
}
public void merge(PersonPro personPro1, PersonPro personPro2) {
PersonPro root1 = findParent(personPro1);
PersonPro root2 = findParent(personPro2);
if (!root1.equals(root2)) {
/* 每进行一次非同源节点的合并
(即两个节点原本不在同一个block中,但是此时合并两个block)
,block数减1 */
blockSum--;
if (rankMap.get(root1) < rankMap.get(root2)) {
fatherMap.put(root1,root2);
} else {
fatherMap.put(root2, root1);
if (rankMap.get(root1).equals(rankMap.get(root2))) {
rankMap.put(root1,rankMap.get(root1) + 1);
}
}
}
}
public PersonPro findParent(PersonPro rootPerson) {
PersonPro tempPerson = rootPerson;
PersonPro tempRoot = tempPerson;
while (!tempRoot.equals(fatherMap.get(tempRoot))) {
tempRoot = fatherMap.get(tempRoot);
}
// 开始路径压缩
while (!tempPerson.equals(tempRoot)) {
PersonPro preFather = fatherMap.get(tempPerson);
fatherMap.put(tempPerson,tempRoot);
tempPerson = preFather;
}
return tempRoot;
}
public boolean isCircle(PersonPro p1, PersonPro p2) {
return findParent(p1).equals(findParent(p2));
}
public int getBlockSum() {
return blockSum;
}
}
2.queryLeastConnection
jml描述
点击查看代码
/*@ public normal_behavior
@ requires contains(id);
@ ensures \result ==
@ (\min Person[] subgroup; subgroup.length % 2 == 0 &&
@ (\forall int i; 0 <= i && i < subgroup.length / 2; subgroup[i * 2].isLinked(subgroup[i * 2 + 1])) &&
@ (\forall int i; 0 <= i && i < people.length; isCircle(id, people[i].getId()) <==>
@ (\exists int j; 0 <= j && j < subgroup.length; subgroup[j].equals(people[i]))) &&
@ (\forall int i; 0 <= i && i < people.length; isCircle(id, people[i].getId()) <==>
@ (\exists Person[] connection;
@ (\forall int j; 0 <= j && j < connection.length - 1;
@ (\exists int k; 0 <= k && k < subgroup.length / 2; subgroup[k * 2].equals(connection[j]) &&
@ subgroup[k * 2 + 1].equals(connection[j + 1])));
@ connection[0].equals(getPerson(id)) && connection[connection.length - 1].equals(people[i])));
@ (\sum int i; 0 <= i && i < subgroup.length / 2; subgroup[i * 2].queryValue(subgroup[i * 2 + 1])));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id);
@*/
public /*@ pure @*/ int queryLeastConnection(int id) throws PersonIdNotFoundException;
实际就是在询问以personId值为id的Person所在block的最小生成树,考虑到本次作业的图大多为稀疏图,我选择使用Kruskal
算法,结合并查集,实测对比中发现,其较Prim
算法效率会优秀很多。具体实现过程略。
3.sendIndirectMessage
jml描述
点击查看代码
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ !isCircle(getMessage(id).getPerson1().getId(), getMessage(id).getPerson2().getId());
@ ensures \result == -1;
@ also
@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ isCircle(getMessage(id).getPerson1().getId(), getMessage(id).getPerson2().getId());
@ assignable messages, emojiHeatList;
@ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
@ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
@ ensures (\exists Person[] pathM;
@ pathM.length >= 2 &&
@ pathM[0].equals(\old(getMessage(id)).getPerson1()) &&
@ pathM[pathM.length - 1].equals(\old(getMessage(id)).getPerson2()) &&
@ (\forall int i; 1 <= i && i < pathM.length; pathM[i - 1].isLinked(pathM[i]));
@ (\forall Person[] path;
@ path.length >= 2 &&
@ path[0].equals(\old(getMessage(id)).getPerson1()) &&
@ path[path.length - 1].equals(\old(getMessage(id)).getPerson2()) &&
@ (\forall int i; 1 <= i && i < path.length; path[i - 1].isLinked(path[i]));
@ (\sum int i; 1 <= i && i < path.length; path[i - 1].queryValue(path[i])) >=
@ (\sum int i; 1 <= i && i < pathM.length; pathM[i - 1].queryValue(pathM[i]))) &&
@ \result==(\sum int i; 1 <= i && i < pathM.length; pathM[i - 1].queryValue(pathM[i])));
@ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
@ (\old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() &&
@ \old(getMessage(id)).getPerson2().getMoney() ==
@ \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessage)\old(getMessage(id))).getMoney());
@ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));
@ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
@ (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
@ emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
@ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
@ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id) ||
@ containsMessage(id) && getMessage(id).getType() == 1;
@*/
public int sendIndirectMessage(int id) throws
MessageIdNotFoundException;
本条指令实则就是在询问最短路径;选择dijkstra
算法即可;考虑到数据量较大,于是我对dijkstra
算法的实现过程进行了优化,同时封装了一个名为NodeMap
的结构,便于代码复用。
Node:点结构
点击查看代码
public class Node implements Comparable<Node> {
private final PersonPro personPro; // 当前节点
private final int distance; // 与出发点的距离
public Node(PersonPro personPro, int distance) {
this.distance = distance;
this.personPro = personPro;
}
public int getDistance() {
return distance;
}
public PersonPro getPersonPro() {
return personPro;
}
public int getPersonId() {
return personPro.getId();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Node)) {
return false;
}
Node node = (Node) o;
return distance == node.distance && personPro.equals(node.personPro);
}
@Override
public int hashCode() {
return Objects.hash(personPro.getId(), distance);
}
@Override
public int compareTo(Node o) {
return Integer.compare(distance, o.getDistance());
}
}
Edge:边结构
点击查看代码
public class Edge implements Comparable<Edge> {
private final PersonPro from; // 出发节点
private final PersonPro to; // 终止节点
private final int value; // 边的权值
public Edge(PersonPro from, PersonPro to, int value) {
this.from = from;
this.to = to;
this.value = value;
}
public PersonPro getFrom() {
return from;
}
public PersonPro getTo() {
return to;
}
public int getValue() {
return value;
}
@Override
public int compareTo(Edge e) {
return value - e.getValue();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Edge)) {
return false;
}
Edge edge = (Edge) o;
return value == edge.value && from.equals(edge.from) && to.equals(edge.to);
}
@Override
public int hashCode() {
return Objects.hash(from, to, value);
}
}
NodeMap:封装以及堆优化的dijkstra
public class NodeMap {
private static final int MAX_VALUE = 0x7fffffff;
private final HashMap<PersonPro, HashSet<PersonPro>> personLinkedMap;
private static final NodeMap NODE_MAP = new NodeMap();
private NodeMap() {
this.personLinkedMap = new HashMap<>();
}
public static NodeMap getNodeMap() {
return NODE_MAP;
}
public void addPerson(PersonPro personPro) {
personLinkedMap.put(personPro,new HashSet<>());
}
public void addRelation(PersonPro from, PersonPro to) {
personLinkedMap.get(from).add(to);
}
public int dijkstra(PersonPro from, PersonPro to) {
HashMap<PersonPro,Integer> ansMap = new HashMap<>(); // ID -> DISTANCE
HashSet<Integer> visitSet = new HashSet<>();
for (PersonPro personPro: personLinkedMap.keySet()) {
ansMap.put(personPro, MAX_VALUE);
}
ansMap.put(from, 0);
PriorityQueue<Node> nodeQueue = new PriorityQueue<>();// 使用堆优化
nodeQueue.add(new Node(from, 0));
while (!nodeQueue.isEmpty()) {
Node node = nodeQueue.poll();
assert node != null;
if (visitSet.contains(node.getPersonId())) {
continue;
}
visitSet.add(node.getPersonId());
for (Person person: node.getPersonPro().getLinkedPersons()) {
PersonPro personPro = (PersonPro) person;
if (!visitSet.contains(personPro.getId()) &&
ansMap.get(node.getPersonPro()) + personPro.queryValue(node.getPersonPro())
< ansMap.get(personPro)) {
ansMap.put(personPro,
ansMap.get(node.getPersonPro()) +
personPro.queryValue(node.getPersonPro()));
nodeQueue.add(new Node(personPro,ansMap.get(personPro)));
}
}
}
return ansMap.get(to);
}
}
三、Network扩展
类规格定义
Commodity:封装商品类
/*@ public instance model non_null String name; // 商品名称
@ public instance model int price; // 商品价格
@ public instance model int preference; // 商品对应消费者偏好值
@*/
Producer
/*@ public instance model non_null Commodity; // 生产的商品,一个生产者只生产一种商品
@ public instance model int sales; // 商品销售额
@*/
Advertiser
/*@ public instance model non_null Producer[] producers; // 一个广告商可为多家厂商打广告
@*/
Log:封装日志类
/*@ public instance model non_null Customer customer; // 消费者
@ public instance model non_null Advertiser advertiser; // 广告商
@ public instance model non_null Producer producer; // 生产者
@*/
Customer
/*@ public instance model non_null Log[] logs; // 一个消费者的购买记录
@ public instance model int preference; // 消费者偏好值
@*/
Advertisement: 继承自Message的广告类
public class Advertisement extends MyMessage {
/*@ public invariant socialValue == 0;
@ public instance model non_null Advertiser advertiser; // 广告商
@ public instance model non_null Producer producer; // 生产者
@*/
/*@ ensures type == 2;
@ ensures group == null;
@ ensures id == messageId;
@ ensures person1 == messagePerson1;
@ ensures person2 == messagePerson2;
@*/
public Advertisement (int messageId, Person messagePerson1, Person messagePerson2) {
super(messageId, 0, messagePerson1, messagePerson2);
this.advertiser = messagePerson1;
this.producer = messagePerson2;
}
...
}
方法规格定义
查询销售路径
/*@ public normal_behavior
@ requires containsCustomer(id) && i < getCustomer(id).records.length;
@ assignable \nothing;
@ ensures \result == getCustomer(id).records[i];
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !containsCustomer(id);
@ signals (RecordsiOutOfBoundException e) containsCustomer(id) &&
@ i >= getCustomer(id).records.length;
@*/
public /*@ pure @*/ Record getRecord(int id, int i); // 查询id为`id`的消费者的第i条购买记录,从而获得销售路径
查询销售额
/*@ public normal_behavior
@ requires containsProducer(id);
@ assignable \nothing;
@ ensures \result == getProducer(id).sales;
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !containsProducer(id);
@*/
public /*@ pure @*/ int getSales(int id);
消费者购买
/*@ public normal_behavior
@ requires containsCustomer(consumerId) && containsAdvertiser(advertiserId)
@ && containsProducer(producerId)
@ && getAdvertiser(advertiserId).producers.contains(getProducer(producerId));
@ assignable getProducer(producerId).sales, getCustomer(consumerId).logs;
@ ensures \old(getProducer(producerId).sales) + getProducer(producerId).price == getProducer(producerId).sales;
@ ensures \old(getCustomer(consumerId).logs.length) + 1 == getCustomer(consumerId).logs.length;
@ ensures getCustomer(consumerId).logs[getCustomer(consumerId).logs.length - 1].customer
@ == getCustomer(consumerId);
@ ensures getCustomer(consumerId).logs[getCustomer(consumerId).logs.length - 1].advertiser
@ == getAdvertiser(advertiserId);
@ ensures getCustomer(consumerId).logs[getCustomer(consumerId).logs.length - 1].producer
@ == getProducer(producerId);
@ ensures (\forall int i; 0 <= i && i < \old(getCustomer(consumerId).logs.length);
@ getCustomer(consumerId).logs[i] == \old(getCustomer(consumerId).logs[i]));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !containsCustomer(consumerId);
@ signals (PersonIdNotFoundException e) containsCustomer(consumerId) && !containsAdvertiser(advertiserId);
@ signals (PersonIdNotFoundException e) containsCustomer(consumerId) && containsAdvertiser(advertiserId)
@ && !containsProducer(producerId);
@ signals (NotAdvertiseException e) containsCustomer(consumerId) && containsAdvertiser(advertiserId)
@ && containsProducer(producerId)
@ && !getAdvertiser(advertiserId).producers.contains(getProducer(producerId));
@*/
public void purchase(int consumerId, int advertiserId, int producerId);
四、程序对拍
-
本次对拍我并未参与测试数据生成的工作,由另一位小伙伴完成,在此便不重点讨论数据生成部分。
-
传统对拍即获取输入输出,然后到网址在线比较两份程序输出的异同;虽然但是,这种低效率工作不能符合我的要求(指bug频出,对拍数据太少会寄),于是我通过数十行
Python
代码,实现了如下简易多人并发对拍机。 -
对拍任务毫无疑问是CPU密集型任务,使用多线程或者协程优化都没有太大意义,通过OS的学习,我认为使用多进程 + 进程池的方式实现对拍,可以让对拍效率最大化,对拍机源码如下:
工具函数部分
import os
import subprocess
import multiprocessing
import time
# 通过管道通信输入方式运行jar包程序,同时返回运行结果,异常,运行时间等数据
def run_jar(jar_path, test_stdin: str):
begin = time.time()
runner = subprocess.Popen('java -jar %s' % jar_path,
shell=True,
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = runner.communicate(test_stdin.encode('utf-8'))
end = time.time()
result = str(stdout, 'utf-8').split('\n')
cost = end - begin
return jar_path, result[:len(result) - 1], str(stderr, 'utf-8'), cost
# 解析txt输入数据,转为字符串输入
def get_stdin_from_file(file_path: str):
with open(file_path, "r") as stdin_file:
stdin = stdin_file.read()
return stdin, stdin.split('\n')[:-1]
# 比较两个程序输出异同
def get_difference(arr1: list[str], arr2: list[str]):
for i in range(len(arr1)):
if arr1[i] != arr2[i]:
return i
return -1
对拍函数主体
# 对拍程序
def compare(jar_list: list[str], stdin_dir_path: str):
stdin_list = filter(lambda x: x.endswith(".txt"), os.listdir(stdin_dir_path))
index = 0
for stdin in stdin_list:
result_list = []
stdin_str, stdin_map = get_stdin_from_file("%s/%s" % (stdin_dir_path, stdin))
pool = multiprocessing.Pool(processes=3)
for item in jar_list:
result_list.append(pool.apply_async(run_jar, (item, stdin_str)))
pool.close()
pool.join()
print("testcase %d begin" % index)
stdout_list = []
cost_list = []
jar_list = []
for res in result_list:
jar_path, stdout, stderr, cost = res.get()
if stderr != '':
print("error occurs with %s at testcase %s\n error as:\n %s " % (jar_path, stdin, stderr))
return
stdout_list.append(stdout)
cost_list.append(cost)
jar_list.append(jar_path)
for i in range(0, len(stdout_list) - 1):
if stdout_list[i] != stdout_list[i + 1]:
print("difference occurs at testcase %s" % stdin)
difference_line = get_difference(stdout_list[i], stdout_list[i + 1])
print("difference at line %d, instruction is %s " %
(difference_line, stdin_map[difference_line]))
for j in range(len(jar_list)):
print("%s's output is %s" % (jar_list[j], stdout_list[j][difference_line]))
return
for i in range(len(cost_list)):
print("%s:\n use time: %.6fs" % (jar_list[i], cost_list[i]))
print("testcase %d end" % index)
index += 1
main函数部分
if __name__ == '__main__':
compare(["F:/OO/tasks/homework11/zjw.jar",
"F:/OO/tasks/homework11/Archer.jar",
"F:/OO/tasks/homework11/Lancer.jar",
"F:/OO/tasks/homework11/Assassin.jar",
"F:/OO/tasks/homework11/Berserker.jar",
"F:/OO/tasks/homework11/Saber.jar"] # 待对拍的所有jar路径存放于此列表下
, "F:/OO/tasks/homework11/normal0group" # 测试数据所在文件夹路径)
五、心得体会
- JML规格,或者说一般意义上的规格,是为了软件开发和团队合作而生的。其在团队合作的作用应该是减少不同人代码开发的耦合度,同时划分明确的分工。在软件开发的层面上,更多地侧重于把软件功能层次化,细分化。
- 规格,侧重于功能而非实现,也就是先给出了一个具体地框架,我认为可以理解为给出了一个黑箱,规格一开始要说明这个黑箱能干什么;然后,规格会进一步拆解这个黑箱,去说明更多的小黑箱能干什么;最终规格会缩小到方法地层面上,说明一个方法黑箱应具有怎么样地输入和输出。
- 对于规格地实现,我的理解是,只需要理解规格给出的功能性说明,而不需要在意规格给出的可能的代码逻辑。举个最简单的例子,第一次作业中Path的数据规格给了一个int[]的类型,实际上这只是说明存在这样的数据结构,而非一定要实现这样的数组,实际上,真正这么搞的同学估计没有几个。
- 我觉得规格使用相当有必要,而且在完成规格后再实现代码会具有更高的效率。目前为止,我所接触的代码都是很小的,几乎不成功能,但是我觉得如果我想完成一个软件,那么我不仅会写设计文档,还会在完成设计文档后,写详细的规格说明书,最后再开始实现代码。
- 最后,规格的限制固然必要,但是如何让规格描述更加通俗易懂且严谨,却也是我们需要思考的问题。