jixuchongai

导航

 

OO第三单元总结

测试方法

专项测试

针对复杂度较高的特定方法,基于互测与公测限制进行数据构造

queryBlockSum

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

其中 isCircle判断两个节点是否可达,也是一个比较复杂的方法,因此,若完全按照JML规格用二重循环书写上述qbs方法必然会超时。在数据要求限制下构造极端数据:666条add Person,334条qbs

qgvs,它调用了Group类内部的 getValueSum方法:

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

JML规格中体现的是一个 n^2复杂度的方法,也可能超时。通过计算复杂度,在数据限制下,选择1111条add Person,1111条add to Group与2778条qgvs来hack

普遍测试

在专项测试没有结果时,可以构造随机数据轰炸程序,进而寻找bug

以下借鉴了wxg同学的参数宏与测试主题

int personId = 0, groupId = 0, messageId = 0, emojiId = 0, sendId = 0, currentEmojiId = 0;
int maxag = 25, maxap = 5000, maxqci = 333, maxqlc = 100, maxsim = 1000, maxEmojiId = 10;
//常量宏,以控制人。群组。消息等类的数量
int cntag = 0, cntap = 0, cntqci = 0, cntqlc = 0, cntsim = 0;//指令数量,针对不同指令进行压力测试

int type = 0; // 0 normal, 1 message, 2 group, 3 graph, 4 exception, 5 complete, 6 sim, 7 emoji, 8 notice, 9 super
//不同类型数据,以针对异常、消息、群组等不同类别进行测试

bool noGroup = false, emojiFull = false, oneGroup = false;

通过数据生成器与自动化脚本相结合实现,以下是脚本

#!/bin/bash
echo "" >error.txt
a=0
b=0
declare -a file
file[9]="cp exception0group/testcase$a.txt testcase.txt"
file[8]="cp exception1group/testcase$a.txt testcase.txt"
file[7]="cp exception25group/testcase$a.txt testcase.txt"
file[6]="cp graph0group/testcase$a.txt testcase.txt"
file[5]="cp graph1group/testcase$a.txt testcase.txt"
file[4]="cp graph25group/testcase$a.txt testcase.txt"
file[3]="cp message0group/testcase$a.txt testcase.txt"
file[2]="cp message1group/testcase$a.txt testcase.txt"
file[1]="cp message25group/testcase$a.txt testcase.txt"
file[0]="cp group1group/testcase$a.txt testcase.txt"
while [ $a -ne 5 ]; do
 # python3 dataGen.py -o testcase.txt -m complete-graph
 b=0
 while [ $b -ne 10 ]; do
  eval ${file[$b]}
   echo test$a$b
  time cat testcase.txt | java -jar zkg.jar >zkg.txt
   echo zkg
  time cat testcase.txt | java -jar lsz.jar > lsz.txt
   echo lsz
   diff lsz.txt zkg.txt
     b=$(($b + 1))
 done
 a=$(($a + 1))
do

通过控制文件数组。循环的起始变量,就可以很方便地有针对性地测试,针对性地改进特定方法

JML工具链

OpenJML

OpenJML是可用于Java程序的程序验证工具,它可以检查使用JML语言进行注释的程序的正确性,支持静态检查、运行时动态检查,以及通过SMT Solvers对程序进行更深层次的验证

JMLUnit

可以检查数据覆盖率以尽可能接近全覆盖

示例(来自qs同学)

@Test
public void deleteColdEmoji() throws EqualPersonIdException, PersonIdNotFoundException, EqualRelationException, EmojiIdNotFoundException, EqualMessageIdException, RelationNotFoundException, MessageIdNotFoundException, EqualEmojiIdException
{
    MyNetwork network = new MyNetwork();
    network.addPerson(p1);
    network.addPerson(p2);
    network.addRelation(1, 2, 12);
    network.storeEmojiId(1);
    network.storeEmojiId(2);
    network.addMessage(new MyEmojiMessage(1, 1, p1, p2));
    network.addMessage(new MyEmojiMessage(2, 2, p1, p2));
    network.addMessage(new MyEmojiMessage(3, 2, p1, p2));
    network.addMessage(new MyEmojiMessage(4, 2, p1, p2));
    network.addMessage(new MyEmojiMessage(5, 2, p1, p2));
    network.addMessage(new MyEmojiMessage(6, 1, p1, p2));
    network.sendMessage(3);
    network.sendMessage(4);
    network.sendMessage(5);
    network.sendMessage(6);
    assert network.deleteColdEmoji(2) == 1 : "dce 1";
    assert network.getMessage(1) == null : "dce 2";
    assert network.getMessage(2).getId() == 2 : "dce 3";
    assert network.queryPopularity(2) == 3 : "dce 4";
    assert network.deleteColdEmoji(3) == 1 : "dce 5";
    assert network.queryPopularity(2) == 3 : "dce 6";
    assert network.deleteColdEmoji(4) == 0 : "dce 7";
    assert network.getMessage(2) == null : "dce 8";
}

