面向对象第三单元总结博客

面向对象第三次作业总结博客

JML语言理论与应用梳理

Java建模语言(Java Modeling Language,JML)是一种进行详细设计的符号语言。这种语言增加了一些符号用以标识一个方法是用来干什么的,其关注的内容在于方法的效果,而不在乎方法的具体实现。这样,使用JML语言就可以预先设计好方法功能而不用管如何实现。

JML对一个方法的描述主要包括以下方面:

1、确定类的使用规范

一个类根据调用时初始条件的不同可能会有不同的行为,例如:当参数传递不合理时会抛出异常,即遵循异常行为模式;反之遵循正常行为模式。

/*@ public normal_behavior
     @ requires containsPathId(pathId);
     @ assignable \nothing;
     @ ensures (\exists int i; 0 <= i && i < pList.length; pidList[i] == pathId && \result == pList[i]);
     @ also
     @ public exceptional_behavior
     @ requires !containsPathId(pathId);
     @ assignable \nothing;
     @ signals_only PathIdNotFoundException;
     @*/

如上,public normal_behavior是正常行为标识而public exceptional_behavior是异常行为模式。

2、前置条件与后置条件

前置条件表示一个方法调用时必须满足的一些要求,后置条件表示这个方法调用完毕后必须满足的要求。

/*@ public normal_behavior
     @ requires path != null && path.isValid() && containsPath(path);
     @ assignable pList, pidList;
     @ ensures containsPath(path) == false;
     @ ensures (\exists int i; 0 <= i && i < \old(pList.length); \old(pList[i].equals(path)) &&
     @           \result == \old(pidList[i]));
     @ ensures (\forall int i; 0 <= i && i < \old(pList.length) && \old(pList[i].equals(path) == false);
     @         containsPath(\old(pList[i])) && containsPathId(\old(pidList[i])));

如上,前置条件要求path不可为null并且path有效并且path包含在容器中;而三个ensures标识的是方法的后置条件,从前置后置条件可以看出这是一个删除path的方法。

3、模型域

模型域类似成员变量,只能被应用到行为规范中。

//@ public instance model non_null Path[] pList;
   //@ public instance model non_null int[] pidList;

如上定义了pListpidList两个模型域,non_null表示这两个域的元素都是非null的。

4、不变量

类级别的不变量是指进入和退出一个方法是都必须满足的条件。

 //@ public invariant pList.length == pidList.length;

比如这个不变量表示pList.lengthpidList.length在任何方法调用前后都应当相同。

5、量词

JML中量词的意思与逻辑学上的量词意思相近,主要是存在量词与全称量词。

 @ ensures \result == (\exists int i; 0 <= i && i < pList.length;
     @                     pList[i].equals(path));

如上,\exists表示存在量词。

JUnitNG/JUnit测试样例

尝试使用了JUnitNG进行自动化测试,测试使用代码如下:

// demo/Demo.java
package demo;

public class Demo {
   public static int[] nodes = new int[5];
   /* @ public normal_behaviour
      @ ensures \result == a - b;
   */
   public static int compare(int a, int b) {
       return a - b;
  }
   /* @ public normal_behaviour
      @ ensures \result = nodes.length;
   */
   public static /*@pure@*/ int size(){
        return nodes.length;
    }
    /* @ public normal_behaviour
        @ requires index >= 0 && index < size();
        @ assignable \nothing;
        @ ensures \result == nodes[index];
    */
   public static /*@pure@*/ int getNode(int index) {
       if (index < 0 || index >= size()) {
           return -1;
      }
       return nodes[index];
  }

   public static void main(String[] args) {
       compare(2,323232);
       nodes[1] = 2;
       nodes[0] = 0;
       size();
       getNode(1);
  }
}

得到测试结果如下:

[TestNG] Running:
Command line suite

Passed: racEnabled()
Passed: constructor Demo()
Passed: static compare(-2147483648, -2147483648)
Failed: static compare(0, -2147483648)
Failed: static compare(2147483647, -2147483648)
Passed: static compare(-2147483648, 0)
Passed: static compare(0, 0)
Passed: static compare(2147483647, 0)
Failed: static compare(-2147483648, 2147483647)
Passed: static compare(0, 2147483647)
Passed: static compare(2147483647, 2147483647)
Passed: static getNode(-2147483648)
Passed: static getNode(0)
Passed: static getNode(2147483647)
Passed: static main(null)
Passed: static main({})
Passed: static size()

