BUAA OO第三单元总结

前言

本单元的代码任务集中在了学习JML的使用,并根据所给JML实现相应的方法和类。

契约式编程、防御式编程与进攻式编程

契约式编程

契约式编程要求我们在「前提条件」、「后继条件」和「不变量条件」进行契约的检查。类似的,例如检查参数,一旦参数不对,当即撕毁契约。

比如后端的方法因为传入的参数不在设计范围内而导致错误,这时就可以去找前端调用方,要求前端按照前置的设计来进行调用。

约束对象:调用者与被调用者

防御式编程

“人类都是不安全、不值得信任的,所有的人,都会犯错误。”而你写的代码,应该考虑到所有可能发生的错误,让你的程序不会因为他人的错误而发生错误。

思路1:运行前检查参数是否合规,不合规则报错

思路2:对不合规的参数提供缺省值

约束对象:被调用者

进攻式编程

认为是防御式编程的分支,不同的是,进攻式编程让错误明显地存在。

进攻式编程针对的两种错误:

可预期错误(Expectable errors)

  1. 无效的用户输入(顾客走进酒吧要了一杯null)

  2. 内存耗尽(数组用完了)

  3. 硬件出故障(突然断网)

可预防错误(Preventable errors)

  1. 函数参数不合法(传进来一个null参数)

  2. 值超出预定范围(比如爆int)

  3. 返回值异常(比如返回值不在switch的case中)

体现进攻式编程的操作:

  1. 把数组的大小开得刚刚好

  2. 不初始化数组,或者初始化成非0的值

  3. 对所有可能错误的参数都进行检查,出现异常直接throw

  4. 不对不合规的参数提供缺省值,而是直接报错(区别于防御式编程)

图模型构建与维护策略

图模型的构建

由于JML的规格已经给定,其实自行发挥的空间是比较小的。观察别人的代码后,发现有人维护了JML规格以外的类去处理一些功能。但由于实际上,

由于每次架构都是迭代的增量开发,三次作业当中并未重构,因此,这里直接展示第三次作业后的类图:

理解java对于类的引用的原理,会发现其所有的引用使用的都是指针,因此,在发送消息这一功能的实现当中,递归下降地调用子类的子函数,和在Network中处理,性能上是差不多的。

由于编码简单,笔者在类似的问题中都选择了在MyNetwork类中统一完成,优点是所有需要用到的变量都已经被放在了MyNetwork,互相引用非常方便。缺点就是复杂度会集中在MyNetwork当中,复杂度的平衡就控制得比较不好。

性能问题和修复情况

性能优化策略

存储中间变量

例如,求取GroupPerson年龄的平均值,一般的做法如下:

    @Override
    public int getAgeMean() {
        int sum = 0;
        for (Person p : people) {
            sum += p.getAge();
        }
        return people.size() == 0 ? 0 : (sum / people.size());
    }

虽然取年龄的平均值并不是一个复杂度很高的指令,但这里面其实是有优化空间的,比如小组中人的年龄的平方和是可以提前存好的,最终的的复杂度可以简化成:

    @Override
    public int getAgeMean() {
        return (totalAgeSquare - 2 * mean * totalAge + mean * mean * people.size()) / people.size();
    }

无底线地使用HashMap

观察所有待实现的方法,将所有本为数组的元素都转变为一个或多个“从检索到被检索元素”的HashMap,会让后续的遍历、筛选、检索变得易于实现,且性能良好。

如我在Network类中,按照以下的方式实现了所需的几个属性。

    /*@ public instance model non_null Person[] people;
      @ public instance model non_null Group[] groups;
      @ public instance model non_null Message[] messages;
      @ public instance model non_null int[] emojiIdList;
      @ public instance model non_null int[] emojiHeatList;
      @*/
    private HashMap<Integer, Person> id2person = new HashMap<>();
    private HashMap<Person, Integer> person2id = new HashMap<>();
    private HashMap<Integer, Group> id2group = new HashMap<>();
    private HashMap<Integer, Message> id2message = new HashMap<>();
    private HashMap<Integer, Integer> emojiId2heat = new HashMap<>();
    private HashMap<Integer, HashSet<Integer>> emojiId2messages = new HashMap<>();

于是乎,所有的查询操作都可以通过O(1)的方法完成,示例如下:

    @Override
    public Message getMessage(int id) {
        return id2message.get(id);
    }

但是这也就意味着,迭代的时候,增删元素务必记住要处理所有hashmap的增删。这一点非常容易出错,这个在下文的bug修复中有提到。

算法的优化

这一点在第一次作业方法queryBlockSum的实现中体现的最为明显。其本质是一个判断连通图个数的问题。如果使用深搜的算法,复杂度就会过高;最佳策略应该是使用并查集的算法。

bug修复

第一次作业

无,比较顺利。

第二次作业

BUG1:qgav的计算方式出错

JML格式是这样给出的

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

由于java中int类型的整除问题,所以是先加还是先除会是一个影响结果的问题。写的时候看错了括号的范围而误写成了先加再除。

BUG2:delPerson未处理用于处理新增指令的HashMap

MyGroup中,为新增的指令建立了一个专用的HashMap<Integer, MyPerson>,用来直接找到发红包的人。但是在为Group删除人的时候忘记处理这个HashMap导致出错。