架构设计

本单元主要是基于JML规格建立一个社交网络并进行各种查询操作,核心类有Network、Group与Person,其中Network为整个网络图、Group为点集、而Person为图中的点。但若完全基于JML规格进行设计,必然会导致超时问题,故需要进行一些查询算法的优化以及建图过程中的动态维护

储存

按照JML规格设计是遍历,不过显式遍历必然会导致TLE,因此引入hashmap高效查询

并查集

第九次作业中的isCircle判断两个人是否可达、queryBlockSum则查询不可达集合的数目,JML规格使用了二重循环,直接按照其来实现必然会超时。从本质上讲,社交网络中的人可以分为并查集,而前者即为查询两者是否处于同一并查集,后者则是返回并查集的数目。

基于此原则动态维护,在network中增设变量记录并查集数目,在加人时+1,在addrelation时合并并查集,从而将qci简化为判断并查集的父节点是否一样,将qbs简化为返回记录变量的值

//MyPerson.java
public MyPerson getFa() {
       if (fa.equals(this)) {
           return fa;
      }
       setFa(fa.getFa());
       return fa;
  }

public void setFa(MyPerson fa) {
   this.fa = fa;
}

public int add(Person person, int value) {
       int flag = 0;
       acquaintance.put(person.getId(), person);
       this.value.put(person.getId(), value);
       if (!((MyPerson) person).getFa().equals(getFa())) {
           flag = 1;
      }
      ((MyPerson) person).getFa().setFa(getFa());
       return flag;
  }

最小生成树

第九次作业建立起了并查集,第十次作业中的queryLeastConnection指令则在此基础上,查询并查集中点构成的图中的最小生成树

我采用的是基于边查询的kruskal方法,首先从边集中提取出本图的边并排序,然后从小到大遍历,并用变量标记是否成环,当已构建好了生成树时直接退出

@Override
   public int queryLeastConnection(int id) throws PersonIdNotFoundException {
       if (!contains(id)) {
           throw new MyPersonIdNotFoundException(id);
      }
       HashMap<Integer, Integer> flags = new HashMap<>();
       for (Integer integer : people.keySet()) {
           if (isCircle(integer, id)) {
               flags.put(integer, integer);
          }
      }
       ArrayList<Edge> edges1 = new ArrayList<>();
       for (Edge edge : edges) {
           if (flags.containsKey(edge.getFrom()) && flags.containsKey(edge.getTo())) {
               edges1.add(edge);
          }
      }
       Collections.sort(edges1);
       int sum = 0;
       int cnt = 0;
       for (Edge edge : edges1) {
           int from = flags.get(edge.getFrom());
           int to = flags.get(edge.getTo());
           if (from != to) {
               sum += edge.getValue();
               cnt++;
               for (Integer key : flags.keySet()) {
                   if (flags.get(key) == to) {
                       flags.put(key, from);
                  }
              }
               if (cnt == flags.size() - 1) {
                   break;
              }
          }
      }
       return sum;
  }

最短路径

第十一次作业中sim指令旨在让两个人经历过并查集中的每个人不直接地发送消息,查询路径权重最小的一个,分析可知为寻求两点间最短路径。由于其为单源点而非多源点,因此采用dijkstra算法,提取出并查集图中的边,每轮循环更新到各点路径权重的最小值直至最终寻找到目标点

