Java 開發最容易寫的 10 個bug

程式設計師cxuan 發表於 2021-10-09
Java

原文連結:10 個讓人頭疼的 bug

那個誰,今天又寫 bug 了,沒錯,他說的好像就是我。。。。。。

作為 Java 開發,我們在寫程式碼的過程中難免會產生各種奇思妙想的 bug ,有些 bug 就挺讓人無奈的,比如說各種空指標異常,在 ArrayList 的迭代中進行刪除操作引發異常,陣列下標越界異常等。

如果你不小心看到同事的程式碼出現了我所描述的這些 bug 後,那你就把我這篇文章甩給他!!!你甩給他一篇文章,並讓他關注了一波 cxuan,你會收穫他在後面像是如獲至寶並滿眼崇拜大神的目光。

廢話不多說,下面進入正題。

錯誤一:Array 轉換成 ArrayList

Array 轉換成 ArrayList 還能出錯?這是哪個笨。。。。。。

等等,你先彆著急說,先來看看是怎麼回事。

如果要將陣列轉換為 ArrayList,我們一般的做法會是這樣

List<String> list = Arrays.asList(arr);

Arrays.asList() 將返回一個 ArrayList,它是 Arrays 中的私有靜態類,它不是 java.util.ArrayList 類。如下圖所示

image-20211005232205213

Arrays 內部的 ArrayList 只有 set、get、contains 等方法,但是沒有能夠像是 add 這種能夠使其內部結構進行改變的方法,所以 Arrays 內部的 ArrayList 的大小是固定的。

image-20211006094537453

如果要建立一個能夠新增元素的 ArrayList ,你可以使用下面這種建立方式:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

因為 ArrayList 的構造方法是可以接收一個 Collection 集合的,所以這種建立方式是可行的。

image-20211006094827686

錯誤二:檢查陣列是否包含某個值

檢查陣列中是否包含某個值,部分程式設計師經常會這麼做:

Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);

這段程式碼雖然沒錯,但是有額外的效能損耗,正常情況下,不用將其再轉換為 set,直接這麼做就好了:

return Arrays.asList(arr).contains(targetValue);

或者使用下面這種方式(窮舉法,迴圈判斷)

for(String s: arr){
	if(s.equals(targetValue))
		return true;
}
return false;

上面第一段程式碼比第二段更具有可讀性。

錯誤三:在 List 中迴圈刪除元素

這個錯誤我相信很多小夥伴都知道了,在迴圈中刪除元素是個禁忌,有段時間內我在審查程式碼的時候就喜歡看團隊的其他小夥伴有沒有犯這個錯誤。

image-20211006101709155

說到底,為什麼不能這麼做(集合內刪除元素)呢?且看下面程式碼

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]

為什麼只有兩個值?我這不是迴圈輸出的麼?

其實,在列表內部,當你使用外部 remove 的時候,一旦 remove 一個元素後,其列表的內部結構會發生改變,一開始集合總容量是 4,remove 一個元素之後就會變為 3,然後再和 i 進行比較判斷。。。。。。所以只能輸出兩個元素。

你可能知道使用迭代器是正確的 remove 元素的方式,你還可能知道 for-each 和 iterator 這種工作方式類似,所以你寫下了如下程式碼

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
 
for (String s : list) {
	if (s.equals("a"))
		list.remove(s);
}

然後你充滿自信的 run xxx.main() 方法,結果。。。。。。ConcurrentModificationException

為啥呢?

那是因為使用 ArrayList 中外部 remove 元素,會造成其內部結構和遊標的改變。

在阿里開發規範上,也有不要在 for-each 迴圈內對元素進行 remove/add 操作的說明。

image-20211006100608623

所以大家要使用 List 進行元素的新增或者刪除操作,一定要使用迭代器進行刪除。也就是

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 迴圈中,編譯器會在刪除元素的操作後呼叫 .next(),導致ConcurrentModificationException。

錯誤四:Hashtable 和 HashMap

這是一條演算法方面的規約:按照演算法的約定,Hashtable 是資料結構的名稱,但是在 Java 中,資料結構的名稱是 HashMap,Hashtable 和 HashMap 的主要區別之一就是 Hashtable 是同步的,所以很多時候你不需要 Hashtable ,而是使用 HashMap。

錯誤五:使用原始型別的集合

這是一條泛型方面的約束:

在 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);
}

這段程式碼會丟擲 java.lang.ClassCastException 異常,為啥呢?

image-20211006162921268