第三次作业

BUG1:对Dijkstra的理解出错

不细说了,就搞错知识点了。

BUG2:遍历删除出错

老生常谈的bug,即以下的写法是错误的

for(Object o : list){
    if(dosomething(o)){
        list.remove(o);
    }
}

严格使用ArrayList的下标进行遍历,或使用removeif语句可以避免这个问题。

Network拓展

要求分析

假设出现了几种不同的Person

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

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

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

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

分析可知最主要的三个业务为:偏好设置、广告发送、商品购买。

实现代码

偏好设置

    /*@ public normal_behavior
      @ requires contains(personId);
      @ requires getPerson(personId) instanceof Customer;
      @ assignable getPerson(personId);
      @ requires containsProduct(productId);
      @ ensures getPerson(personId).isFavorable(productId) == true;
      @ also
      @ public normal_behavior
      @ requires contains(personId);
      @ requires !(getPerson(personId) instanceof Customer);
      @ assignable \nothing;
      */
    public /*@ pure @*/void setPreference(int personId, int productId);

考虑到可能重复设置偏好,应该也可以被包容为一个正确的操作,因此,前置条件不需要要求此前该Customer对这一商品出于非偏好的状态。

广告发送

    /*@ public normal_behavior
      @ requires containsAdvertisement(id);
      @ assignable advertisements;
      @ ensures !containsAdvertisement(id) && advertisements.length == \old(advertisements.length) - 1;
      @ ensures (\exists int i; 0 <= i && i < \old(advertisements.length); \old(advertisements[i].getId()) == id);
      @ ensures (\forall int j; 0 <= j && j < advertisements.length;(\exists int i; 0 <= i && i < \old(advertisements.length); advertisements[j].equals(\old(advertisements[i]))));
      @ ensures (\forall int i; 0 <= i && i < people.length; people[i].isFavorable(id) ==> (\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements.length == \old(people[i].advertisements.length) + 1) &&  people[i].advertisements[j] == id) );
      @ ensures (\forall int i; 0 <= i && i < people.length; !people[i].isFavorable(id) ==> !(\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements[j] == id)) &&  people[i].advertisements.length == \old(people[i].advertisements.length));
      @ ensures (\forall int i; 0 <= i && i < people.length; (\forall int j; 0 <= j < \old(people[i].advertisements.length)(\exists int k; 0 <= k < people[i].advertisements.length;  \old(people[i].advertisements[j]) == people[i].advertisements[k])));
      @*/
    public void sendAdvertisement(int id);

考虑到Customer只会消费自己收到广告,且此时对其有篇好的商品,故在Customer中设置一个广告列表是有必要的,JML中也是按照这种思路进行描述的。

商品购买

    /*@ public normal_behavior
      @ requires contains(personId);
      @ requires getPerson(personId) instanceof Customer;
      @ requires containsProduct(productId);
      @ ensures getPerson(personId).isFavorable(productId) == true;
      @ ensures (\exists int cnt; cnt == (\sum int i;0<=i && i< \old(getPerson(personId).advertisements.length) && \old(getPerson(personId).advertisements[i].getId() == productId);1);getPerson(personId).advertisements.length == \old(getPerson(personId).advertisements.length) - cnt);
      @ also
      @ public normal_behavior
      @ requires !(getPerson(personId) instanceof Customer);
      @ assignable \nothing;
      @*/
    public /*@ pure @*/void buyProduct(int personId, int productId);

考虑到一个商品,Customer也许会重复地收到它的广告,这里设定:用户买下产品后,他手中的关于该商品的广告全部失效。

个人小发现:有关测试中可能算是遗漏的重要情况

在第三次作业Network类的addMessage方法中,判断message相同的方法是这样的。

      @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message));

其采用的是调用messages[i].equals(message)的方法。

而观察Message.equals方法

    /*@ also
      @ public normal_behavior
      @ requires obj != null && obj instanceof Message;
      @ assignable \nothing;
      @ ensures \result == (((Message) obj).getId() == id);
      @ also
      @ public normal_behavior
      @ requires obj == null || !(obj instanceof Message);
      @ assignable \nothing;
      @ ensures \result == false;
      @*/
    public /*@ pure @*/ boolean equals(Object obj);

会发现如果相比较的其中一个元素为null,则返回false。

然而大部分人判断Message相同的方法,是建立一个HashMap或信息ID的HashSet。实践发现,当HashMapHashSet当中存在null元素时,调用contains(null)containsKey(null)将会返回true,与JML中的行为不符。

不过由于本次作业保证了不存在为nullMessage,故这个遗漏并不会影响成绩,但个人认为这作为一个考察点。

学习体会

这一单元如果仅仅为了通过测试的话,只需做一个无情的JML翻译机器就可以了。但是如果能够真正掌握JML的撰写方法和撰写规律,才能有更明显的收获。

本单元总体难度比之前低,但是要保证正确,思维的严谨性和视力的要求是很高的。到这一章的时候似乎大多数同学都已经用上了自己的评测机,我很惭愧到现在都还没有做出来,希望下一单元能够弥补这一缺憾。

posted @ 2022-06-06 14:14  wooood  阅读(38)  评论(0编辑  收藏  举报