Java 開發者最容易犯的10個錯誤
我最近在學習 Java,覺得這篇舊文不錯,就翻譯了一下,感覺對新手有些幫助。
原文:www.programcreek.com/2014/05/top…
顧問:張博(Gradle 開發者之一)
譯文開始
這 10 個錯誤是我綜合 GitHub 上的專案、StackOverflow 上的問答和 Google 搜尋關鍵詞的趨勢而分析得出的。
1 將 Array 轉換成 ArrayList 時出錯
一些開發者經常用這樣的程式碼將 Array 轉換成 ArrayList
List<String> list = Arrays.asList(arr);
複製程式碼
Arrays.asList() 的返回值是一個 ArrayList 類的物件,這個 ArrayList 類是 Arrays 類裡的一個私有靜態類(java.util.Arrays.ArrayList),並不是 java.util.ArrayList 類。
java.util.Arrays.ArrayList 有 set() / get() / contains() 方法,但是並不提供任何新增元素的方法,因此它的長度是固定的。如果你希望得到一個 java.util.ArrayList 類的例項,你應該這麼做:
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));
複製程式碼
ArrayList 的建構函式可以接受一個 Collection 例項,而 Collection 是 java.util.Arrays.ArrayList 的超類。
2 檢查 array 裡是否含有某個值時出錯
一些開發者會這麼寫:
Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);
複製程式碼
上面的程式碼可以工作,但是其實沒必要把 list 轉為 set,這有些浪費時間,簡單的寫法是這樣的:
Arrays.asList(arr).contains(targetValue);
複製程式碼
或者這樣的
for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;
複製程式碼
這兩種寫法中,前者可讀性更好。
3 遍歷 list 移除元素時出錯
下面的程式碼在迭代時移除了元素:
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);
複製程式碼
得到的結果是
[b, d]
複製程式碼
這種程式碼的問題在於,當元素被移除時,list 的長度也隨之變小了,index 也同時發生了變化。
你可能認為正確的方法是使用迭代器來刪除元素,比如 foreach 迴圈看起來就是一個迭代器,其實這樣還是有問題。 考慮以下程式碼(程式碼 1):
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("a"))
list.remove(s);
}
複製程式碼
會丟擲 ConcurrentModificationException 異常。
要正確地在遍歷時刪除元素,應該這麼寫(程式碼 2):
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String s = iter.next();
if (s.equals("a")) {
iter.remove();
}
}
複製程式碼
你必須在每次迴圈裡先呼叫 .next() 再呼叫 .remove()。
程式碼 1 中的 foreach 則是每次迴圈先呼叫 .remove() 再呼叫 .next(),導致 ConcurrentModificationException 異常,如果你想深入瞭解,可以看看 ArrayList.iterator() 的原始碼。
4 用 Hashtable 還是用 HashMap
一般來說,演算法中的 Hashtable 是一種常見的資料結構的名字。但是在 Java 中,這種資料結構的名字卻是 HashMap,不是 Hashtable。Java 中 Hashtable 和 HashMap 的最重要的區別之一是 Hashtable 是同步的(synchronized)。因此大部分時候你不需要用 Hashtable,應該用 HashMap。
5 直接使用 Collection 的原始型別時出錯
在 Java 中,「原始型別」和「無限制萬用字元型別」很容易被搞混。舉例來說,Set 是一個原始型別,而 Set<?> 是一個無限制萬用字元型別。
下面的程式碼中的 add 接受原始型別 List 作為引數:
public static void add(List list, Object o){
list.add(o);
}
public static void main(String[] args){
List<String> list = new ArrayList<String>();
add(list, 10);
String s = list.get(0);
}
複製程式碼
這個程式碼會在執行時才丟擲異常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at ...
複製程式碼
使用原始型別的 collection 是很危險的,因為原始型別沒有泛型檢查。Set / Set<?> / Set<Object>
之間有非常大的差異,詳情可以看看《Set vs. Set<?>》和《Java Type Erasure Mechanism》
6 訪問級別設定過高
很多開發者為了省事,把類欄位標記為 public,這不是個好習慣。好習慣應該是將訪問級別設定得越低越好。
7 ArrayList 和 LinkedList 選用錯誤
如果不瞭解 ArrayList 和 LinkedList 的區別,你很容易會傾向於使用 ArrayList,因為它看起來更常見。
但是,ArrayList 和 LinkedList 有巨大的效能差異。簡單來說,如果 add/remove 操作較多,則應該使用 LinkedList;如果隨機訪問操作較多,則應該使用 ArrayList。
如果你想深入瞭解這些效能差異,可以看看《ArrayList vs. LinkedList vs. Vector》
8 可變還是不可變?
不可變物件有很多好處,比如簡單、安全等。但是不可變對了要求每次改動都生成新的物件,物件一多就容易對垃圾回收造成壓力。我們應該在可變物件和不可變物件上找到一個平衡點。
一般來說,可變物件可以避免產生太多中間物件。一個經典的例子就是連結大量字串。如果你使用不可變字串,你就會造出許多中間物件,給垃圾回收造成壓力。這既浪費時間又消耗 CPU,所以這種情況下你應該使用可變物件,如 StringBuilder:
String result="";
for(String s: arr){
result = result + s;
}
複製程式碼
還有一些情況值得使用可變物件。比如你可以讓一個可變物件多次進出不同的方法,這樣你就可以收集多個結果。再比如排序和過濾操作,雖然你可以返回新的被排序之後的物件,但是如果元素數量眾多,這就會浪費不少記憶體。
9 超類和子類的建構函式
class Super {
String s;
public Super(String s){
this.s = s;
}
}
public class Sub extend Super{
int x = 200;
public Sub(String s){ // 編譯錯誤
}
public Sub(){ // 編譯錯誤
System.out.println("Sub");
}
public static void main(String[] args){
Sub s = new Sub();
}
}
複製程式碼
上述程式碼會有編譯錯誤,因為沒有實現 Super() 建構函式。Java 中,如果一個類沒有定義建構函式,編譯器將會插入一個預設的沒有引數的建構函式。但是如果 Super 類已經有了一個建構函式 Super(String s),那麼編譯器就不會插入這個預設的無引數的建構函式。這就是上述程式碼的遇到的情況。
Sub 類的兩個建構函式,一個有引數一個沒有引數,都會呼叫 Super 類的無引數建構函式。因為編譯器會嘗試在 Sub 類的兩個建構函式裡插入 super()
,由於 Super 類沒有無引數建構函式,所以編譯器就報錯了。
解決這個問題,有三種方法:
- 給 Super 類新增一個 Super() 無引數建構函式
- 刪掉 Super 類裡的有引數的建構函式
- 在 Sub 類的建構函式裡新增 super(value)
想了解更多詳情,可以看《Constructors of Sub and Super Classes in Java?》
10 用 "" 還是用建構函式
字串有兩種構造途徑:
// 1. 使用雙引號
String x = "abd";
// 2. 使用建構函式
String y = new String("abc");
複製程式碼
有什麼區別呢?
下面的程式碼可以很快的告訴你區別:
String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True
String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True
複製程式碼
想了解這兩種方式生成的字串在記憶體中是如何存在的,可以看看《Create Java String Using ” ” or Constructor?》
總結
這 10 個錯誤是我綜合 GitHub 上的專案、StackOverflow 上的問答和 Google 搜尋關鍵詞的趨勢而分析得出的。它們可能並不是真正的 10 個最多的錯誤,但還是挺普遍的。如果你有異議,可以給我留言。如果你能告訴我其他常見的錯誤,我會非常感謝你。