OO2022第三单元总结

OO2022第三单元总结

第三单元主要学习了契约式编程以及JML,根据作业提供的JML实现相应的接口来完成一定的任务。

JML只严格约束了方法和类的行为、对象可见状态的改变等,内部的实际实现是自由的。在本单元作业中,需要灵活使用一些算法(Dijkstra、Kruskal等)、数据结构(二叉堆、并查集等)或是利用一些公式变换(如方差的计算可以维护age的和,age平方的和)来提高性能。JML强调一个方法要做什么,而实际怎么做是由Java实现的。

感觉JML本身是一种比较好的设计,但是好像用的不是很多,配套的工具不是很完善,比如IDEA没有JML语法高亮(至少我没有找到相应插件),导致看括号眼花缭乱。可能有由JML自动生成JUnit测试的工具,但是配置有些麻烦,且可能不兼容,最后还是自己根据JML手写测试。

整体架构设计(第三次作业)

主体架构已经给出,而三次作业都是在此基础上添加功能,所以直接给出第三次作业完成后架构。

图模型构建

Edge类用于抽象图中的边,由于建模的是无向图,所以实际上在加边的时候会在两个Person中同时加边:

public void addRelation(MyPerson o, int value) {
        this.acquaintance.put(o, new Edge(this, o, value));
        o.acquaintance.put(this, new Edge(o, this, value));
  }

为了方便处理,Edge对象同时记录了起点和终点。

每个Person使用HashMap记录与自己关联的边,以提升查询效率。

private final HashMap<MyPerson, Edge> acquaintance = new HashMap<>();

为了处理qbs查询连通块数量指令以及实现isCircle(),使用了并查集,我采用的做法是建立一个UnionSet类来单独管理,用一个数组实现(其实可以直接每个Person对象维护一个指向根的引用,这样实现可能是写C的惯性),但是因为没有保证Person的id是连续的,所以在加入Person的时候,需要分配一个(从0开始)连续的编号。并查集只实现了路径压缩,没有按秩合并。

同时,单独实现的UnionSet可以在实现Kruskal算法时重用。

性能优化

除了求最短路,求最小生成树,连通块不能照着JML实现,而应该使用相应的较为高效的算法,以及为了处理大量的依据id找相应对象建立HashMap外,在group相关的查询指令上,需要实现相应的优化,否则容易超时。

求方差

需要查询一个GroupPeople年龄的方差。

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

注意到都是整型运算,存在精度损失,那么优化也必须保证精度与依照JML直接实现的精度一致。

\[Var(age) = \frac{\sum(x_i-\bar{x})}{n} = \frac{\sum x_i^2 - 2\sum x_i \bar{x} + \sum \bar{x}^2}{n} = \\ \frac{\sum x_i^2 - 2\bar{x}\sum{x_i} + n\bar{x}^2 }{n} = \\ (ageSum2 - 2 * avg * ageSum + people.size() * avg * avg) / people.size() \\ avg = getAgeMean() \]

不能化成\(E(X^2)-E(X)^2\)的形式,会有精度问题。由上式就可以维护ageSum2(年龄平方的和)以及ageSum快速计算年龄方差。

求边权和(getValueSum()

和求方差相似,也是采用了维护ValueSum的方式,在从组里面增加人和删除人的时候维护,看起来没有问题,然后强测就挂了。

Bug

不仅从组里增删人会改变ValueSumaddRelation()也会改变 ValueSum,所以在addRelation()时,需要向相关的Group发送消息,更新ValueSum

//MyNetwork.java
public void addRelation(int id1, int id2, int value)
    throws PersonIdNotFoundException, EqualRelationException {
    //......
    for (Group g :
         idMapGroup.values()) {
        ((MyGroup) g).relationAdded(myPerson, myPerson1, value);
    }
}
//MyGroup.java
public void relationAdded(MyPerson person1, MyPerson person2, int value) {
    if (people.contains(person1) && people.contains(person2)) {
        valueSum += value * 2;
    }
}

向组加入人和从组删除人时维护相关变量

//MyGroup.java
public void delPerson(Person person) {
    people.remove((MyPerson) person);
    ageSum -= person.getAge();
    ageSum2 -= (long) person.getAge() * person.getAge();
    for (MyPerson p :
         people) {
        Edge t = ((MyPerson) person).getAcquaintance().get(p);
        if (t != null) {
            valueSum -= 2 * t.getValue();
        }
    }
}
public void addPerson(Person person) {
    people.add((MyPerson) person);
    ageSum += person.getAge();
    ageSum2 += (long) person.getAge() * person.getAge();
    for (MyPerson p :
         people) {
        Edge t = ((MyPerson) person).getAcquaintance().get(p);
        if (t != null) {
            valueSum += 2 * t.getValue();
        }
    }
}

sendMessage()的优化

sendMessage()的JML中,规定了发送单对单消息时有把消息加入接收者最近接收消息列表的动作。

  @ 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)));