@Override
   public int sendIndirectMessage(int id) throws MessageIdNotFoundException {
       /*...*/
       int id1 = message.getPerson1().getId();
       int id2 = message.getPerson2().getId();
       HashMap<Integer, Integer> find = new HashMap<>();
       for (Integer integer : people.keySet()) {
           if (((MyPerson) getPerson(id1)).getFa()
                  .equals(((MyPerson) getPerson(integer)).getFa())) {
               find.put(integer, 2147483647);
          }
      }
       HashMap<Integer, HashMap<Integer, Integer>> graphs = new HashMap<>();
       for (Edge edge : edges) {
           if (find.containsKey(edge.getFrom()) && find.containsKey(edge.getTo())) {
               putIn(edge, graphs);
          }
      }
       HashMap<Integer, Integer> dis = new HashMap<>();
       HashMap<Integer, Integer> sort = new HashMap<>();
       dis.put(id1, 0);
       find.remove(id1);
       int from = id1;
       while (true) {
           int len = dis.get(from);
           HashMap<Integer, Integer> temp = graphs.get(from);
           for (Integer integer : temp.keySet()) {
               if (find.containsKey(integer)) {
                   int length = temp.get(integer);
                   if (length + len < find.get(integer)) {
                       find.put(integer, length + len);
                       sort.put(integer, length + len);
                  }
              }
          }
           graphs.remove(from);
           int min = 2147483647;
           for (Integer i : sort.keySet()) {
               if (sort.get(i) < min) {
                   min = sort.get(i);
                   from = i;
              }
          }
           sort.remove(from);
           dis.put(from, min);
           find.remove(from);
           if (from == id2) {
               sendMessage(message);
               return min;
          }
      }
  }

由于未采用堆优化方法,本算法效率略低于同学的实现

动态维护

动态维护的核心目的就是将复杂度分摊至每一次添加当中,以降低复杂度的次幂。

除了上述算法中的动态维护外主要体现在与group有关的查询指令qgav与qgvs当中。若直接按照JML规格设计,两者分别为O(n)和O(n^2),因此在MyGroup中分别添加记录年龄和、年龄平方和与关系权重和的变量,每次添加时动态维护,将三个指令的复杂度全部降至O(1)

//MyGroup.java
@Override
   public void addPerson(Person person) {
       if (!hasPerson(person)) {
           people.put(person.getId(), person);
           sumAge += person.getAge();
           sumAge2 += person.getAge() * person.getAge();
           for (Person item : people.values()) {
               sumValue += item.queryValue(person);
          }
      }
  }

   @Override
   public void delPerson(Person person) {
       if (hasPerson(person)) {
           for (Person item : people.values()) {
               sumValue -= item.queryValue(person);
          }
           people.remove(person.getId());
           sumAge -= person.getAge();
           sumAge2 -= person.getAge() * person.getAge();
      }
  }

细节

在添加关系时,需要遍历群组添加关系权重值,以防止遗漏;此外需要注意qgvs返回的值应该为记录变量的2倍

//MyGroup.java
public void addValue(int value) {
       sumValue += value;
  }
@Override
   public int getValueSum() {
       return 2 * sumValue;
  }

在增加接收信息时,JML规格是插入到前面并将其他的后移,而在后续查询时则从前往后查询;为了方便管理降低复杂度,我选择了将新关系插入到动态数组后面,并从后向前查询

//MyPerson.java
@Override
   public List<Message> getReceivedMessages() {
       List<Message> list = new ArrayList<>();
       for (int i = messages.size() - 1; i >= 0; i--) {
           list.add(messages.get(i));
           if (i == messages.size() - 4) {
               break;
          }
      }
       return list;
  }

涉及到群发红包时需要注意整除问题,即发给每个人的红包钱数为向下取整,此处需要严格按照JML规格实现,否则可能会出现向下取整造成的bug

if (message.getType() == 1) {
           int money = 0;
           if (message instanceof RedEnvelopeMessage) {
               money = ((RedEnvelopeMessage) message).getMoney() / (message.getGroup().getSize());
               message.getPerson1().addMoney(-money * (message.getGroup().getSize() - 1));
          }
           for (Person item : (((MyGroup) message.getGroup()).getPeople().values())) {
               item.addSocialValue(message.getSocialValue());
               if (message instanceof RedEnvelopeMessage && !item.equals(message.getPerson1())) {
                   item.addMoney(money);
              }
          }
      }