使用原始型別集合是比較危險的,因為原始型別會跳過泛型檢查而且不安全,Set、Set<?> 和 Set<Object> 存在巨大的差異,而且泛型在使用中很容易造成型別擦除。

大家都知道,Java 的泛型是偽泛型,這是因為 Java 在編譯期間,所有的泛型資訊都會被擦掉,正確理解泛型概念的首要前提是理解型別擦除。Java 的泛型基本上都是在編譯器這個層次上實現的,在生成的位元組碼中是不包含泛型中的型別資訊的,使用泛型的時候加上型別引數,在編譯器編譯的時候會去掉,這個過程成為型別擦除

如在程式碼中定義List<Object>List<String>等型別,在編譯後都會變成List,JVM 看到的只是List,而由泛型附加的型別資訊對 JVM 是看不到的。Java 編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法在執行時刻出現的型別轉換異常的情況,型別擦除也是 Java 的泛型與 C++ 模板機制實現方式之間的重要區別。

比如下面這段示例

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}

在這個例子中,我們定義了兩個ArrayList陣列,不過一個是ArrayList<String>泛型型別的,只能儲存字串;一個是ArrayList<Integer>泛型型別的,只能儲存整數,最後,我們通過list1物件和list2物件的getClass()方法獲取他們的類的資訊,最後發現結果為true。說明泛型型別StringInteger都被擦除掉了,只剩下原始型別。

所以,最上面那段程式碼,把 10 新增到 Object 型別中是完全可以的,然而將 Object 型別的 "10" 轉換為 String 型別就會丟擲型別轉換異常。

錯誤六:訪問級別問題

我相信大部分開發在設計 class 或者成員變數的時候,都會簡單粗暴的直接宣告 public xxx,這是一種糟糕的設計,宣告為 public 就很容易赤身裸體,這樣對於類或者成員變數來說,都存在一定危險性。

錯誤七:ArrayList 和 LinkedList

哈哈哈,ArrayList 是我見過程式設計師使用頻次最高的工具類,沒有之一。

image-20211006165557687

當開發人員不知道 ArrayList 和 LinkedList 的區別時,他們經常使用 ArrayList(其實實際上,就算知道他們的區別,他們也不用 LinkedList,因為這點效能不值一提),因為看起來 ArrayList 更熟悉。。。。。。

但是實際上,ArrayList 和 LinkedList 存在巨大的效能差異,簡而言之,如果新增/刪除操作大量且隨機訪問操作不是很多,則應首選 LinkedList。如果存在大量的訪問操作,那麼首選 ArrayList,但是 ArrayList 不適合進行大量的新增/刪除操作。

錯誤八:可變和不可變

不可變物件有很多優點,比如簡單、安全等。但是不可變物件需要為每個不同的值分配一個單獨的物件,物件不具備複用性,如果這類物件過多可能會導致垃圾回收的成本很高。在可變和不可變之間進行選擇時需要有一個平衡。

一般來說,可變物件用於避免產生過多的中間物件。 比如你要連線大量字串。 如果你使用一個不可變的字串,你會產生很多可以立即進行垃圾回收的物件。 這會浪費 CPU 的時間和精力,使用可變物件是正確的解決方案(例如 StringBuilder)。如下程式碼所示:

String result="";
for(String s: arr){
	result = result + s;
}

所以,正確選擇可變物件還是不可變物件需要慎重抉擇。

錯誤九:建構函式

首先看一段程式碼,分析為什麼會編譯不通過?

image-20211006172246303

發生此編譯錯誤是因為未定義預設 Super 的建構函式。 在 Java 中,如果一個類沒有定義建構函式,編譯器會預設為該類插入一個預設的無引數建構函式。 如果在 Super 類中定義了建構函式,在這種情況下 Super(String s),編譯器將不會插入預設的無引數建構函式。 這就是上面 Super 類的情況。

要想解決這個問題,只需要在 Super 中新增一個無引數的建構函式即可。

public Super(){
    System.out.println("Super");
}

錯誤十:到底是使用 "" 還是建構函式

考慮下面程式碼:

String x = "abc";
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

這就是一個典型的記憶體分配問題。

後記

今天我給你彙總了一下 Java 開發中常見的 10 個錯誤,雖然比較簡單,但是很容易忽視的問題,細節成就完美,看看你還會不會再犯了,如果再犯,嘿嘿嘿。

image-20211005230533419

點贊在看分享朋友圈是基操哦!快來一鍵三連!!!