OO - 第三单元总结 - 博客作业- 79066020 路凯
单元题目介绍和说明
在第三单元中,我们开始学习JML(Java建模语言)及其在开发和测试社交网络中的应用。本单元的主要重点是理解和实现使用JML编写的精确软件规范,并确保我们的代码严格遵循这些规范。在每个作业中,我们都有一个官方包,其中包含一个处理数据输入的Runner类,以及我们需要实现的Person、Network和Tag等类的接口(我们命名为MyPerson、MyNetwork、MyTag等)。我们还需要处理各种类型的异常。第一个作业相对简单,只要求我们实现Network和Person类。第二个作业引入了Tag类,第三个作业增加了Message类及其派生类,包括RedEnvelopeMessage和NoticeMessage。下面,我将详细解释本单元的测试、数据处理、架构设计和性能改进策略。
单元的测试过程
代码开发中测试的重要性
测试是软件开发中的关键部分。它确保代码按预期运行,符合需求,并且没有漏洞。有效的测试提高了软件的可靠性、性能和安全性,对于交付高质量的应用程序至关重要。在我们的OO课程中,测试非常重要,这样我们可以确保代码在提交平台之前没有错误并且正常工作。强有力的测试还能确保我们获得更好的成绩。
JML和测试的介绍
JML(Java建模语言)是一种用于定义Java模块行为的规范语言。通过使用JML,我们可以指定类和方法的前置条件、后置条件和不变量。这对于团队协作非常方便;首先可以编写JML规范,然后不同的人可以分别实现不同的类,确保它们遵循该规范,这样我们就可以确保整个代码的协同工作。
让我们看看第三单元中的一个JML规范:
// An highlighted block
/*@ public normal_behavior
@ requires containsTag(id);
@ ensures (\exists int i; 0 <= i && i < tags.length; tags[i].getId() == id &&
@ \result == tags[i]);
@ also
@ public normal_behavior
@ requires !containsTag(id);
@ ensures \result == null;
@*/
public /*@ pure @*/ Tag getTag(int id);
这个JML规范说明,如果tags数组中存在指定id的标签,方法getTag应该返回该标签。否则,它应该返回null。requires子句指定了前置条件,即对于第一个行为,方法containsTag(id)必须返回true,而对于第二个行为则必须返回false。ensures子句指定了后置条件,即如果存在指定id的标签,结果将是该标签,否则结果将是null。
使用JML,我们甚至可以进行TDD(测试驱动开发)。由于规范作为合同,我们可以在编写实际实现之前根据这些合同编写测试。这有助于确保代码从一开始就符合定义的规范。
例如,下面是Network的JML要求:
public interface Network {
/*@ public instance model non_null Person[] persons;
@ public instance model non_null Message[] messages;
@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@*/
这些JML规范定义了四个模型数组来表示Network接口的不同数据。所有数组都使用了public instance model声明,并且使用non_null关键字保证它们及其元素不能为null。通过这些定义,Network接口保证了在实现和使用过程中对象数据的一致性和完整性。不仅TDD与JML结合更加方便,而且团队协作也更加高效。因为我们已经有了类的规范,如果其他人正在开发一个将与Network类交互的类,他们已经知道使用的数据类型,并且在这种情况下,它们都是non_null的。
测试类型及其重要性
- 黑盒测试: 种软件测试方法,测试人员不知道被测试项目的内部结构/设计/实现。只测试外部设计和结构。
- 白盒测试: 种测试技术,其中测试人员了解软件的内部工作原理,并可以测试单个代码片段、算法和方法。
- 单元测试: 一个代码块,用于验证较小的、独立的应用程序代码块(通常是函数或方法)的准确性。
- 功能测试: 种测试,旨在确定每个应用程序功能是否按照软件要求运行。将每个功能与相应的要求进行比较,以确定其输出是否符合最终用户的期望。
- 集成测试: 种软件测试,其中将软件应用程序的不同单元、模块或组件作为组合实体进行测试
- 压力测试: 评估软件在极端条件下的性能,识别突破点,确保其在高负载下的稳定性。
- 回归测试: 软件开发周期中的一种测试,在每次更改后运行,以确保更改不会引入意外中断。
第三单元的架构设计
在这个单元中,主要的挑战是处理JML(Java建模语言)规范中指定的数据类型。这些类型通常不是最方便或性能最优的,但由于它们在JML中被明确规定了,所以我们不能更改它们。
例如,在我的例子中,我添加了一个HashMap<Integer, Person>来与Network类中的Person数组同步。这使得一些方法更容易实现。然而,我和许多同学都意识到,由于作业的截止日期非常紧迫,最好的策略是先以最简单的方式实现这些方法,然后如果时间允许,再对它们进行重构以提高性能。对于我来说,这种方法尤其重要,特别是考虑到我在前两个单元中遇到的困难,以确保我能通过这个单元的测试。
以下是我如何实现Person和Network接口的一个例子:
public class MyNetwork implements Network {
/*@ public instance model non_null Person[] persons;
@ public instance model non_null Message[] messages;
@ public instance model non_null int[] emojiIdList;
@ public instance model non_null int[] emojiHeatList;
@*/
private Person [] persons = new Person[0];
private Message [] messages = new Message[0];
private int [] emojiIds = new int[0];
private int [] emojiHeatList = new int[0];
private final Map<Integer, Person> personMap = new HashMap<>();
private int tripleRelationCounter;
下面是我基本Exception的处理方法:
public class MyEmojiIdNotFoundException extends EmojiIdNotFoundException {
private int id;
private static int staticCounter = 0;
private static HashMap<Integer, Integer> EqualCountPerId = new HashMap<>();
public MyEmojiIdNotFoundException(int id) {
staticCounter++;
this.id = id;
EqualCountPerId.put(id, EqualCountPerId.getOrDefault(id, 0) + 1);
}
@Override
public void print() {
System.out.println("einf-" + staticCounter + ", " + id + "-" + EqualCountPerId.get(id));
}
}
在这中exception类,我通过构造函数创建对象,并使用EqualCountPerId的HashMap来控制每个ID异常的数量,同时还使用一个static int staticCounter计数器来获取同类型异常的发生次数。
下面是Message类的结构, 官方包的Message, NoticeMessage,EmojiMessage和RedEnvelopeMessage的结构上我实现了MyMessage,MyNoticeMessage,MyEmojiMessage和MyRedEnvelopeMessage。 这里最大的挑战是处理tag的Message(算群消息)。
@Override
public void sendMessage(int id)
throws RelationNotFoundException, MessageIdNotFoundException, TagIdNotFoundException {
ValidationUtils.checkMessageExists(id, messages);
Message message = getMessage(id);
MyPerson person1 = (MyPerson) message.getPerson1();
if (message.getType() == 0) {
MyPerson person2 = (MyPerson) message.getPerson2();
ValidationUtils.areLinked(getPerson(person1.getId()),
getPerson(person2.getId()));
processType0Message(message, person1, person2);
} else {
Tag tag = message.getTag();
ValidationUtils.containsTag(tag.getId(), person1);
processType1Message(message, person1, tag);
}
deleteMessage(id);
}
在我的sendMessage方法中,我通过多个processMessage方法单独处理每种类型的消息。我原本打算将所有processMessage方法分开放在另一个类中,但在实现这个想法的过程中,代码变得越来越复杂,最后因为时间紧迫,未能完成这个计划。
性能问题及其修复情况
在这三个作业的过程中性能出问题的主要是: queryBlockSum, queryTripleSum, queryCoupleSum三个方法。 我第一次实现了这些方法有约了O(n^3)的时间复杂度, 但是因为当时最怕的是代码提交的ddl, 先按最简答的方法做, 再修改。
/*@ ensures \result ==
@ (\sum int i; 0 <= i && i < persons.length &&
@ (\forall int j; 0 <= j && j < i; !isCircle(persons[i].getId(), persons[j].getId()));
@ 1);
@*/
public /*@ pure @*/ int queryBlockSum();
/*@ ensures \result ==
@ (\sum int i; 0 <= i && i < persons.length;
@ (\sum int j; i < j && j < persons.length;
@ (\sum int k; j < k && k < persons.length
@ && getPerson(persons[i].getId()).isLinked(getPerson(persons[j].getId()))
@ && getPerson(persons[j].getId()).isLinked(getPerson(persons[k].getId()))
@ && getPerson(persons[k].getId()).isLinked(getPerson(persons[i].getId()));
@ 1)));
@*/
public /*@ pure @*/ int queryTripleSum();
/*@ ensures \result ==
@ (\sum int i, j; 0 <= i && i < j && j < persons.length
@ && persons[i].acquaintance.length > 0 && queryBestAcquaintance(persons[i].getId()) == persons[j].getId()
@ && persons[j].acquaintance.length > 0 && queryBestAcquaintance(persons[j].getId()) == persons[i].getId();
@ 1);
@*/
public /*@ pure @*/ int queryCoupleSum();
我在MyNetwork加了一个private int tripleRelationCounter, 然后通过了了一个在updateTripleSumOnAdd同步tripleRelationCounter, 需要返回数据的时候直接返回tripleRelationCounter。 queryBlockSum我最终通过了UnionFind实现了, 在性能问题有很大的帮助。其他方法我还是通过了一个和Array同步的int Id和Person的Hash Map减少时间复杂度。
private void updateTripleSumOnAdd(MyPerson person1, MyPerson person2) {
Set<Person> commonAcquaintances = new HashSet<>(Arrays.asList(person1.getAcquaintances()));
commonAcquaintances.retainAll(Arrays.asList(person2.getAcquaintances()));
for (Person common : commonAcquaintances) {
MyPerson commonPerson = (MyPerson) common;
if (commonPerson.isLinked(person1) && commonPerson.isLinked(person2)) {
tripleRelationCounter++; // Only increment if all three are mutually connected
}
}
}
总结和收获
在完成单元 1 和单元 2 的作业后,我发现自己的编码能力有了很大的提高。最初,我在处理复杂的代码逻辑和数据结构时遇到了很多困难,但通过不断的练习和努力,我逐渐掌握了这些技能。在这个单元中,尽管面临着紧迫的截止日期和繁重的任务,我学会了如何在有限的时间内有效地实现功能,并逐步优化代码以提高性能。例如,我在实现sendMessage方法时,原本打算将所有processMessage方法分开放在另一个类中,但由于时间紧迫和实现过程中的复杂性,我选择了更简洁的方案来完成任务。
此外,在这个单元中,我不仅学习了 JML 和测试的重要性,还学到了如何解决实际编码中的问题。 我相信,通过这些经验和积累的知识,我将在单元 4 中表现得更加出色,继续提高自己的编程能力和问题解决能力。