如果按照JML直接实现,每发送一条此类消息,需要把数组中的消息依次后移。为了提高效率,可以选择反过来,每发送一条消息把它加到数组末尾,输出时反着输出。

//MyPerson.java
public void addMessage(Message msg) {
    this.messages.add(msg);
}

public List<Message> getReceivedMessages() {
    List<Message> ret = new ArrayList<>();
    int n = Math.min(messages.size(), 4);
    int last = messages.size() - 1;
    for (int i = 0; i < n; i++) {
        ret.add(messages.get(last - i));
    }
    return ret;
}

更进一步,虽然有Person::getMessages()方法获取收到的所有消息,但是似乎没有被调用过,所以可能可以只维护收到的最近四条消息,但是这会违反JML的约束,我没有进行这个“优化”。

容易出现问题的地方

从容器中删除元素

在迭代过程中删除元素是一种常见的Bug原因。deleteColdEmoji()removeNoticeMessage()等方法都涉及从容器中删除元素,为了正确地实现删除操作,可以:

  • 先迭代一遍记录下要删除的元素,再依次删除元素。
  • 直接创建一个新的容器,把不删除的移过去,然后替换。
  • stream().filter()好像性能有问题,不敢用。
  • removeIf()

异常抛出的顺序

某些方法可能抛出多个异常,这时需要仔细阅读JML,确定抛出哪个异常的优先级最大。定义异常抛出顺序的JML一般有这样的形式:

@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@          !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
@          !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));

注意!containsMessage(id)containsMessage(id),这保证情况没有重叠。

测试

同时使用了JUnit测试和随机数据生成测试

基于JML编写JUnit测试

利用JUnit编写测试,将自己实现的类当作黑箱,假设只知道实现符合JML,调用方法操作对象并与预期结果相比较。如果在IDEA中选择“使用覆盖率运行”可以得到测试覆盖率信息,这可以保证不会有某个方法写出来后直到强测甚至没有调用过。

JUnit的使用比较好上手,且与IDEA很好地集成。除了一般的assertEqual()等断言,还可以通过assertThrows()断言此方法会抛出异常,可以测试异常的实现是否正确。

例如,

 assertThrows(MessageIdNotFoundException.class, ()->network.sendMessage(3));

例如对最短路的测试:

import com.oocourse.spec3.exceptions.EmojiIdNotFoundException;
import com.oocourse.spec3.exceptions.MessageIdNotFoundException;
import com.oocourse.spec3.main.Network;
import org.junit.Test;
import static org.junit.Assert.*;
public class ShortestPathTest {
    @Test
    public void functionTest1() throws Exception{
        Network network = new MyNetwork();
        for (int i = 0; i < 10; i++) {
            network.addPerson(new MyPerson(i,"robot"+i, 5));
        }
        network.addRelation(0,1,4);
        network.addRelation(0,2,1);
        network.addRelation(1,3,1);
        network.addRelation(2,3,2);
        network.addRelation(2,4,1);
        network.addRelation(3,4,10);
        network.addRelation(3,5,3);
        network.addRelation(4,5,10);
        network.addMessage(new MyMessage(0,5,network.getPerson(0),network.getPerson(5)));
        assertEquals(6, network.sendIndirectMessage(0));
        network.addMessage(new MyNoticeMessage(1, "233",network.getPerson(0),network.getPerson(1)));
        assertEquals(4,network.sendIndirectMessage(1));
        assertThrows(MessageIdNotFoundException.class, ()->network.sendIndirectMessage(2));
        assertEquals(8,network.querySocialValue(0));
        assertEquals(5,network.querySocialValue(5));
        assertEquals(3,network.querySocialValue(1));
        assertThrows(EmojiIdNotFoundException.class,
                ()->network.addMessage(new MyEmojiMessage(2,1,network.getPerson(5),network.getPerson(4))));
        network.storeEmojiId(1);
        network.addMessage(new MyEmojiMessage(2,1,network.getPerson(5),network.getPerson(4)));
        assertEquals(6,network.sendIndirectMessage(2));
        assertEquals(1,network.querySocialValue(4));
        assertEquals(6,network.querySocialValue(5));
        assertEquals(8,network.querySocialValue(0));
        network.addMessage(new MyRedEnvelopeMessage(3, 100, network.getPerson(2),network.getPerson(1)));
        assertEquals(3,network.sendIndirectMessage(3));
        assertEquals(-100,network.queryMoney(2));
        assertEquals(100,network.queryMoney(1));
        network.addMessage(new MyRedEnvelopeMessage(4, 100, network.getPerson(0),network.getPerson(6)));
        assertEquals(-1,network.sendIndirectMessage(4));

    }

基于JML手动编写JUnit测试还是带有不少的主观性,假设考虑的情况有疏漏或者本身没有读懂JML,那么可能难以测出Bug。感觉最理想的是能够基于JML自动生成测试,然而相关工具好像不太成熟。

基于随机数据生成器的测试

from random import randint, random
def gen_name()->str:
    s = ''
    for i in range(randint(5,10)):
        s += chr(ord('a')+randint(0,25))
    return s

def gen_gen_uid():
    cnt = -1
    def gen_uid()->int:
        nonlocal cnt
        cnt += 1
        return cnt 
    return gen_uid

gen_people_id = gen_gen_uid()
gen_msg_id = gen_gen_uid()
gen_group_id = gen_gen_uid()
gen_emoji_id = gen_gen_uid()

cmd_num = 20000

people_num = 50
group_num = 10
msg_num = 1000
emj_num = 20
out = []
for i in range(people_num):
    out.append(f'ap {gen_people_id()} {gen_name()} {randint(1,100)}')

for i in range(group_num):
    out.append(f'ag {gen_group_id()}')

for i in range(emj_num):
    out.append(f'sei {gen_emoji_id()}')
for i in range(msg_num):
    t = randint(1,4)
    tt = randint(0,1)
    if tt == 0:
        if t == 1:
            out.append(
                f'am {gen_msg_id()} {randint(1,100)} 0 {randint(0,people_num-1)} {randint(0,people_num-1)}')
        if t == 2:
            out.append(
                f'arem {gen_msg_id()} {randint(1,100)} 0 {randint(0,people_num-1)} {randint(0,people_num-1)}')  
        if t == 3:
            out.append(
                f'anm {gen_msg_id()} {gen_name()} 0 {randint(0,people_num-1)} {randint(0,people_num-1)}') 
        if t == 4:
            out.append(
                f'aem {gen_msg_id()} {randint(0,emj_num-1)} 0 {randint(0,people_num-1)} {randint(0,people_num-1)}')
    if tt == 1:
        if t == 1:
            out.append(
                f'am {gen_msg_id()} {randint(1,100)} 1 {randint(0,people_num-1)} {randint(0,group_num-1)}')
        if t == 2:
            out.append(
                f'arem {gen_msg_id()} {randint(1,100)} 1 {randint(0,people_num-1)} {randint(0,group_num-1)}')  
        if t == 3:
            out.append(
                f'anm {gen_msg_id()} {gen_name()} 1 {randint(0,people_num-1)} {randint(0,group_num-1)}') 
        if t == 4:
            out.append(
                f'aem {gen_msg_id()} {randint(0,emj_num-1)} 1 {randint(0,people_num-1)} {randint(0,group_num-1)}')   

for i in range(people_num):
    out.append(f'ar {randint(0,people_num-1)} {randint(0,people_num-1)} {randint(1,100)}') 

def pfid(x):
    return randint(0, x + 5)
def rndid(x):
    return randint(0, x * 2)
for i in range(cmd_num):
    t = randint(1,26)
    if t == 1:
        out.append(f'ap {rndid(people_num)} {gen_name()} {randint(1,100)}')
    if t == 2:
        out.append(f'ar {pfid(people_num)} {pfid(people_num)} {randint(1,100)}')
    if t == 3:
        out.append(f'qv {pfid(people_num)} {pfid(people_num)}')
    if t == 4:
        out.append('qps')
    if t == 5:
        out.append(f'qci {pfid(people_num)} {pfid(people_num)}')
    if t == 6:
        out.append(f'qbs')
    if t == 7:
        out.append(f'ag {rndid(group_num)}')
    if t == 8:
        out.append(f'atg {pfid(people_num)} {pfid(group_num)}')
    if t == 9:
        out.append(f'dfg {pfid(people_num)} {pfid(group_num)}')
    if t == 10:
        out.append(f'qgps {pfid(group_num)}')
    if t == 11:
        out.append(f'qgvs {pfid(group_num)}')
    if t == 12:
        out.append(f'qgav {pfid(group_num)}')    
    if t == 13:
        out.append(
            f'am {gen_msg_id()} {randint(1,100)} {randint(0,1)} {randint(0,people_num-1)} {randint(0,group_num-1)}'
        )
    if t == 14:
        out.append(
            f'sm {pfid(msg_num)}'
        )
    if t == 15:
        out.append(f'qsv {pfid(people_num)}')
    if t == 16:
        out.append(f'qrm {pfid(people_num)}')
    if t == 17:
        out.append(f'qlc {pfid(people_num)}')
    if t == 18:
        out.append(
            f'arem {gen_msg_id()} {randint(1,100)} {randint(0,1)} {randint(0,people_num-1)} {randint(0,group_num-1)}')  
    if t == 19:
        out.append(
            f'anm {gen_msg_id()} {gen_name()} {randint(0,1)} {randint(0,people_num-1)} {randint(0,group_num-1)}') 
    if t == 20:
        out.append(
            f'aem {gen_msg_id()} {randint(0,emj_num-1)} {randint(0,1)} {randint(0,people_num-1)} {randint(0,group_num-1)}')
    if t == 21:
        out.append(f'cn {pfid(people_num)}')
    if t == 22:
        out.append(f'sei {rndid(emj_num)}')
    if t == 23:
        out.append(f'qp {pfid(emj_num)}')
    if t == 24:
        out.append(f'dce {randint(0,2)}')
    if t == 25:
        out.append(f'qm {pfid(people_num)}')
    if t == 26:
        out.append(f'sim {pfid(msg_num)}')

with open('in.txt','w',encoding='utf-8') as f:
    for line in out:
        f.write(line+'\n')

一个简单的随机数据生成器却帮我找到了至少两个Bug,为了保证数据有效性,提前生成了一些保证有效的People,Group,Message,Emoji

Network扩展

添加的接口

添加以下继承自Message以及Person的接口,可以修改sendMessage()addMessage()Message相关方法,也可以另外实现市场营销专用的方法,基于之前实现的Message扩展实现使Network支持市场营销。

public interface AdvertisementMessage extends Message {
    /*@ public instance model non_null String good;
      @
      @*/
    public String getGood();
}
public interface OrderMessage extends Message {
    /*@ public instance model non_null String good;
      @ public instance model Customer customer;
      @*/
    public String getGood();
    public Customer getCustomer();
}

public interface PromoteMessage extends Message {
    /*@ public instance model non_null String good;
      @
      @*/
    public String getGood();
}
public interface Advertiser extends Person {
    /*@ public instance model non_null PromoteMessage[] promoteMessages;
      @
      @*/
    /*
     * 用于判断合法性,只有Producer发送了PromoteMessage,Advertiser才能发送相应AdvertisementMessage
     */
	public boolean containPromoteMessage(Message message);
}

Producer需要实现两个方法以实现查询某种商品的销售额和销售路径,销售路径的信息保存在OrderMessage中(Producer、advertiser、customer分别是谁),通过统计OrderMessage也可以获得某种商品的销售额。

public interface Producer extends Person {
    /*@  public instance model non_null OrderMessage[] receivedOrders;
      @*/
    /* @ public normal_behavior
       @ ensures \result == (\sum OrderMessage msg;
       @                       (\exists int i; 0 <= i && i < receivedOrders.length; receivedOrders[i] == msg) &&
       @                         msg.getGood() == good;
       @                        1
       @                     );
       @*/
    public /*@ pure @*/ int getSaleStatistic(String good);
    /*@ public normal_behavior
      @ ensures (\result.size() == receivedOrders.length) &&
      @           (\forall int i; 0 <= i && i < receivedOrders.length;
      @             receivedOrders[i] == \result.get(i));
      @
      @*/
    public /*@ pure @*/ List<OrderMessage> getReceivedOrders();
    public /*@ pure @*/ boolean containReceivedOrder(Message msg);
}

public interface Customer extends Person{
    /*@ public instance model non_null AdvertisementMessage[] advertisementMessages;
      @
      @*/
    public boolean containAdvertisementMessage(AdvertisementMessage advertisementMessage);
}

扩展Network

主要接口JML

生产商向广告商发送要推广的商品

//void promote(int id)
public normal_behavior
requires (\exists int i; 0 <= i && i < \old(messages.length); \old(messages[i]).getId() == producer &&
			\typeof(\old(messages[i])) == PromoteMessage.TYPE 
             &&
             (\exists int j; 0 <= j && j < people.length; people[j].getId() == \old(messages[i]).getPerson1() &&
                \typeof(people[j]) == Producer.TYPE) 
             &&
             (\exists int j; 0 <= j && j < people.length; people[j].getId() == \old(messages[i]).getPerson2() && 
                \typeof(people[j]) == Advertiser.TYPE)
		 );
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 int i; 0 <= i && i < \old(messages.length); \old(messages[i]).getId() == producer &&
			\typeof(\old(messages[i])) == PromoteMessage.TYPE 
             &&
             (\exists int j; 0 <= j && j < people.length; people[j].getId() == \old(messages[i]).getPerson2() && 
                \typeof(people[j]) == Advertiser.TYPE) &&
             people[j].containPromoteMessage(\old(messages[i]))
		 );
		 