===============================================
Command line suite
Total tests run: 17, Failures: 3, Skips: 0
===============================================

可以看到原代码中compare方法存在溢出问题,通过JMLUnitNG自动化测试检测了出来。

如下是使用JUnit自己设计测试样例对作业的测试代码:

针对本单元第二次作业中的MyGraphTest类,使用JUnit生成测试样例,并按照规格进行测试。

首先建图时希望尽可能代表情况种类数多,

private static MyGraph graph = new MyGraph();
   private static MyPath p1 = new MyPath(1,2,2,3,4,5);
   private static MyPath p2 = new MyPath(1,2,6,2,6,8);
   private static MyPath p3 = new MyPath(7,6,4,9);
   private static MyPath p4 = new MyPath(10,11,12,13);
@BeforeClass
public static void before() throws Exception {
   graph.addPath(p1);
   graph.addPath(p2);
   graph.addPath(p3);
   graph.addPath(p4);
}

接下来是对部分方法的测试:

@Test
public void testContainsPath() throws Exception {
   Assert.assertTrue(graph.containsPath(p1));
   Assert.assertTrue(graph.containsPath(p2));
   Assert.assertTrue(graph.containsPath(p3));
   Assert.assertTrue(graph.containsPath(p4));
   Assert.assertEquals(false,graph.containsPath(new MyPath(4,5)));
}
@Test(expected = NodeIdNotFoundException.class)
public void testRemovePath() throws Exception {
   Assert.assertEquals(1,graph.removePath(p1));
   graph.getShortestPathLength(2,5);
   Assert.assertEquals(5,graph.addPath(p1));
}
@Test
public void testContainsNode() throws Exception {
   Assert.assertTrue(graph.containsNode(2));
   Assert.assertTrue(graph.containsNode(4));
   Assert.assertTrue(graph.containsNode(8));
   Assert.assertTrue(graph.containsNode(6));
   Assert.assertFalse(graph.containsNode(14));
}
@Test
public void testContainsEdge() throws Exception {
   Assert.assertTrue(graph.containsEdge(2, 2));
   Assert.assertFalse(graph.containsEdge(1, 1));
   Assert.assertTrue(graph.containsEdge(13, 12));
   Assert.assertFalse(graph.containsEdge(2, 4));
   Assert.assertTrue(graph.containsEdge(2, 6));
   Assert.assertFalse(graph.containsEdge(7, 8));
   Assert.assertFalse(graph.containsEdge(8, 13));
   Assert.assertFalse(graph.containsEdge(231, -23));
   Assert.assertFalse(graph.containsEdge(2, 231));
}
@Test
public void testIsConnected() throws Exception {
   Assert.assertTrue(graph.isConnected(2, 2));
   Assert.assertTrue(graph.isConnected(1, 1));
   Assert.assertTrue(graph.isConnected(2, 4));
   Assert.assertFalse(graph.isConnected(8, 13));
   Assert.assertTrue(graph.isConnected(9, 5));
   Assert.assertTrue(graph.isConnected(3, 8));

}
@Test(expected = NodeIdNotFoundException.class)
public void testIsConnected2() throws Exception {
   graph.isConnected(-31,12);
}

@Test(expected = NodeIdNotFoundException.class)
public void testIsConnected3() throws Exception {
   graph.isConnected(11,0);
}

