java對象通用方法之覆蓋equals時請遵守通用約定、覆蓋equals時總要覆蓋hashCode、始終要覆蓋toString、考慮實現Comparable接口

原文地址:

http://www.cnblogs.com/stephen-liu74/archive/2012/01/16/2228360.html

內容摘要:

覆蓋equals時請遵守通用約定:

      對於Object類中提供的equals方法在必要的時候是必要重載的,然而如果違背了一些通用的重載準則,將會給程序帶來一些潛在的運行時錯誤。如果自定義的class沒有重載該方法,那麼該類實例之間的相等性的比較將是基於兩個對象是否指向同一地址來判定的。因此對於以下幾種情況可以考慮不重載該方法:
      1.    類的每一個實例本質上都是唯一的。
      不同於值對象,需要根據其內容作出一定的判定,然而該類型的類,其實例的自身便具備了一定的唯一性,如Thread、Timer等,他本身並不具備更多邏輯比較的必要性。
      2.    不關心類是否提供了“邏輯相等”的測試功能。
      如Random類,開發者在使用過程中並不關心兩個Random對象是否可以生成同樣隨機數的值,對於一些工具類亦是如此,如NumberFormat和DateFormat等。
      3.    超類已經覆蓋了equals,從超類繼承過來的行爲對於子類也是合適的。
      如Set實現都從AbstractSet中繼承了equals實現,因此其子類將不在需要重新定義該方法,當然這也是充分利用了繼承的一個優勢。
      4.    類是私有的或是包級別私有的,可以確定它的equals方法永遠不會被調用。
    
      那麼什麼時候應該覆蓋Object.equals呢?如果類具有自己特有的“邏輯相等”概念,而且超類中沒有覆蓋equals以實現期望的行爲,這是我們就需要覆蓋equals方法,如各種值對象,或者像Integer和Date這種表示某個值的對象。在重載之後,當對象插入Map和Set等容器中時,可以得到預期的行爲。枚舉也可以被視爲值對象,然而卻是這種情形的一個例外,對於枚舉是沒有必要重載equals方法,直接比較對象地址即可,而且效率也更高。
      在覆蓋equals是,該條目給出了通用的重載原則:
      1.    自反性:對於非null的引用值x,x.equals(x)返回true。
      如果違反了該原則,當x對象實例被存入集合之後,下次希望從該集合中取出該對象時,集合的contains方法將直接無法找到之前存入的對象實例。
      2.    對稱性:對於任何非null的引用值x和y,如果y.equals(x)爲true,那麼x.equals(y)也爲true。

 1     public final class CaseInsensitiveString {
 2         private final String s;
 3         public CaseInsensitiveString(String s) {
 4             this.s = s;
 5         }
 6         @Override public boolean equals(Object o) {
 7             if (o instanceof CaseInsensitiveString) 
 8                 return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
 9             if (o instanceof String) //One-way interoperability
10                 return s.equalsIgnoreCase((String)o);
11             return false;
12         }
13     }
14     public static void main(String[] args) {
15         CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
16         String s = "polish";
17         List<CaseInsensitiveString> l = new ArrayList<CaseInsensitiveString>();
18         l.add(cis);
19         if (l.contains(s)) 
20             System.out.println("s can be found in the List");
21     }

      對於上例,如果執行cis.equals(s)將會返回true,因爲在該class的equals方法中對參數o的類型針對String作了特殊的判斷和特殊的處理,因此如果equals中傳入的參數類型爲String時,可以進一步完成大小寫不敏感的比較。然而在String的equals中,並沒有針對CaseInsensitiveString類型做任何處理,因此s.equals(cis)將一定返回false。針對該示例代碼,由於無法確定List.contains的實現是基於cis.equals(s)還是基於s.equals(cis),對於實現邏輯兩者都是可以接受的,既然如此,外部的使用者在調用該方法時也應該同樣保證並不依賴於底層的具體實現邏輯。由此可見,equals方法的對稱性是非常必要的。以上的equals實現可以做如下修改:

1     @Override public boolean equals(Object o) {
2         if (o instanceof CaseInsensitiveString) 
3             return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
4         return false;
5     }

      這樣修改之後,cis.equals(s)和s.equals(cis)都將返回false。    
      3.    傳遞性:對於任何非null的引用值x、y和z,如果x.equals(y)返回true,同時y.equals(z)也返回true,那麼x.equals(z)也必須返回true。

 1     public class Point {
 2         private final int x;
 3         private final int y;
 4         public Point(int x,int y) {
 5             this.x = x;
 6             this.y = y;
 7         }
 8         @Override public boolean equals(Object o) {
 9             if (!(o instanceof Point)) 
10                 return false;
11             Point p = (Point)o;
12             return p.x == x && p.y == y;
13         }
14     }

      對於該類的equals重載是沒有任何問題了,該邏輯可以保證傳遞性,然而在我們試圖給Point類添加新的子類時,會是什麼樣呢?

 1     public class ColorPoint extends Point {
 2         private final Color c;
 3         public ColorPoint(int x,int y,Color c) {
 4             super(x,y);
 5             this.c = c;
 6         }
 7         @Override public boolean equals(Object o) {
 8             if (!(o instanceof ColorPoint)) 
 9                 return false;
10             return super.equals(o) && ((ColorPoint)o).c == c;
11         }
12     }

      如果在ColorPoint中沒有重載自己的equals方法而是直接繼承自超類,這樣的相等性比較邏輯將會給使用者帶來極大的迷惑,畢竟Color域字段對於ColorPoint而言確實是非常有意義的比較性字段,因此該類重載了自己的equals方法。然而這樣的重載方式確實帶來了一些潛在的問題,見如下代碼:

1     public void test() {
2         Point p = new Point(1,2);
3         ColorPoint cp = new ColorPoint(1,2,Color.RED);
4         if (p.equals(cp))
5             System.out.println("p.equals(cp) is true");
6         if (!cp.equals(p))
7             System.out.println("cp.equals(p) is false");
8     }

      從輸出結果來看,ColorPoint.equals方法破壞了相等性規則中的對稱性,因此需要做如下修改:

1     @Override public boolean equals(Object o) {
2         if (!(o instanceof Point)) 
3             return false;
4         if (!(o instanceof ColorPoint))
5             return o.equals(this);
6         return super.equals(o) && ((ColorPoint)o).c == c;
7     }

      經過這樣的修改,對稱性確實得到了保證,但是卻犧牲了傳遞性,見如下代碼:

1     public void test() {
2         ColorPoint p1 = new ColorPoint(1,2,Color.RED);
3         Point p2 = new Point(1,2);
4         ColorPoint p1 = new ColorPoint(1,2,Color.BLUE);
5         if (p1.equals(p2) && p2.equals(p3))
6             System.out.println("p1.equals(p2) && p2.equals(p3) is true");
7         if (!(p1.equals(p3))
8             System.out.println("p1.equals(p3) is false");
9     }

      再次看輸出結果,傳遞性確實被打破了。如果我們在Point.equals中不使用instanceof而是直接使用getClass呢?

1     @Override public boolean equals(Object o) {
2         if (o == null || o.getClass() == getClass()) 
3             return false;
4         Point p = (Point)o;
5         return p.x == x && p.y == y;
6     }

      這樣的Point.equals確實保證了對象相等性的這幾條規則,然而在實際應用中又是什麼樣子呢?

 1     class MyTest {
 2         private static final Set<Point> unitCircle;
 3         static {
 4             unitCircle = new HashSet<Point>();
 5             unitCircle.add(new Point(1,0));
 6             unitCircle.add(new Point(0,1));
 7             unitCircle.add(new Point(-1,0));
 8             unitCircle.add(new Point(0,-1));
 9         }
10         public static boolean onUnitCircle(Point p) {
11             return unitCircle.contains(p);
12         }
13     }

      如果此時我們測試的不是Point類本身,而是ColorPoint,那麼按照目前Point.equals(getClass方式)的實現邏輯,ColorPoint對象在被傳入onUnitCircle方法後,將永遠不會返回true,這樣的行爲違反了"里氏替換原則"(敏捷軟件開發一書中給出了很多的解釋),既一個類型的任何重要屬性也將適用於它的子類型。因此該類型編寫的任何方法,在它的子類型上也應該同樣運行的很好。
      如何解決這個問題,該條目給出了一個折中的方案,既複合優先於繼承,見如下代碼:

 1     public class ColorPoint {
 2         //包含了Point的代理類
 3         private final Point p;
 4         private final Color c;
 5         public ColorPoint(int x,int y,Color c) {
 6             if (c == null)
 7                 throw new NullPointerException();
 8             p = new Point(x,y);
 9             this.c = c;
10         }
11         //提供一個視圖方法返回內部的Point對象實例。這裏Point實例爲final對象非常重要,
12 //可以避免使用者的誤改動。視圖方法在Java的集合框架中有着大量的應用。
13         public Point asPoint() {
14             return p;
15         }
16         @Override public boolean equals(Object o) {
17             if (!(o instanceof ColorPoint)) 
18                 return false;
19             ColorPoint cp = (ColorPoint)o;
20             return cp.p.equals(p) && cp.c.equals(c);
21         }
22     }

      4.    一致性:對於任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被改變,多次調用x.equals(y)就會一致的返回true,或者一致返回false。
      在實際的編碼中,儘量不要讓類的equals方法依賴一些不確定性較強的域字段,如path。由於path有多種表示方式可以指向相同的目錄,特別是當path中包含主機名稱或ip地址等信息時,更增加了它的不確定性。再有就是path還存在一定的平臺依賴性。
      5.    非空性:很難想象會存在o.equals(null)返回true的正常邏輯。作爲JDK框架中極爲重要的方法之一,equals方法被JDK中的基礎類廣泛的使用,因此作爲一種通用的約定,像equals、toString、hashCode和compareTo等重要的通用方法,開發者在重載時不應該讓自己的實現拋出異常,否則會引起很多潛在的Bug。如在Map集合中查找指定的鍵,由於查找過程中的鍵相等性的比較就是利用鍵對象的equals方法,如果此時重載後的equals方法拋出NullPointerException異常,而Map的get方法並未捕獲該異常,從而導致系統的運行時崩潰錯誤,然而事實上,這樣的問題是完全可以通過正常的校驗手段來避免的。綜上所述,很多對象在重載equals方法時都會首先對輸入的參數進行是否爲null的判斷,見如下代碼:

1     @Override public boolean equals(Object o) {
2         if (o == null)
3             return false;
4         if (!(o instanceof MyType)) 
5             return false;
6         ...
7     }

      注意以上代碼中的instanceof判斷,由於在後面的實現中需要將參數o進行類型強轉,如果類型不匹配則會拋出ClassCastException,導致equals方法提前退出。在此需要指出的是instanceof還有一個潛在的規則,如果其左值爲null,instanceof操作符將始終返回false,因此上面的代碼可以優化爲:

1     @Override public boolean equals(Object o) {
2         if (!(o instanceof MyType)) 
3             return false;
4         ...
5     }

      鑑於之上所述,該條目中給出了重載equals方法的最佳邏輯:
      1.    使用==操作符檢查"參數是否爲這個對象的引用",如果是則返回true。由於==操作符是基於對象地址的比較,因此特別針對擁有複雜比較邏輯的對象而言,這是一種性能優化的方式。
      2.    使用instanceof操作符檢查"參數是否爲正確的類型",如果不是則返回false。
      3.    把參數轉換成爲正確的類型。由於已經通過instanceof的測試,因此不會拋出ClassCastException異常。
      4.    對於該類中的每個"關鍵"域字段,檢查參數中的域是否與該對象中對應的域相匹配。

      如果以上測試均全部成功返回true,否則false。見如下示例代碼:

 1     @Override public boolean equals(Object o) {
 2         if (o == this) 
 3             return true;
 4         
 5         if (!(o instanceof MyType))
 6             return false;
 7             
 8         MyType myType = (MyType)o;
 9         return objField.equals(o.objField) && intField == o.intField 
10             && Double.compare(doubleField,o.doubleField) == 0 
11             && Arrays.equals(arrayField,o.arrayField);
12     }

      從上面的示例中可以看出,如果域字段爲Object對象,則使用equals方法進行兩者之間的相等性比較,如果爲int等整型基本類型,可以直接比較,如果爲浮點型基本類型,考慮到精度和Double.NaN和Float.NaN等問題,推薦使用其對應包裝類的compare方法,如果是數組,可以使用JDK 1.5中新增的Arrays.equals方法。衆所周知,&&操作符是有短路原則的,因此應該將最有可能不相同和比較開銷更低的域比較放在最前面。
      最後需要提起注意的是Object.equals的參數類型爲Object,如果要重載該方法,必須保持參數列表的一致性,如果我們將子類的equals方法寫成:public boolean equals(MyType o);Java的編譯器將會視其爲Object.equals的過載(Overload)方法,因此推薦在聲明該重載方法時,在方法名的前面加上@Override註釋標籤,一旦當前聲明的方法因爲各種原因並沒有重載超類中的方法,該標籤的存在將會導致編譯錯誤,從而提醒開發者此方法的聲明存在語法問題。
    
覆蓋equals時總要覆蓋hashCode:

      一個通用的約定,如果類覆蓋了equals方法,那麼hashCode方法也需要被覆蓋。如果將會導致該類無法和基於散列的集合一起正常的工作,如HashMap、HashSet。來自JavaSE6的約定如下:
      1.    在應用程序執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那麼對這同一個對象多次調用,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致。
      2.    如果兩個對象根據equals(Object)方法比較是相等的,那麼調用這兩個對象中任意一個對象的hashCode方法都必須產生同樣的整數結果。
      3.    如果兩個對象根據equals(Object)方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不一定要產生不同的整數結果。但是程序員應該知道,給不相等的對象產生截然不同的整數結果,有可能提高散列表的性能。
      如果類沒有覆蓋hashCode方法,那麼Object中缺省的hashCode實現是基於對象地址的,就像equals在Object中的缺省實現一樣。如果我們覆蓋了equals方法,那麼對象之間的相等性比較將會產生新的邏輯,而此邏輯也應該同樣適用於hashCode中散列碼的計算,既參與equals比較的域字段也同樣要參與hashCode散列碼的計算。見下面的示例代碼:

 1     public final class PhoneNumber {
 2         private final short areaCode;
 3         private final short prefix;
 4         private final short lineNumber;
 5         public PhoneNumber(int areaCode,int prefix,int lineNumber) {
 6             //做一些基於參數範圍的檢驗。
 7             this.areaCode = areaCode;
 8             this.prefix = prefix;
 9             this.lineNumber = lineNumber;
10         }
11         @Override public boolean equals(Object o) {
12             if (o == this)
13                 return true;
14             if (!(o instanceof PhoneNumber)) 
15                 return false;
16             PhoneNumber pn = (PhoneNumber)o;
17             return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;
18         }
19     }
20     public static void main(String[] args) {
21         Map<PhoneNumber,String> m = new HashMap<PhoneNumber,String>();
22         PhoneNumber pn1 = new PhoneNumber(707,867,5309);
23         m.put(pn1,"Jenny");
24         PhoneNumber pn2 = new PhoneNumber(707,867,5309);
25         if (m.get(pn) == null)
26             System.out.println("Object can't be found in the Map");
27     }

      從以上示例的輸出結果可以看出,新new出來的pn2對象並沒有在Map中找到,儘管pn2和pn1的相等性比較將返回true。這樣的結果很顯然是有悖我們的初衷的。如果想從Map中基於pn2找到pn1,那麼我們就需要在PhoneNumber類中覆蓋缺省的hashCode方法,見如下代碼:

1     @Override public int hashCode() {
2         int result = 17;
3         result = 31 * result + areaCode;
4         result = 31 * result + prefix;
5         result = 31 * result + lineNumber;
6         return result;
7     }

      在上面的代碼中,可以看到參與hashCode計算的域字段也同樣參與了PhoneNumber的相等性(equals)比較。對於生成的散列碼,推薦不同的對象能夠儘可能生成不同的散列,這樣可以保證在存入HashMap或HashSet中時,這些對象被分散到不同的散列桶中,從而提高容器的存取效率。對於有些不可變對象,如果需要被頻繁的存取於哈希集合,爲了提高效率,可以在對象構造的時候就已經計算出其hashCode值,hashCode()方法直接返回該值即可,如:

 1     public final class PhoneNumber {
 2         private final short areaCode;
 3         private final short prefix;
 4         private final short lineNumber;
 5         private final int myHashCode;
 6         public PhoneNumber(int areaCode,int prefix,int lineNumber) {
 7             //做一些基於參數範圍的檢驗。
 8             this.areaCode = areaCode;
 9             this.prefix = prefix;
10             this.lineNumber = lineNumber;
11             myHashCode = 17;
12             myHashCode = 31 * myHashCode + areaCode;
13             myHashCode = 31 * myHashCode + prefix;
14             myHashCode = 31 * myHashCode + lineNumber;
15         }
16         @Override public boolean equals(Object o) {
17             if (o == this)
18                 return true;
19             if (!(o instanceof PhoneNumber)) 
20                 return false;
21             PhoneNumber pn = (PhoneNumber)o;
22             return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;
23         }
24         @Override public int hashCode() {
25             return myHashCode;
26         }
27     }

      另外,該條目還建議不要僅僅利用某一域字段的部分信息來計算hashCode,如早期版本的String,爲了提高計算哈希值的效率,只是挑選其中16個字符參與hashCode的計算,這樣將會導致大量的String對象具有重複的hashCode,從而極大的降低了哈希集合的存取效率。
    
始終要覆蓋toString:

      與equals和hashCode不同的是,該條目推薦應該始終覆蓋該方法,以便在輸出時可以得到更明確、更有意義的文字信息和表達格式。這樣在我們輸出調試信息和日誌信息時,能夠更快速的定位出現的異常或錯誤。如上一個條目中PhoneNumber的例子,如果不覆蓋該方法,就會輸出PhoneNumber@163b91 這樣的不可讀信息,因此也不會給我們診斷問題帶來更多的幫助。以下代碼重載了該方法,那麼在我們調用toString或者println時,將會得到"(408)867-5309"。

1     @Override String toString() {
2         return String.format("(%03d) %03d-%04d",areaCode,prefix,lineNumber);
3     }

      對於toString返回字符串中包含的域字段,如本例中的areaCode、prefix和lineNumber,應該在該類(PhoneNumber)的聲明中提供這些字段的getter方法,以避免toString的使用者爲了獲取其中的信息而不得不手工解析該字符串。這樣不僅帶來不必要的效率損失,而且在今後修改toString的格式時,也會給使用者的代碼帶來負面影響。提到toString返回字符串的格式,有兩個建議,其一是儘量不要固定格式,這樣會給今後添加新的字段信息帶來一定的束縛,因爲必須要考慮到格式的兼容性問題,再者就是推薦可以利用toString返回的字符串作爲該類的構造函數參數來實例化該類的對象,如BigDecimal和BigInteger等裝箱類。
      這裏還有一點建議是和hashCode、equals相關的,如果類的實現者已經覆蓋了toString的方法,那麼完全可以利用toString返回的字符串來生成hashCode,以及作爲equals比較對象相等性的基礎。這樣的好處是可以充分的保證toString、hashCode和equals的一致性,也降低了在對類進行修訂時造成的一些潛在問題。儘管這不是剛性要求的,卻也不失爲一個好的實現方式。該建議並不是源於該條目,而是去年在看effective C#中瞭解到的。
    
考慮實現Comparable接口:

      和之前提到的通用方法equals、hashCode和toString不同的是compareTo方法屬於Comparable接口,該接口爲其實現類提供了排序比較的規則,實現類僅需基於內部的邏輯,爲compareTo返回不同的值,既A.compareTo(B) > 0可視爲A > B,反之則A < B,如果A.compareTo(B) == 0,可視爲A == B。在C++中由於提供了操作符重載的功能,因此可以直接通過重載操作符的方式進行對象間的比較,事實上C++的標準庫中提供的缺省規則即爲此,如bool operator>(OneObject o)。在Java中,如果對象實現了Comparable接口,即可充分利用JDK集合框架中提供的各種泛型算法,如:Arrays.sort(a); 即可完成a對象數組的排序。事實上,JDK中的所有值類均實現了該接口,如Integer、String等。
      Object.equals方法的通用實現準則也同樣適用於Comparable.compareTo方法,如對稱性、傳遞性和一致性等,這裏就不做過多的贅述了。然而兩個方法之間有一點重要的差異還是需要在這裏提及的,既equals方法不應該拋出異常,而compareTo方法則不同,由於在該方法中不推薦跨類比較,如果當前類和參數對象的類型不同,可以拋出ClassCastException異常。在JDK 1.5 之後我們實現的Comparable<T>接口多爲該泛型接口,不在推薦直接繼承1.5 之前的非泛型接口Comparable了,新的compareTo方法的參數也由Object替換爲接口的類型參數,因此在正常調用的情況下,如果參數類型不正確,將會直接導致編譯錯誤,這樣有助於開發者在coding期間修正這種由類型不匹配而引發的異常。
      在該條目中針對compareTo的相等性比較給出了一個強烈的建議,而不是真正的規則。推薦compareTo方法施加的等同性測試,在通常情況下應該返回和equals方法同樣的結果,考慮如下情況:

 1     public static void main(String[] args) {
 2         HashSet<BigDecimal> hs = new HashSet<BigDecimal>();
 3         BigDecimal bd1 = new BigDecimal("1.0");
 4         BigDecimal bd2 = new BigDecimal("1.00");
 5         hs.add(bd1);
 6         hs.add(bd2);
 7         System.out.println("The count of the HashSet is " + hs.size());
 8         
 9         TreeSet<BigDecimal> ts = new TreeSet<BigDecimal>();
10         ts.add(bd1);
11         ts.add(bd2);
12         System.out.println("The count of the TreeSet is " + ts.size());
13     }
14     /*    輸出結果如下:
15         The count of the HashSet is 2
16         The count of the TreeSet is 1
17 */    

      由以上代碼的輸出結果可以看出,TreeSet和HashSet中包含元素的數量是不同的,這其中的主要原因是TreeSet是基於BigDecimal的compareTo方法是否返回0來判斷對象的相等性,而在該例中compareTo方法將這兩個對象視爲相同的對象,因此第二個對象並未實際添加到TreeSet中。和TreeSet不同的是HashSet是通過equals方法來判斷對象的相同性,而恰恰巧合的是BigDecimal的equals方法並不將這個兩個對象視爲相同的對象,這也是爲什麼第二個對象可以正常添加到HashSet的原因。這樣的差異確實給我們的編程帶來了一定的負面影響,由於HashSet和TreeSet均實現了Set<E>接口,倘若我們的集合是以Set<E>的參數形式傳遞到當前添加BigDecimal的函數中,函數的實現者並不清楚參數Set的具體實現類,在這種情況下不同的實現類將會導致不同的結果發生,這種現象極大的破壞了面向對象中的"里氏替換原則"。
      在重載compareTo方法時,應該將最重要的域字段比較方法比較的最前端,如果重要性相同,則將比較效率更高的域字段放在前面,以提高效率,如以下代碼:

 1     public int compareTo(PhoneNumer pn) {
 2         if (areaCode < pn.areaCode)
 3             return -1;
 4         if (areaCode > pn.areaCode)
 5             return 1;
 6             
 7         if (prefix < pn.prefix)
 8             return -1;
 9         if (prefix > pn.prefix)
10             return 1;
11             
12         if (lineNumber < pn.lineNumer)
13             return -1;
14         if (lineNumber > pn.lineNumber)
15             return 1;
16         return 0;
17     }

      上例給出了一個標準的compareTo方法實現方式,由於使用compareTo方法排序的對象並不關心返回的具體值,只是判斷其值是否大於0,小於0或是等於0,因此以上方法可做進一步優化,然而需要注意的是,下面的優化方式會導致數值類型的作用域溢出問題。

 1     public int compareTo(PhoneNumer pn) {
 2         int areaCodeDiff = areaCode - pn.areaCode;
 3         if (areaCodeDiff != 0)
 4             return areaCodeDiff;
 5         int prefixDiff = prefix - pn.prefix;
 6         if (prefixDiff != 0)
 7             return prefixDiff;
 8     
 9         int lineNumberDiff = lineNumber - pn.lineNumber;
10         if (lineNumberDiff != 0)
11             return lineNumberDiff;
12         return 0;
13     }    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章