把相应的广告推送给所有的顾客。

//void advertise(int id)
public normal_behavior
requires (\exists int i; 0 <= i && i < \old(messages.length); \old(messages[i]).getId() == producer &&
			\typeof(\old(messages[i])) == AdvertisementMessage.TYPE 
             &&
             (\exists int j; 0 <= j && j < people.length; people[j].getId() == \old(messages[i]).getPerson1() &&
                \typeof(people[j]) == Advertiser.TYPE) 
              &&
              (\exists int j; 0<= j && j < \old(messages[i]).getPerson1().promoteMessages.length;
              	\old(messages[i]).getPerson1().promoteMessages[j].getGood() == \old(messages[i]).getGood())
		 );
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 int i; 0 <= i && i < \old(messages.length); \old(messages[i]).getId() == id &&
			\typeof(\old(messages[i])) == AdvertisementMessage.TYPE && 
				(\forall int j;0 <= j && j < people.length && people[j] instanceof Customer; 										people[j].containAdvertisementMessage(\old(messages[i])));

顾客通过广告商向生产商发送订单。

// void order(int id)
public normal_behavior
requires (\exists int i; 0 <= i && i < \old(messages.length); \old(messages[i]).getId() == producer &&
			\typeof(\old(messages[i])) == OrderMessage.TYPE &&
           (\exists int j; 0<= j && j < \old(messages[i]).getPerson1().promoteMessages.length;
              	\old(messages[i]).getPerson1().promoteMessages[j].getGood() == \old(messages[i]).getGood()) &&
           (\exists int j; 0<= j && j < \old(messages[i]).getCustomer().advertisementMessages.length;
              	\old(messages[i]).getCustomer().advertisementMessages[j].getGood() == \old(messages[i]).getGood())
         );
 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 int i; 0 <= i && i < \old(messages.length); \old(messages[i]).getId() == id &&
			\typeof(\old(messages[i])) == OrderMessage.TYPE && 
				\old(messages[i]).getPerson2().containReceivedOrder(\old(messages[i])));

学习体会

本单元主要学习了契约式编程,了解了JML。JML相对自然语言有无二义性,能够明确描述方法和对象的行为的优点,所以在本单元的作业中,基本只需要保证实现完全符合JML(优化也需要保证从外部看行为与JML一致),就能保证不出问题。

在完成作业的过程中,也学习了关于Java异常处理和单元测试(JUnit)的内容。

JML如果有更完整的工具支持,应该可以发挥更大的作用,但是好像各个方面的支持都不是很成熟,而且JML显得稍微有些冗长,比如

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

\old(getMessage(id)).getPerson1().getSocialValue()被重复书写了多次(或许可以至少加上不可变的变量绑定,感觉可能也不会影响语义准确性),以及深层次嵌套的括号,都显著影响了可读性。

可能更主要的是学习DBC的思想,而不是只是去考虑特定的工具。

posted @ 2022-06-01 18:55  aaicy64  阅读(34)  评论(0编辑  收藏  举报