@Test(expected = NodeIdNotFoundException.class)
public void testIsConnected4() throws Exception {
   graph.isConnected(-31,231);
}
@Test
public void testGetShortestPathLength() throws Exception {
   Assert.assertEquals(0,graph.getShortestPathLength(2,2));
   Assert.assertEquals(0,graph.getShortestPathLength(1,1));
   Assert.assertEquals(1,graph.getShortestPathLength(2,6));
   Assert.assertEquals(2,graph.getShortestPathLength(9,5));
   Assert.assertEquals(2,graph.getShortestPathLength(5,9));
   Assert.assertEquals(0,graph.getShortestPathLength(2,2));
   Assert.assertEquals(4,graph.getShortestPathLength(1,9));
}
@Test(expected = NodeNotConnectedException.class)
public void testGetShortestPathLength2() throws Exception {
   graph.getShortestPathLength(3,13);
}
@Test(expected = NodeIdNotFoundException.class)
public void testGetShortestPathLength3() throws Exception {
   graph.getShortestPathLength(0,13);
}
@Test(expected = NodeIdNotFoundException.class)
public void testGetShortestPathLength4() throws Exception {
   graph.getShortestPathLength(2,321);
}
@Test(expected = NodeIdNotFoundException.class)
public void testGetShortestPathLength5() throws Exception {
   graph.getShortestPathLength(0,123);
}

写测试样例时主要考虑的几个方面是:增删操作是否能正确执行,增删操作对图结构的变更是否正确(例如先增再删同一条Path,图结构是否能保持不变),最短路径的求值是否正确(重点在于存在多条路径时能否正确求出),节点与边的添加是否正确,异常是否正确抛出(有没有抛,抛的对不对)。

架构设计

第一次作业

第一次作业比较简单,对于PathContainer的实现基本仅需根据接口规格直接写即可,重点时getDistinctNodeCount的实现,如果每次查询都遍历计算一边,时间消耗非常大,所以维护了一个Set用来保存Node,每次addPath时讲Path的Node添加到维护的Set里,查询时直接返回Set.size()即可

第二次作业

第二次作业在第一次作业的基础上实现了一个GraphGraph继承了第一次作业的PathContainer并新增了四个方法:

    boolean containsNode(int var1);

   boolean containsEdge(int var1, int var2);

   boolean isConnected(int var1, int var2) throws NodeIdNotFoundException;

   int getShortestPathLength(int var1, int var2) throws NodeIdNotFoundException, NodeNotConnectedException;

第二次作业将一个简单容器特化为一个图,难点在于getShortestPathLength方法,同第一次作业,如果每次在查询时计算,必然会超时,所以解决方法是维护一个二维数组,保存任意两个节点间的最短路径,每次增删路径时更新一次这个二维数组,同时更新节点编号到数组下标的映射关系。更新二维数组使用佛洛依德算法。

第三次作业

第三次作业进一步特化,实现了RailwaySystem其本质还是一个图,但是除最短路径外还增加了票价、换乘次数、不满意度等指标:

public int getLeastTicketPrice(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException;

public int getLeastTransferCount(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException;

public int getLeastUnpleasantValue(int fromNodeId, int toNodeId) throws NodeIdNotFoundException, NodeNotConnectedException;

public int getConnectedBlockCount();

更新后有两个问题,第一是多指标评价问题,这个可以考虑建立一个新的类保存四个指标,或者开四个二维数组分别保存一个指标,我采用的是前一种方法

第二个问题是换乘代价的影响,一是换乘对指标计算的影响,二是换乘对计算最小指标的算法的影响,无论是一般的dijstra算法还是floyd算法,都要求最短路径具有最优子结构,而换乘代价破坏了最优子结构,因此需要构思一种新的算法来计算最小指标。

首先这次问题不能直接求最短路的原因来自换乘代价所产生的不具有最优子结构问题,即一条最优路径中的某一部分可能不是这部分最优的。而导致的原因来自且仅来自换乘代价,所以考虑优先求取同一条Path上的最优解。

对于一个图,由多条Path组成,单独拿出一条Path即成为一个子图,并且在这个子图中不存在换乘问题。

于是第一步我们先将每条Path单独抽出,对单条Path形成的子图直接Floyd求各个点之间的最短路径,并且保存下来。

第二步我们将第一步中求到的单个Path内部的最短路径进行合并,形成一个整体的最短路径矩阵。

第三步我们对第二步求得的最短路矩阵进行Floyd,这次在计算权值的时候要加上换乘代价,例如2=5,3 = 3,在计算由这两条联通形成的路径权值时应计算为5+3+换乘代价(比如求ticketprive就是2)

最终得到的就是所求的最短代价矩阵。

这种算法的复杂度是比较低的,实现起来也简单,仅需要在每次调用addPath算法时单独计算一次新增路径的最小指标,并以HashMap的形式储存起来,在调用Floyd算法计算整体路径时,根据保存下来的各个单条路径最小指标构图,再调用传统的Floyd算法即可。

private HashMap<Integer,HashMap<Integer,Edge>> pathFloyd(Path path) {
       HashMap<Integer,Integer> partialIdToMark = new HashMap<>();
       HashMap<Integer,Integer> partialMarkToId = new HashMap<>();
       Edge[][] partialMatrix = new Edge[maxNodeNum][maxNodeNum];
       HashMap<Tuple,Integer> partialEdgeSet = new HashMap<>();
       int partialMark = 0;
       for (int i = 0; i < path.size();i++) {
           int nodeId = path.getNode(i);
           if (i < path.size() - 1) {
               Tuple t = new Tuple(nodeId,path.getNode(i + 1));
               Integer integer = edgeSet.get(t);
               if (integer == null) {
                   edgeSet.put(t,1);
              } else {
                   edgeSet.put(t,integer + 1); }
               partialEdgeSet.put(t,1);
          }
           Integer tem = distinctMap.get(nodeId);
           if (tem == null) {
               distinctMap.put(nodeId,1);
          } else {
               distinctMap.put(nodeId,tem + 1); }
           if (partialIdToMark.get(nodeId) == null) {
               partialIdToMark.put(nodeId, partialMark);
               partialMarkToId.put(partialMark, nodeId);
               partialMark++;
          }
      }
       for (int i = 0;i < partialMark;i++) {
           for (int j = i;j < partialMark;j++) {
               if (i == j) {
                   partialMatrix[i][j] = new Edge(0,0,0,0);
                   continue;
              }
               if (partialEdgeSet.containsKey(
                       new Tuple(partialMarkToId.get(i),
                               partialMarkToId.get(j)))) {
                   int unpleasant = (int) Math.round(Math.pow(4,Math.max(
                          (partialMarkToId.get(i) % 5 + 5) % 5,
                          (partialMarkToId.get(j) % 5 + 5) % 5)));
                   Edge edge = new Edge(1,0,1,unpleasant);
                   partialMatrix[i][j] = edge;
                   partialMatrix[j][i] = edge;
              }
          }
      }
       coreFloyd(partialMatrix,partialMark,0);
       HashMap<Integer,HashMap<Integer,Edge>> re = new HashMap<>();
       for (int i = 0;i < partialMark;i++) {
           HashMap<Integer,Edge> tem = new HashMap<>();
           for (int j = 0;j < partialMark;j++) {
               if (partialMatrix[i][j] != null) {
                   tem.put(partialMarkToId.get(j),partialMatrix[i][j]);
              }
          }
           re.put(partialMarkToId.get(i),tem);
      }
       return re;
  }

 

private Edge[][] floyd() {
       Edge[][] temp = new Edge[maxNodeNum][maxNodeNum];
       int mark = 0;
       for (int integer : distinctMap.keySet()) {
           markToNodeId.put(mark,integer);
           nodeIdToMark.put(integer,mark);
           mark++;
      }
       for (HashMap<Integer,HashMap<Integer,Edge>> h:subs) {
           if (h != null) {
               for (int i:h.keySet()) {
                   HashMap<Integer,Edge> htem = h.get(i);
                   for (int j:htem.keySet()) {
                       if (j < i) {
                           continue;
                      }
                       Edge etem = htem.get(j);
                       Edge edge = new Edge(etem.getLength(),
                               etem.getTransNo(),
                               etem.getTicketPrice(),
                               etem.getUnpleasantValue());
                       int imark = nodeIdToMark.get(i);
                       int jmark = nodeIdToMark.get(j);
                       if (imark == jmark) {
                           temp[imark][jmark] = new Edge(0,0,0,0);
                           continue;
                      }
                       if (temp[imark][jmark] == null) {
                           temp[imark][jmark] = edge;
                           temp[jmark][imark] = edge;
                      } else {
                           setEdge(temp[imark][jmark],edge.getLength()
                                  ,edge.getTransNo(),edge.getTicketPrice()
                                  ,edge.getUnpleasantValue());
                      }
                  }
              }
          }
      }
       coreFloyd(temp,mark,1);
       return temp;
  }
private void coreFloyd(Edge[][] partialMatrix,int partialMark,int check) {
       for (int k = 0;k < partialMark;k++) {
           for (int i = 0; i < partialMark; i++) {
               for (int j = i; j < partialMark; j++) {
                   if (i == j || partialMatrix[i][k] == null
                           || partialMatrix[k][j] == null) {
                       continue;
                  }
                   int len = partialMatrix[i][k].getLength()
                           + partialMatrix[k][j].getLength();
                   int tra = partialMatrix[i][k].getTransNo()
                           + partialMatrix[k][j].getTransNo() + 1;
                   if (check == 0) {
                       tra -= 1;
                  }
                   int tic = partialMatrix[i][k].getTicketPrice()
                           + partialMatrix[k][j].getTicketPrice() + 2;
                   if (check == 0) {
                       tic -= 2;
                  }
                   int unp = partialMatrix[i][k].getUnpleasantValue()
                           + partialMatrix[k][j].getUnpleasantValue() + 32;
                   if (check == 0) {
                       unp -= 32;
                  }
                   if (partialMatrix[i][j] == null) {
                       Edge edge = new Edge(len, tra, tic, unp);
                       partialMatrix[i][j] = edge;
                       partialMatrix[j][i] = edge;
                  }
                   setEdge(partialMatrix[i][j],len,tra,tic,unp);
              }
          }
      }
  }

   private void setEdge(Edge e,int le,int tr,int ti,int un) {
       if (e.getLength() > le) {
           e.setLength(le);
      }
       if (e.getTransNo() > tr) {
           e.setTransNo(tr);
      }
       if (e.getTicketPrice() > ti) {
           e.setTicketPrice(ti);
      }
       if (e.getUnpleasantValue() > un) {
           e.setUnpleasantValue(un);
      }
  }

三次作业一次是一次的特化,每次新作页除新增方法需要实现,add和remove方法需要根据个人实现增加新功能外,其他部分都可以直接继承上一次。

BUG及修复情况

这单元作业bug比较多,多数为细节上的bug

首先是第一次作业,在实现PathcompareTo方法时,直接返回了两个数的差值,导致数字比较大时发生溢出的情况,修正后改用先判断大小再返回1或0或-1。

第三次作业的bug源于可变对象的共享使用,出bug部分代码如下:

for (HashMap<Integer,HashMap<Integer,Edge>> h:subs) {
           if (h != null) {
               for (int i:h.keySet()) {
                   HashMap<Integer,Edge> htem = h.get(i);
                   for (int j:htem.keySet()) {
                       if (j < i) {
                           continue;
                      }
                       Edge edge = htem.get(j);
                       int imark = nodeIdToMark.get(i);
                       int jmark = nodeIdToMark.get(j);
                       if (imark == jmark) {
                           temp[imark][jmark] = new Edge(0,0,0,0);
                           continue;
                      }
                       if (temp[imark][jmark] == null) {
                           temp[imark][jmark] = edge;
                           temp[jmark][imark] = edge;
                      } else {
                           setEdge(temp[imark][jmark],edge.getLength()
                                  ,edge.getTransNo(),edge.getTicketPrice()
                                  ,edge.getUnpleasantValue());
                      }
                  }
              }
          }
      }

在通过保存的各个Path的最小指标图来构建Floyd的初始图时,将从HasMap中得到的Edge对象的引用直接赋给了temp二维数组的对应元素,这导致在接下来进行Floyd计算时可能会对这个对象进行修改,也就造成了错误。修复后改为new一个新的完全一样的对象存入temp数组。

规格理解和撰写的心得体会

本单元主要内容是规格,规格固然是一个模块化方法的好方法,但实际应用时JML规格写起来还是很麻烦的,而且非常容易写错(比如每次作业提供的JML规格都有或大或小的错误),读规格也同样,许多方法直接理解很方便,使用JML规格却要弯弯绕绕许多,不仅容易写错,而且读者也看得云里雾里。当然尽管有这些缺点,使用JML规格规范化编程是非常有必要的,或许在撰写方法的说明时同时使用自然语言描述和JML规格会取得更好的效果。

猜你喜欢

转载自www.cnblogs.com/rubickkciburblogs/p/10902430.html