性能问题与修复情况

性能问题已在架构设计中提及,主要通过图论算法和动态维护分摊复杂度来避免

本人在三次公测与互测中出现的唯一bug是,对边排序时使用了Collections.sort()方法,但起初在边类中实现的比较函数只返回了±1,忽略了相等时返回0的情况,导致产生异常

//Collections.sort(edges1):需要注意相等时返回0
@Override
   public int compareTo(Edge o) {
       return Integer.compare(value, o.getValue());
  }

前两次互测中分别发现了同学qbs与qgvs复杂度较高的问题,究其原因是没有按照上述分析问题的本质以及分摊复杂度,而是按照JML直接实现,在互测的限制下造出极端情况数据便可以hack出bug

Network扩展

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

显然Advertiser、Producer以及Customer都实现Person接口,其中Advertiser需要新增属性表示发送广告的id,Producer需要新增属性表示生产产品的id,Customer新增属性表示自身偏好产品的id

JML实现规格如下

// 设置/增加消费者偏好
/*@ public normal_behavior
     @ requires @ signals (EqualPreferenceIdException e) (\exists int i; 0 <= i && i < customers.length;
     @         customers[i].getId() == id1); && (\exists int i; 0 <= i && i < Preferences.length;
     @           preferences[i].getId() == id2) && !getCustomer(id1).hasPreference(getPreference(id2));
     @ assignable getCustomer(id1).preferences;
     @ ensures (\forall Preference i; \old(getCustomer(id1).hasPreference(i));
     @         getCustomer(id1).hasPreference(i));
     @ ensures \old(getCustomer(id1).preferences.length) == getCustomer(id1).preferences.length - 1;
     @ ensures getCustomer(id1).hasPreference(getPreference(id2));
     @ also
     @ public exceptional_behavior
     @ signals (CustomerIdNotFoundException e) !(\exists int i; 0 <= i && i < customers.length;
     @         customers[i].getId() == id1);
     @ signals (PreferenceIdException e) (\exists int i; 0 <= i && i < customers.length;
     @         customers[i].getId() == id1) && !(\exists int i; 0 <= i && i < Preferences.length;
     @           preferences[i].getId() == id2);
     @ signals (EqualPreferenceIdException e) (\exists int i; 0 <= i && i < customers.length;
     @         customers[i].getId() == id1); && (\exists int i; 0 <= i && i < Preferences.length;
     @           preferences[i].getId() == id2) && getCustomer(id1).hasPreference(getPreference(id2));
     @*/
   public void addPreferenceToCustomer(int id1, int id2) throws CustomerIdNotFoundException,
           PreferenceIdException, EqualPreferenceIdException;

// 发送广告
   /*@ public normal_behavior
     @ requires (\exists int i; 0 <= i && i < customers.length;
     @         customers[i].getId() == id1) && (\exists int i; 0 <= i && i < getAdvertiser(id1).advertisements.length;
     @           getAdvertiser(id1).advertisements[i].getId() == id2);
     @ assignable getAdvertiser(id1).advertisements
     @ ensures (\forall Advertisement i; \old(getAdvertiser(id1).hasAdvertisement(i));
     @         getAdvertiser(id1).hasAdvertisement);
     @ ensures \old(getPerson(id1).advertisements.length) == getPerson(id1).advertisements.length - 1;
     @ ensures getAdvertiser(id1).hasAdvertisement(getAdvertisement(id2));
     @ also
     @ public exceptional_behavior
     @ signals (AdvertiserIdNotFoundException e) !(\exists int i; 0 <= i && i < advertisers.length;
     @         advertisers[i].getId() == id1);
     @ signals (AdvertisementIdException e) (\exists int i; 0 <= i && i < customers.length;
     @         customers[i].getId() == id1) && !(\exists int i; 0 <= i && i < getAdvertiser(id1).advertisements.length;
     @           getAdvertiser(id1).advertisements[i].getId() == id2);
     @*/
   public void sendAdvertisement(int id1, int id2) throws AdvertiserIdNotFoundException,
           AdvertisementIdException;

