Java程序員常犯的10個錯誤


人非聖賢,孰能無過。都說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 。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM