系列文章目录
哈工大软件构造lab1小结
哈工大软件构造lab2小结
文章目录
前言
这次实验难度比起上次更大,知识点也更加丰富。关注博主,带你深入浅出了解java。
提示:以下是本篇文章正文内容,下面案例可供参考
知识补充
1.ADT
ADT是本次实验中重点要求掌握的概念,我们结合实验内容对它进行理解。
ADT的概念:
ADT(抽象数据类型),强调“作用于数据上的操作”,程序员和用户无需关心数据如何具体存储的,只需设计/使用操作即可。
ADT是由操作定义的,与其内部如何实现无关。
然后是关于抽象类的操作方法(这个可能考)
1.构造器(Creator)。构造器将某一个(某一些)与被构造数据类型不同的数据类型的对象作为参数,构造某个数据类型的具体对象。
例如本次实验中的empty()
2.生产器(Producer)。生产器利用某一类型的数据对象构造出该类型的新的数据对象。(用已有该类型对象产生新对象)
本次实验中没有比较好的例子可以说明,但是注意:
mutator与producer类似,但区别就是mutator会改变fields的内容,而producer不会改变原类。
3.观察器(Observer)。观察器以某一类型的数据作为被观测对象,会返回一个不同数据类型的值。
例如本次实验中的sources(L target)、targets(L source)
4.变值器(Mutator)。变值器改变某个对象的属性。
例如本次实验中的add(L vertex)、set(L source, L target, int weight)、remove(L vertex)。
测试ADT的思路:
测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否满足spec;
测试observers:调用creators, producers, and mutators等方法产生或 改变对象,来看结果是否正确。
2.抛出异常
在java的异常处理机制中,我们要求在出现错误时,程序能够做到:
返回到一种安全状态,继续执行其他命令
允许用户保存工作结果,妥善地结束程序
一般的异常处理方式有两种,捕获异常和抛出异常。其中涉及了许多细节上的知识,不必过于深入地了解,只需要记住下面两点:
1.抛出异常是方法本身不进行处理这个异常需要调用方法的时候进行处理,捕获异常是在方法本身自己将异常进行处理。
2.为了明确指出一个方法不捕获某类异常,而让调用该方法的其他方法捕获该异常,可以在定义方法的时候,使用throws可选项,用以抛出该类异常。
throw语句可以主动抛出异常,比如:
throw new RuntimeException("not implemented");
3.接口
在第一次实验的知识点总结中,我们就提到了关于map接口、collection接口。由于在目前老师的讲授范围中没有接口的内容,我们只需作为简单科普了解:
接口使用 interface 关键字来声明,可以看做是一种特殊的抽象类,可以指定一个类必须做什么,而不是规定它如何去做。
接口必须通过类来实现(implements)它的抽象方法,然后再实例化类。类实现接口的关键字为implements。
本次实验就涉及了接口的设计和使用:
接口设计:
public interface Graph<L>
接口实现:
public class ConcreteVerticesGraph<L> implements Graph<L>
public class ConcreteEdgesGraph<L> implements Graph<L>
详细介绍见链接3
一、编写测试用例
充分掌握目标代码的功能是测试用例的编写基础,这样才能使得测试用例达到较高的测试覆盖度。
Graph的功能:graph实现的是构造一个带有标记顶点的可变加权有向图。
待测试的方法有:
empty()、add(L vertex)、set(L source, L target, int weight)、remove(L vertex)、vertices()、sources(L target)、targets(L source)
通过目标代码中给出的注释,我们能够了解到以上方法的功能,针对这些功能,我们给出与之对应的测试。
测试empty:
@Test
public void testInitialVerticesEmpty() {
// TODO you may use, change, or remove this test
assertEquals("expected new graph to have no vertices",
Collections.emptySet(), emptyInstance().vertices());
}
首先我们新建全局变量test1作为待测试的图:
Graph<String> test1 = emptyInstance();
然后对具体的方法测试:
具体的测试思路非常简单,根据方法的规约对输入划分等价类即可。之前在lab1知识点总结中提过,这里我们不加详细描述。
下面给出其中几个函数测试的代码,仅为便于理解:
//test whether the vertices are added correctly
@Test
public void testAdd(){
test1.add("a");
assertFalse(test1.add("a"));
assertTrue(test1.add("b"));
}
//test whether the vertices are showed correctly
@Test
public void testVertices(){
List<String> list = new LinkedList<>();
list.add("ac");
assertTrue(list.retainAll(test1.vertices()));
}
//test whether the sources are showed correctly
@Test
public void testSources(){
test1.set("d","a",1);
test1.set("b","c",2);
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
assertEquals(map, test1.sources("c"));
}
//test whether the sources are showed correctly
@Test
public void testTargets(){
test1.set("d","b",2);
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
assertEquals(map, test1.targets("d"));
}
二、实现Graph< String >
2.1. 实现ConcreteEdgesGraph
首先我们关注数据类型:
private final Set<String> vertices = new HashSet<>();
private final List<Edge> edges = new ArrayList<>();
为了构建可变加权有向图,题目要求我们使用一个顶点集和一个边的列表实现。
根据题意,我们首先应该完善Edge类:
// TODO constructor
String source ;
String target ;
int weight;
// TODO checkRep
// TODO methods
public Edge(String source,String target, int weight) {
this.source = source;
this.target = target;
this.weight = weight;
}
// TODO toString()
@Override
public String toString(){
return "(" + source.toString() + "," + target.toString() + ")" + ", " + weight;
}
这里我们介绍java中的this:
this关键字的意义被解释为“指向当前对象的引用”
this最关键的用法就是:
区分成员变量和局部变量,即把外界传入的变量和类内部的变量区分开,避免重名导致的赋值错误。
this其他的用法我们暂时用不到,这里不多介绍,有兴趣可以见链接1
然后根据规约写出对应的方法:
//这里我们直接利用add方法的返回特点即可满足要求
@Override public boolean add(String vertex) {
//throw new RuntimeException("not implemented");
return vertices.add(vertex);
}
//注意函数的效率问题,以及对非法输入的检查
@Override public int set(String source, String target, int weight) {
//throw new RuntimeException("not implemented");
if (weight < 0){
System.out.println("wrong weight!");
return -1;
}
if (source.equals(target)){
System.out.println("wrong edge!");
return -1;
}
if (vertices.contains(source) && vertices.contains(target)){
Edge edge = new Edge(source, target, weight);//目标边
int pre_weight = 0;
for (Edge edge1 : edges){
if (edge1.target.equals(target) && edge1.source.equals(source)){
pre_weight = edge1.weight;
if (weight == 0){//修改完了就返回
edges.remove(edge1);
return pre_weight;//删除边
}
edge1.weight = weight;//修改权值
return pre_weight;
}
}
edges.add(edge);//将新的边添加到边表中
return pre_weight;
}else {
vertices.add(target);
vertices.add(source);
Edge edge = new Edge(source, target, weight);
edges.add(edge);
return 0;
}
}
//使用removeIf方法可以代替写一个循环
@Override public boolean remove(String vertex) {
//throw new RuntimeException("not implemented");
edges.removeIf(edge1 -> edge1.source.equals(vertex) || edge1.target.equals(vertex));
return vertices.remove(vertex);
}
//直接返回即可
@Override public Set<String> vertices() {
//throw new RuntimeException("not implemented");
return vertices;
}
//遍历边表,查找与目标顶点对应的点,加入map中
@Override public Map<String, Integer> sources(String target) {
//throw new RuntimeException("not implemented");
Map<String, Integer> map = new HashMap<>();
for (Edge edge1 : edges){
if (edge1.target.equals(target)){
map.put(edge1.source,edge1.weight);
}
}
return map;
}
//同理,不多赘述
@Override public Map<L, Integer> targets(L source) {
//throw new RuntimeException("not implemented");
Map<L, Integer> map = new HashMap<>();
for (Edge<L> edge1 : edges){
if (edge1.source.equals(source)){
map.put(edge1.target,edge1.weight);
}
}
return map;
}
// TODO toString()
// TODO toString()
//由于需要多次修改对象值,我们使用可变类型StringBuilder使效率更高
@Override
public String toString() {
StringBuilder rep = new StringBuilder();
rep.append("Vertices : ").append(vertices).append("\n");
rep.append("Edges : [ ");
int i = 0;
for(; i < edges.size()-1; i++)
{
rep.append(edges.get(i).toString()).append(" , ");
}
rep.append(edges.get(i).toString()).append(" ]");
return rep.toString();
}
从上面的代码我们可以看出java良好的封装性使得编程更加轻松,我们只需要用一些简单的语句就能实现目标的功能。
下面我们介绍java中循环的用法:
java中循环和c语言一样,也有三种。其中while 以及 do while的用法和c语言完全相同,for循环与c语言中的for基本一样,但是java5加强了for:
(主要用于数组)
for(声明语句 : 表达式)
{
//代码句子
}
声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等
表达式:表达式是要访问的数组名,或者是返回值为数组的方法。
上面代码的意思就是从表达式的第一项开始遍历,直到遍历完整个表达式。
遗憾的是,java在for循环的加强中没有做到和python那样简便,假如我们想从数组的第二项开始往后遍历,就只能使用普通的for循环实现(当然你也可以改变原数组,但是从效率上来说是没必要的)
另外,java for循环提供一种简便的跳出循环机制:
aa: for (int i = 1; i <= 3; i++) {
bb: for (int j = 1; j <= 3; j++) {
if (i == 2 && j == 2) {
break aa;
}
System.out.println(i + " " + j);
}
}
//详见https://www.yiibai.com/java/java-for-loop.html
这和我们在c语言中使用 goto 实现大同小异,但是代码看起来就清爽很多。
2.2. 实现ConcreteVerticesGraph
同样的,我们关注数据结构:
private final List<Vertex> vertices = new ArrayList<>();
图在这里被表示成一个列表,那么我们使用邻接表来实现它:
String head;
Map<String,Integer> map ;
// TODO constructor
// TODO checkRep
// TODO methods
public Vertex(String head,Map<String, Integer> map){
this.head = head;
this.map = map;
}
然后是根据规约实现各种方法,上面我们已经实现了类似的步骤,下面的代码思想就很容易理解了。唯一需要注意的是要根据我们手中特殊的数据结构给出方法实现:
//添加节点时注意要新建一个map
@Override
public boolean add(String vertex) {
//throw new RuntimeException("not implemented");
for (Vertex ver1 : vertices){
if (ver1.head.equals(vertex)){
return false;
}
}
Vertex ver = new Vertex(vertex,new HashMap<String,Integer>());
return vertices.add(ver);
}
//由于添加新节点必然需要添加新的map,为了避免发生覆盖,我们需要多次分类讨论
@Override
public int set(String source, String target, int weight) {
//throw new RuntimeException("not implemented");
if (weight < 0){
System.out.println("wrong weight!");
return -1;
}
if (source.equals(target)){
System.out.println("wrong edge!");
return -1;
}
int pre_weight = 0;
for (Vertex ver1 : vertices){
if (ver1.head.equals(source)){
if (ver1.map.containsKey(target)){//边存在
pre_weight = ver1.map.get(target);
if (weight == 0){//删除
ver1.map.remove(target);
return pre_weight;
}
ver1.map.put(target,weight);//修改
return pre_weight;
}
ver1.map.put(target,weight);//添加
for (Vertex ver2 : vertices){
if (ver2.head.equals(target)){
return pre_weight;
}
}
Vertex ver = new Vertex(target,new HashMap<String,Integer>());
vertices.add(ver);
return pre_weight;
}
}
Vertex ver = new Vertex(source,new HashMap<String,Integer>());
ver.map.put(target,weight);
vertices.add(ver);
for (Vertex ver2 : vertices){
if (ver2.head.equals(target)){
return pre_weight;
}
}
Vertex ver1 = new Vertex(target,new HashMap<String,Integer>());
vertices.add(ver1);
return pre_weight;
}
//移出顶点不要忘记移除与之相连的边
@Override
public boolean remove(String vertex) {
//throw new RuntimeException("not implemented");
for (Vertex ver2 : vertices){
ver2.map.remove(vertex);
}
return vertices.remove(vertex);
}
//返回一个顶点表,只需要用一个循环把顶点内容存储到集合中
@Override
public Set<String> vertices() {
//throw new RuntimeException("not implemented");
Set<String> set = new HashSet<>();
for (Vertex ver1 : vertices){
set.add(ver1.head);
}
return set;
}
//用顶点表实现sources非常简单
@Override
public Map<String, Integer> sources(String target) {
//throw new RuntimeException("not implemented");
Map<String, Integer> map = new HashMap<>();
for (Vertex ver1 : vertices){
if (ver1.map.containsKey(target)){
map.put(ver1.head,ver1.map.get(target));
}
}
return map;
}
//原理同上,这里不多赘述
@Override
public Map<String, Integer> targets(String source) {
//throw new RuntimeException("not implemented");
Map<String, Integer> map = new HashMap<>();
for (Vertex ver1 : vertices){
if (ver1.head.equals(source)){
map.putAll(ver1.map);
}
}
return map;
}
//打印出图,按照读者喜好设计即可
// TODO toString()
@Override
public String toString(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("vertices :"+"[");
int i = 0;
for( ;i < vertices.size() - 1; i++)
{
stringBuilder.append(vertices.get(i).head).append(" , ");
}
stringBuilder.append(vertices.get(i).head).append("]\n").append("Edges:").append("[");
i=0;
for(; i < vertices.size() - 1; i++)
stringBuilder.append(vertices.get(i).toString()).append(" , ");
stringBuilder.append(vertices.get(vertices.size()-1).toString());
stringBuilder.append("]");
return stringBuilder.toString();
}
在作者的软件构造课程小结中我们曾提到:
java 中数据类型分为可变类型和不可变类型,从上面的代码中,你可能会提出疑问:为什么不使用String , 而要使用StringBuilder?这是因为String属于不可变类型,当我们改变它的属性时,就要删除原来的存储内容,并根据修改结果重新生成一份。当我们需要多次修改时,这就使得代码运行的效率大大降低。
而使用StringBuilder就不用担心这个问题了,它属于可变数据类型,每次修改时都会改变内容,而不改变指针,这样修改的效率更高。
三.实现泛型Graph
关于泛型:
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
关于泛型的知识不必过于深入,我们只需要知道:
定义泛型方法的规则:
所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔)。
一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
类型参数能被用来声明返回值类型。
泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
比如:
//单独使用泛型方法,而不是在泛型类中使用
public static < E > void printArray( E[] inputArray )...
//在泛型类中使用
public void add(T t)....
其余的细节可以见链接2
在本次实验中,我们使用泛型就比较简单,只需要把对应的String修改成指定的L、以及在修改数据类型之后修改对应的声明处(加上< L >)即可。由于实现相对简单,代码就不介绍了。
关于empty():
我们需要返回一个空的图,那么我们只需要按照下面的代码写就行。这里涉及到接口的实现,我们在知识补充中将进一步介绍。
return new ConcreteEdgesGraph<>();
四、诗意的散步
这部分内容是对我们前面建立的图的一种应用,要求我们根据给出的文本建立有向图,为输入的句子添加"bridge",从而达到“写诗”的效果。
建立图的过程根据给出的规约就能完成:
public GraphPoet(File corpus) throws IOException {
//throw new RuntimeException("not implemented");
FileReader fileReader = new FileReader(corpus);
BufferedReader bufferReader = new BufferedReader(fileReader);
String str = bufferReader.readLine();
while (str != null){
str = str.toLowerCase(Locale.ROOT);
String[] words;
words = str.split(" ");
int length = words.length;
int pre_weight;
for (int i = 0; i < length - 1; i++){
pre_weight = graph.set(words[i],words[i + 1], 0);//获得原来边的权值
pre_weight++;
graph.set(words[i],words[i + 1], pre_weight);
}
str = bufferReader.readLine();
}
}
上面是建造图的过程,我们充分利用了set的返回值。注意,这里不能直接从edge中读取边的权值,因为我们对Graph接口的使用相当于黑盒,是不能直接看到内部数据结构中的存储内容的。
public String poem(String input) {
//throw new RuntimeException("not implemented");
String[] inputWords = input.split(" ");
List<String> outputWords = new ArrayList<>();
int inputLength = inputWords.length;
for (int i = 0; i < inputLength - 1; i++) {
Map<String, Integer> targetsMap = graph.targets(inputWords[i].toLowerCase());//用于储存所有的出边
Map<String, Integer> sourcesMap = graph.sources(inputWords[i + 1].toLowerCase());//用于储存所有的入边
Map<String, Integer> chosenMap = new HashMap<>();
outputWords.add(inputWords[i]);
// 找到所有的桥接单词
for (String bridge : targetsMap.keySet()) {
if (sourcesMap.containsKey(bridge)) {
chosenMap.put(bridge, targetsMap.get(bridge) + sourcesMap.get(bridge));
//System.out.println(bridge);
}
}
// 找到权值最大的桥接单词
String bridgeString = "";
int weight = 0;
for (String bridge : chosenMap.keySet()) {
if (chosenMap.get(bridge) > weight) {
weight = chosenMap.get(bridge);
bridgeString = bridge;
//System.out.println(bridgeString);
}
}
if (!bridgeString.contentEquals("")) {//将空白字符去掉,加入桥接词
outputWords.add(bridgeString);
}
}
outputWords.add(inputWords[inputLength - 1]);//加入待输出集合
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < outputWords.size() - 1; i++) {//依次加上所有的桥接词
sBuilder.append(outputWords.get(i)).append(" ");
}
sBuilder.append(outputWords.get(outputWords.size() - 1));
checkRep();
//System.out.println(sBuilder.toString());
return sBuilder.toString();
}
构建图之后我们得到的代码并不复杂,只需要按照步骤做即可。
链接
- https://zhuanlan.zhihu.com/p/120934262
- https://www.runoob.com/java/java-generics.html
- https://www.runoob.com/java/java-interfaces.html
总结
提示:这里对文章进行总结:
以上就是今天要讲的内容,期末考试将重点考核实验内容,大家好好复习吧!