// 查询销售额
/*@ public normal_behavior
    @ requires (\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1) && (\exists int i; 0 <= i && i < getProducer(id1).products.length; getProducer(id1).products[i].getId() == id2);
    @ \results == getProducer(id1).getProductValiue(id2)
    @ also
    @ public exceptional_behavior
    @ signals (ProducerIdNotFoundException e) !(\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1)
    @ public exceptional_behavior
    @ signals (ProductIdNotFoundException e) (\exists int i; 0 <= i && i < producers.length; producers[i].getId() == id1) && !(\exists int i; 0 <= i && i < getProducer(id1).products.length; getProducer(id1).products[i].getId() == id2);
*/
public /*@ pure @*/ int queryValue(int id1, id2) throws ProducerIdNotFoundException, ProductIdNotFoundException;

学习体会

契约式编程

介绍

契约式编程是编程的一种方法,是一种对软件系统中的元素之间相互合作以及“责任”与“权利”的比喻,来自于现实生活中”合同“与”契约“的概念:

  • 供应商必须提供某种产品(责任),并且他有权期望客户已经付款(权利)。

  • 客户必须付款(责任),并且有权得到产品(权利)。

  • 契约双方必须履行那些对所有契约都有效的责任,如法律和规定等。

同理,在面向对象程序设计中一个类的函数提供了某种功能,那么它要:

  • 期望所有调用它的客户模块都保证一定的进入条件:这就是函数的先验条件,这样它就无需处理不满足先验条件的情况

  • 保证退出时给出特定的属性:这就是函数的后验条件的义务

  • 在进入时假定,并在退出时保持一些特定的属性:由不变条件满足

历史

契约式编程最早由伯兰特·迈耶于1986年提出, 他设计了EIffel编程语言来实现这种程序设计方法,并在《面向对象软件建构》(Object-Oriented Software Construction)一书中,又提出两个后继版本

核心思想

霍尔逻辑

采用霍尔三元组来描述一段代码的执行如何改变计算机的状态:{P} C {Q} ,其中P和Q是断言,C是命令, P 叫做 前条件Q 叫做 后条件 。断言是谓词逻辑的公式,这个三元组在直觉上读做:只要 PC 执行前的状态下成立,则在执行之后 Q 也成立

形式验证

根据某个或某些形式规范或属性,使用数学的方法证明其正确性或者非正确性

软件测试无法证明系统不存在缺陷或严格符合一定的属性,只有形式化验证的过程才可以证明。

面向对象中的应用

面向对象主要采用模块化、层次化的思想,种种方法仅对外保留一个接口,而不关心内部实现细节。故在面向对象中引入契约式编程后可以更加明确每个方法的责任界限。例,当参数不在设计范围内导致bug时,可以寻求调用者要求其按照契约进行设计。这样可以快速定位错误、明确责任、有利于工程的维护

防御性编程

在查阅契约式编程相关资料时发现了一个经常一同出现的概念——防御性编程,在此简单介绍:

防御性编程式指,通过在内部兼容处理来自外部的错误以保证, 对程序的不可预见的使用,不会造成程序功能上的损坏

具体而言,程序会在内部通过断言、抛出异常等方式来兼容处理来自外部的错误,好处在于:

  • 提高工程质量——减少bug和问题

  • 提高源码可读性—— 源码应该变得可读且可理解,并且能经受代码审计

  • 让软件能通过预期的行为来处理不可预期的用户操作

但过度的防御性编程可能会预防不可能会发生的错误,这样将导致运行时间与维护的损耗。当源码中拥有过多异常捕捉和异常处理,这有可能导致结果不正确或者被隐藏

与契约式编程的区别在于,契约式编程会在不满足契约约束条件时直接撕毁契约终止程序;而防御性会在内部兼容处理错误。

将这两者结合使用,才能更有效地编写高质量的软件工程代码

JML剖析

语言介绍

JML(Java Modeling Language)是用于对JAVA程序进行规格化设计的一种语言,很好地规定了行为规范。JML不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。非常地利于开发。

一般而言,JML两种用法

  • 开展规格化设计:自然语言由于其逻辑、表述等问题容易给人造成歧义,这也是指导书、题面表述等一切自然语言表述类说明需要不断检查更新的原因。采用规格化设计后,交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格,更有利于实现

  • 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

语言用途

