診斷Java代碼:Double Descent錯誤模式

2021-01-08 IT168

  【IT168 技術文章】

  不要強制轉換這個類!

  與可怕的 空指針異常(該異常除了報告空指針之外,對於將要發生的事情什麼也不說)不同,類強制轉換異常相對來說容易調試。

  類強制轉換經常發生在遞歸下行數據結構的程序中,通常是當代碼的某些部分在每次方法調用中下行了兩級且在第二次下行時調度不當時發生的。程式設計師可通過學習 Double Descent 錯誤模式來識別這種問題。

  Double Descent 錯誤模式

  本周的專題是 Double Descent 錯誤模式。它通過類強制轉換異常來表明。它是由遞歸下行複合數據結構引起的,這種下行方式有時在一次遞歸調用中要下行多級。這樣做經常需要添加類型強制轉換來編譯代碼。但是,在這種下行中,很容易忘記檢查是否滿足了適當的不變量來保證這些類型強制轉換成功。

  考慮以下的 int 二元樹的類層次結構。因為我們希望考慮到空樹的情況,所以將不把 value 欄位放入 Leaf 類中。由於這一決定使所有的 Leaf 相同,我們將用一個靜態欄位為 Leaf 保留一個單元素。

  清單 1. int 二元樹的類層次結構

1 abstract class Tree {
2 }
3 class Leaf extends Tree {
4   public static final Leaf ONLY = new Leaf();
5 }
6 class Branch extends Tree {
7   public int value;
8   public Tree left;
9   public Tree right;
10   public Branch(int _value, Tree _left, Tree _right) {
11     this.value = _value;
12     this.left = _left;
13     this.right = _right;
14   }
15 }
16

  現在,假定我們希望在 Tree 上添加一個方法,該方法確定任意兩個連貫的節點(比如一個分支和它的其中一個子分支)是否都包含一個 0 作為它們的值。我們可能添加以下方法(注意:最後一個方法將不以它的當前形式編譯):

  清單 2. 確定兩個連貫的節點是否都包含值 0 的方法

1 // in class Tree:
2   public abstract boolean hasConsecutiveZeros();
3   // in class Leaf:
4   public boolean hasConsecutiveZeros() {
5     return false;
6   }
7   // in class Branch:
8   public boolean hasConsecutiveZeros() {
9     boolean foundOnLeft = false;
10     boolean foundOnRight = false;
11     if (this.value == 0) {
12       foundOnLeft = this.left.value == 0;
13       foundOnRight = this.right.value == 0;
14     }
15     if (foundOnLeft || foundOnRight) {
16       return true;
17     }
18     else {
19       foundOnLeft = this.left.hasConsecutiveZeros();
20       foundOnRight = this.right.hasConsecutiveZeros();
21       return foundOnLeft || foundOnRight;
22     }
23   }
24

  類 Branch 中的方法將不編譯,因為 this.left 和 this.right 不保證具有 value 欄位。

  我們無法編譯強烈地表明我們對這些數據結構所進行的操作中有邏輯錯誤。但是假設我們忽略此警告,只是僅僅在適當的 if 語句中將 this.left 和 this.right 強制轉換為 Branch ,如下所示:

  清單 3. 在適當的 if 語句中將 this.left 和 this.right 強制轉換為 Branch

