人非聖賢,孰能無過。都說Java語言是一門簡單的編程語言,基於C++演化而來,剔除了很多C++中的復雜特性,但這並不能保證Java程序員不會犯錯。那么對於廣大的Java程序員來說,它們最常犯的10個錯誤是什么呢?本文通過總結出Java程序員最常犯的10大錯誤,可以有效地幫組Java后來者少走彎路,少加班,並寫出更健壯的應用程序。
數組轉ArrayList
為了實現把一個數組轉換成一個ArrayList,很多Java程序員會使用如下的代碼:
List<String> list = Arrays.asList(arr);
Arrays.asList確實會返回一個 ArrayList對象,但是該類是 Arrays類 中一個私有靜態內部類,而不是常見的 java.util.ArrayList類。這個 java.util.Arrays.ArrayList 類具有 set(),get(),contains()等方法,但是不具有任何添加或移除元素的任何方法。因為該類的大小(size)是固定的。為了創建出一個真正的 java.util.ArrayList,代碼應該如下所示:
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));
我們知道,ArrayList的構造方法可以接受一個 Collection 類型的對象,而我們的 java.util.Arrays.ArrayList 正好也是它的一個子類。實際上,更加高效的代碼示例是:
ArrayList<String> arrayList = new ArrayList<String>(arr.length); Collections.addAll(arrayList, arr);
關於這個問題的細節,建議參看本站的這篇文章。
數組是否包含特定值
為了檢查數組中是否包含某個特定值,很多Java程序員會使用如下的代碼:
Set<String> set = new HashSet<String>(Arrays.asList(arr)); return set.contains(targetValue);
就功能而言,該代碼是正確無誤的,但在數組轉List,List再轉Set的過程中消耗了大量的性能。我們可以優化成如下形式:
Arrays.asList(arr).contains(targetValue);
或者,進一步優化成如下所示最高效的代碼:
for(String s: arr){ if(s.equals(targetValue)) return true; } return false;
關於這個問題的細節,建議參看本站的這篇文章。
在迭代時移除List中的元素
首先,看一下在迭代過程中移除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的大小(size)就會縮減,同時也改變了索引的指向。所以,在迭代的過程中使用索引,將無法從List中正確地刪除多個指定的元素。
你可能知道解決這個錯誤的方式之一是使用迭代器(iterator)。而且,你可能認為Java中的 foreach 語句與迭代器(iterator)是非常相似的,但實際情況並不是這樣。我們考慮一下如下的示例代碼:
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d")); for (String s : list) { if (s.equals("a")) list.remove(s); }
這個示例代碼會拋出來一個 ConcurrentModificationException。我們應該修改成如下所示:
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()方法之前被調用。在 foreach 循環中,編譯器使得 remove()方法先於next()方法被調用,這就導致了 ConcurrentModificationException 異常。具體細節可以查看 ArrayList.iterator()的源碼。
Hashtable vs HashMap
學習過數據結構的讀者都知道一種非常重要的數據結構叫做 哈希表。在Java中,對應哈希表的的類是 HashMap 而不是 Hashtable。HashMap與Hashtable之間的最核心區別就是:HashMap是非同步的,Hashtable是同步的。關於它們之間的更多細節與比較,建議參看本站的這篇文章。
在Collection中使用原始類型
在Java中,很容易把原始類型與無限通配類型混淆。我們舉個Set相關的例子:Set就是原始類型;Set<?>就是無限通配類型。我們看一個使用在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之間具有非常大的差異,具體細節,建議參看本站的這篇文章。
訪問權限
很多的Java初學者喜歡使用 public 來修飾類的成員。這樣可以很方便地直接訪問和存取該成員。但是,這是一種非常糟糕的編程風格,正確的設計風格應該是盡可能降低類成員的訪問權限。具體細節,建議參看本站的這篇文章。
ArrayList vs LinkedList
很多的Java初學者不明白ArrayList與LinkedList之間的區別,所以,他們完全只用相對簡單的ArrayList,甚至不知道JDK中還存在LinkedList。但是,在某些具體場景下,這兩種List的選擇會導致程序性能的巨大差異。簡單而言:當應用場景中有很多的 add/remove 操作,只有少量的隨機訪問操作時,應該選擇LinkedList;在其他的場景下,考慮使用ArrayList。關於ArrayList與LinkedList之間的更多細節,建議參看本站的這篇文章。
可變 vs 不可變
不可變的對象具有非常多的優勢,比如簡單,安全等。但是,對於每一個不同的值,都需要該類的一個對象。而且,生成很多對象帶來的問題就是可能導致頻繁的垃圾回收。所以,在選擇可變類還是不可變類時,應該綜合考慮后再做抉擇。
通常而言,可變對象可以避免創建大量的中間對象。一個非常經典的例子就是鏈接大量的短String對象為一個長的String對象。如果使用不可變String類,鏈接的過程將產生大量的,適合立即被垃圾回收的中間String對象,這將消耗大量的CPU性能和內存空間。此時,使用一個可變的StringBuilder或StringBuffer才是正確的。
String result=""; for(String s: arr){ result = result + s; }
除了上述情況,可變對象在其他場景下可能由於不可變對象。比如,傳遞一個可變的對象到方法內部,利用該對象可以收集多個結果,而不用在多個循環層次中跳進跳出。有興趣的讀者可以參看本站的這篇文章。
繼承中的構造函數
上圖中出現的兩個編譯時錯誤是因為:父類中沒有定義默認構造函數,而子類中又調用了父類的默認構造函數。在Java中,如果一個類不定義任何構造函數,編譯期將自動插入一個默認構造函數到給類中。一旦一個類定義了任何一個構造函數,編譯期就不會插入任何構造函數到類中。在上面的示例中,Super類定義了一個參數類型為String的構造函數,所以該類中只有一個構造函數,不會有默認構造函數了。
&emps;在我們的子類 Sub 中,我們定義了兩個構造函數:一個參數類型為String的構造函數,另一個為午餐的默認函數。由於它們都沒有在函數體的第一行指定調用父類的哪一個構造函數,所以它們都需要調用父類 Super 的默認構造函數。但是,父類 Super 的默認構造函數是不存在的,所以編譯器報告了這兩個錯誤信息。關於解決方案和更多細節,建議參看本站的這篇文章。
字符串對象的兩個構建方式
Java中的字符串對象具有兩個常見的創建方式:
//1. use double quotes String x = "abc"; //2. use constructor 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
關於兩個方式的細節和對象的內存布局,建議參看 [天天編碼](www.tiantianbianma.com)。想要及時查看更多好文章,請使用微信掃描二維碼關注公眾號:TianTianBianMa 。