JML 是为契约性编程而生的,从它的原生语法来看,非常符合契约设计的理念:requires 描述先验条件,ensures 描述后验条件,old 描述副作用,exceptional_behavior 描述异常。

JML类似于离散数学建立起了一套命题逻辑与谓词逻辑的公理系统。测试只能保证测不出来bug,而从理论上来说,通过 JML 这种语言,可以规范化编程,将编程变为一种可以形式化验证、消除歧义并进行自动的分析和推导的过程,让 bug 真正不复存在。

语法

注: 很多jml规格看名称就很容易猜出意思

注释结构

JML以javadoc注释的方式表示,每行开头以@开头。同时还分为行注释和块注释。

原子表达式

常用关键词含义作用
requires 前置条件 规定方法执行前的状态所必须满足的条件
ensures 后置条件 规定方法执行后的状态所必须满足的条件
assignable 副作用范围 规定方法所能够修改的变量
normal_behavior 正常行为 规定方法的正常行为
exceptional_behavior 异常行为 规定方法的异常行为
also 条件分支 标记各种正常行为与异常行为的分界
pure 纯净方法 标记方法不修改当前状态,可被其他方法引用
invariant 不变式 规定所有方法执行前后都需要满足的状态
constraint 状态变化约束 对前序可见状态和当前可见状态的关系进行约束
signals 条件异常抛出 当满足前置条件时抛出特定异常
signals_only 直接异常抛出 只要满足前置条件就抛出相应的异常
\old 前置原值 方法执行前变量的值
\result 结果返回值 规定方法的返回值

量化表达式

表达式描述
\forall 全称量词,表示范围内所有的元素均满足条件
\exists 存在量词,表示在范围内存在元素均满足条件
\sum 返回给定范围内的表达式的和
\product 返回给定范围内的表达式的连乘结果
\max 返回给定范围内的表达式的最大值
\min 返回给定范围内的表达式的最小值

上面的许多表达式需要遍历变量,一般变量用i,j,k表示,如:

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

操作符:

常用逻辑量化符含义作用
== 相等 表示两个变量相等
&& 规定两个条件同时满足
|| 规定两个条件至少有一个满足
! 表示不满足此表达式
> 大于 变量满足左大于右
>= 大于等于 变量满足左大于等于右
< 小于 变量满足左小于右
<= 小于等于 变量满足左小于等于右
+ 变量相加
- 变量相减
<==> 等价 左右两表达式同时满足或不满足
==> 蕴含 当做表达式满足时有表达式必满足
\sum 求和 返回给定范围内表达式的和
\max 最大值 返回给定范围内表达式的最大值
\min 最小值 返回给定范围内表达式的最小值
\forall 全称量词 对所有的元素都满足条件
\exists 特称量词 存在某元素满足条件
\nothing 空集 不包含任何变量元素
\everything 全集 包含所有变量元素
方法规格

前置条件

主要通过requires子句。使用requires P语句,规定P对应的应当为真

后置条件

主要通过ensures子句。使用ensures P语句,规定P对应的应当为真。

副作用范围限定

一般使用关键词assignablemodified,表示其后的可修改。

其他

  • 纯粹查询方法(/*@ pure @ */)

  • public normal_behavior 为正常功能

  • public exceptional_behavior为异常行为

  • signals (***Exception e) b_expr; 表示b_expr成立时抛出异常Exception

  • 注意,normal_behaviorexceptional_behavior 不能有任何交集(往往逻辑上互斥)

其他规格设计语言

作业体会

JML摒弃了自然语言存在的种种缺陷,使得编程有了清晰的严格的依据。但实际上我们不一定要严格按照JML描述的遍历方法来实现,例如三次作业中我们使用了各种图论算法来简化复杂度等。归结起来,JML只规定了我们要实现的功能,但未局限实现的方法,我们依然可以在满足规格的基础上利用算法来简化。

此外,本单元体现了很好的架构与设计模式,各种功能的查询均在顶层network中进行,再具体到下层的person、group等结构中实现,体现了代理模式、外观模式等结构。这也为我第四单元的UML解析器架构设计提供了一个很好的参考。

最后,感谢老师和助教的辛苦付出以及提供评测机与对拍支持的同学们

posted on 2022-06-06 14:47  继续宠爱  阅读(19)  评论(0编辑  收藏  举报