1 public boolean hasConsecutiveZeros() {
2     boolean foundOnLeft = false;
3     boolean foundOnRight = false;
4     if (this.value == 0) {
5       foundOnLeft = ((Branch)this.left).value == 0;
6       foundOnRight = ((Branch)this.right).value == 0;
7     }
8     if (foundOnLeft || foundOnRight) {
9       return true;
10     }
11     else {
12       foundOnLeft = this.left.hasConsecutiveZeros();
13       foundOnRight = this.right.hasConsecutiveZeros();
14       return foundOnLeft || foundOnRight;
15     }
16   }
17

  症狀

  現在代碼將會編譯。實際上,在許多測試事例中它都會成功。但是假設我們要在圖 1 所示的樹上運行這段代碼,其中樹的分支都用圓形表示,值在中心,葉子用正方形表示。調用這棵樹上的 hasConsecutiveZeros 將導致類強制轉換異常。

  圖 1. 在這棵樹上,調用 hasConsecutiveZeros 導致類強制轉換異常

  起因

  問題發生在左分支上。因為該分支的值為 0, hasConsecutiveZeros 將其子分支強制轉換為 Branch 類型,當然,轉換失敗。

  治療和預防措施

  修正上述問題的方法與預防這種問題的方法相同。但是,在討論這個修正方法之前,我先討論一種 不修正的方法。

  一種快速但不正確的解決這個問題的方法是除去 Leaf 類並通過簡單地將空指針放在 Branch 的 left 和 right 欄位中來表示 Leaf 節點。這種方法可除去上面代碼中類型強制轉換的需要,但不修正錯誤。

  相反,在運行時發出的錯誤將會是一個空指針異常而不是類強制轉換異常。因為空指針異常更難診斷,這種「修正」實際上會降低代碼的質量。

  那麼,我們如何修正這個錯誤呢?一種方法是將每個類型強制轉換都包在 instanceof 檢查語句中。

  清單 4. 一種修正方法:將每個類型強制轉換都包在 instanceof 檢查語句中

1 if (! (this.left instanceof Leaf)) {  
2       // this.left instanceof Branch
3       foundOnLeft = ((Branch)this.left).value == 0;
4     }
5     if (! (this.right instanceof Leaf)) {
6       // this.right instanceof Branch
7       foundOnRight = ((Branch)this.right).value == 0;
8     }
9

  順便注意一下斷定每個 if 語句正文中希望保留的不變量的注釋。在代碼中添加類似的注釋是個好習慣。這種習慣對於 else 子句尤其有用。因為我們很少對 else 子句中希望保留的不變量進行顯式檢查,所以在代碼中清楚說明該不變量是一個不錯的主意。

  把類型強制轉換當作一種斷言,把不變量當做說明該斷言為 true 的原因的參數。

  以這種方式使用 instanceof 檢查語句的一個缺點是,如果我們要添加 Tree 的另一個子類(比如一個 LeafWithValue 類),我們將不得不修改這些 instanceof 檢查語句。由於這個原因,只要可能我都會設法避開 instanceof 檢查語句。

  相反,我向為每個子類執行適當的操作的子類添加額外的方法。畢竟,添加這種多態方法的能力是面向對象語言的關鍵優勢之一。

  在目前的示例中,我們可以通過向 Tree 類中添加 valueIs 方法來完成這個操作,如下所示:

  清單 5. 使用 valueIs 代替 instanceof

1 // in class Tree:
2   public abstract boolean valueIs(int n);
3   // in class Leaf:
4   public boolean valueIs(int n) { return false; }
5   // in class Branch:
6   public boolean valueIs(int n) {
7     return value == n;
8   }
9
10   // in class Branch, method hasConsecutiveZeros
11   if (this.valueIs(0)) {
12     foundOnLeft = this.left.valueIs(0);
13     foundOnRight = this.right.valueIs(0);
14   }
15

  注意:我已經添加了 valueIs 方法來代替 getValue 方法。如果我們已經向 Leaf 類添加了 getValue 方法,我們要麼是不得不返回一些類型的標誌值表明此方法應用是無意義的,要麼是實際拋出一個異常。

  返回一個標誌值將引起許多與我們上次討論的空標誌錯誤模式一樣的錯誤。拋出一個異常在本例中幫不了什麼忙,因為我們將不得不在 hasConsecutiveZeros 中添加 instanceof 檢查語句以確保我們沒有觸發異常。而這正是在新方法中我們要設法避免的。

  valueIs 通過封裝我們真正希望每個類單獨處理的內容:檢查類的一個實例是否包含給定的值,以避開所有這些問題。

  總結

  下面是本周的錯誤模式的小結:

  模式:Double Descent

  症狀:在數據結構上執行遞歸下行時拋出類強制轉換異常。

  起因:代碼的某些部分在每次方法調用中下行了兩級且第二次下行時調度不當。

  治療和預防措施:把類型強制轉換代碼分解到每個類的單獨方法中去。還有一種選擇是,檢查不變量以確保類型強制轉換將會成功。

  簡言之,這些方法的本質總是使您確信代碼塊內部的不變量會確保代碼塊中的任何類型強制轉換都將成功。當對每個類型強制轉換進行這種級別的詳細審查時,您可能會發現通過向相關的子類添加方法,您將許多這些類型強制轉換分解了。

相關焦點

  • (提高Java代碼質量)|25個優化Java代碼的小技巧
    ,要麼是無意義的代碼。正例:7.頻繁調用 Collection.contains 方法請使用 Set在 java 集合類庫中,List 的 contains 方法普遍時間複雜度是 O(n) ,如果在代碼中需要頻繁調用 contains 方法查找數據,可以先將 list 轉換成 HashSet 實現,將
  • java float double精度為什麼會丟失?淺談java的浮點數精度問題
    問題大概情況可以通過如下代碼理解:得到的結果如下:f=2.0015E7d=2.0015E7d2=2.0014999E7從輸出結果可以看出double 可以正確的表示20014999 ,而float 沒有辦法表示20014999 ,得到的只是一個近似值。這樣的結果很讓人訝異。
  • 尚學堂知識整理:Java double數據類型
    double數據類型使用64位來存儲浮點數。double值也稱為雙精度浮點數。它可以表示一個最小為4.9 x 10^-324,最大為1.7 x 10^308的數字。它可以是正的或負的。所有實數被稱為double字面量。
  • Android被指抄襲Java代碼引爭議
    Mueller仔細檢查了Android的代碼,除了甲骨文在訴訟中提到的一個文件之外,他還發現了六個與Java文件非常相似的文件。這些文件是在Android 2.2版和2.3版中發現的。此外,Mueller指出,在Android的代碼中有三十七個文件包含一些提示,稱這個代碼是Sun專有的代碼。
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    運行時創建對象 方法調用,執行引擎解釋為機器碼 CPU執行指令 多線程切換上下文 編譯 我們都知道,java代碼是運行在Java虛擬機上的。
  • Java多態,對象轉型,和簡單工廠模式 希望對您有幫助!
    大家還需要知道的是:多態是java面向對象的三大特徵之一。而java的多態分為兩種:靜態多態和動態多態。靜態多態的小名叫編譯時多態,通過方法的重載來實現。動態多態是運行時的多態形式,通過對象的多態性質來實現。多態有哪些常用的實現形式呢?分為三種:1,父類作為方法的行參。2,父類作為方法的一個返還值。3,父類引用直接指向子類對象。接下來需要注意的是,敲黑板!
  • 重學Java 設計模式:實戰命令模式「模擬高檔餐廳八大菜系,小二點單...
    目錄一、前言二、開發環境三、命令模式介紹四、案例場景模擬五、用一坨坨代碼實現1. 工程結構2. 代碼實現六、命令模式重構代碼1. 工程結構2. 代碼實現3. 測試驗證七、總結一、前言持之以恆的重要性初學編程往往都很懵,幾乎在學習的過程中會遇到各種各樣的問題,哪怕別人那運行好好的代碼,但你照著寫完就報錯。
  • 精選20道Java代碼筆試題
    1、運算符優先級問題,下面代碼的結果是多少?第二條編譯錯誤,字符串無法與數字用減號連接。第三條、第四條中乘除的優先級高,會先運算,而後再與字符串連接,因此結果分別為:「i1 * i2 = 100」、「i1 * i2 = 1」。
  • 設計模式之狀態模式(java實現)
    下面我們使用代碼來演示一下狀態模式到底是什麼樣的。二、代碼實現第一步:定義抽象狀態類第二步:定義具體折扣類首先是買一件打九折:然後是買兩件打5折最後是買三件被拉入黑名單了第三部:具體環境類第四步:我們就演示一下整個狀態模式從上面的輸出結果可以看到,我們執行不同的命令,會有不同的狀態。
  • 這10道 Java 測試題,據說阿里P7的正確率只有50%
    題目一: float a = 0.125f; double b = 0.125d; System.out.println((a - b) == 0.0); 代碼的輸出結果是什麼?A. trueB. false題目二: double c = 0.8; double d = 0.7; double e = 0.6; 那麼c-d與d-e是否相等?
  • 大神詳解,這麼詳細的Java設計模式不收藏可惜了
    引子設計模式是很多程式設計師總結出來的優秀實踐。曾經在剛開始寫項目的時候學習過設計模式,在開發過程中,也主動或者被動的使用過。現在寫代碼雖說不會特意明確在用哪種設計模式,但潛移默化的寫出來公認的優秀實踐代碼,畢竟看的比較清爽。
  • 跟我學java編程—認識java語言的字符類型
    用記事本打開「CharSample.java」文件,輸入以下代碼:編譯「CharSample.java」文件,在命令行窗口輸入「javac CharSample.java」並執行命令,編譯通過後,在命令行窗口輸入「java CharSample」運行Java程序,命令行窗口顯示如下信息:
  • 打工人打工魂,打工的必會java調用python的幾種用法
    /download.html下載Jython的jar包或者在maven的pom.xml文件中加入如下代碼:<dependency>java<groupId>org.python</groupId> <artifactId>jython-standalone</artifactId> <version
  • 跟我學java編程—認識java的整數類型
    示例2:int類型的溢出在D盤Java目錄下,新建「OverFlow.java」文件。用記事本打開「OverFlow.java」文件,輸入以下代碼:編譯「OverFlow.java」文件,在命令行窗口輸入「javac OverFlow.java」並執行命令,編譯器顯示如下信息:編譯器給出過大的整數錯誤信息,num的數值明顯超出的int所能表示的最大值。
  • Java中long和double的非原子性你了解嗎?
    導語天在看Effective Java的時候有這樣一句話:Java的語言規範保證了讀寫一個變量是原子的,除非這個變量的類型為long或double。換句話說,讀取一個非long或double類型的變量,可以保證返回的值是某個線程保存在該變量中的,即時多個線程在沒有同步的情況下並發地修改這個變量也是如此。其實本來很簡單的一句話我反覆讀了幾遍還沒明白是啥意思,直到後來網上查了一下才恍然大悟。三個特性JMM的三大特性:可見性、有序性、原子性。
  • Java常見內存溢出異常分析
    下面我們通過如下的代碼來演示一下此種情況的溢出:   [java] view plain copyimport java.util.*;   import java.lang.   at com.test.OutOfMemoryErrorTest.stackOutOfMemoryError(OutOfMemoryErrorTest.java:27)   棧溢出拋出java.lang.StackOverflowError錯誤,出現此種情況是因為方法運行的時候棧的深度超過了虛擬機容許的最大深度所致。
  • 能寫出這種代碼的程式設計師都是神仙吧!
    部分代碼://這是他的MyFrame.java文件,都是常規操作public class MyFrame extends JFrame{ MyPanel panel; public static final int width=1920;
  • 程式設計師:java單例模式,為什麼要加雙重鎖?為什麼要加volatile?
    單例模式單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
  • 九碼課堂|解決方法,double類型精度丟失
    public double add(){double number1 = 1; double number2 = 20.2; double number3 = 300.03; double result = number1 + number2 + number3; System.out.println(result); result result;}列印結果如下
  • Java編程中基礎反射詳細解析
    類加載器負責加載所有的類,系統為所有加載到內存中的類生成一個java.lang.Class 的實例。那麼我們便可以更靈活的編寫代碼,代碼可以在運行時裝配,無需在組件之間進行原始碼連結,降低代碼的耦合度;還有動態代理的實現等等。