Java開發(fā)中最讓人頭疼的十個(gè)bug
前言
作為 Java 開發(fā),我們?cè)趯懘a的過程中難免會(huì)產(chǎn)生各種奇思妙想的 bug ,有些 bug 就挺讓人無奈的,比如說各種空指針異常,在 ArrayList 的迭代中進(jìn)行刪除操作引發(fā)異常,數(shù)組下標(biāo)越界異常等。
如果你不小心看到同事的代碼出現(xiàn)了我所描述的這些 bug 后,那你就把我這篇文章甩給他?。?!
廢話不多說,下面進(jìn)入正題。
錯(cuò)誤一:Array 轉(zhuǎn)換成 ArrayList
Array 轉(zhuǎn)換成 ArrayList 還能出錯(cuò)?這是哪個(gè)笨。。。。。。
等等,你先別著急說,先來看看是怎么回事。
如果要將數(shù)組轉(zhuǎn)換為 ArrayList,我們一般的做法會(huì)是這樣
List<String> list = Arrays.asList(arr);
Arrays.asList() 將返回一個(gè) ArrayList,它是 Arrays 中的私有靜態(tài)類,它不是 java.util.ArrayList 類。如下圖所示
Arrays 內(nèi)部的 ArrayList 只有 set、get、contains 等方法,但是沒有能夠像是 add 這種能夠使其內(nèi)部結(jié)構(gòu)進(jìn)行改變的方法,所以 Arrays 內(nèi)部的 ArrayList 的大小是固定的。
如果要?jiǎng)?chuàng)建一個(gè)能夠添加元素的 ArrayList ,你可以使用下面這種創(chuàng)建方式:
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));
因?yàn)?ArrayList 的構(gòu)造方法是可以接收一個(gè) Collection 集合的,所以這種創(chuàng)建方式是可行的。
錯(cuò)誤二:檢查數(shù)組是否包含某個(gè)值
檢查數(shù)組中是否包含某個(gè)值,部分程序員經(jīng)常會(huì)這么做:
Set<String> set = new HashSet<String>(Arrays.asList(arr)); return set.contains(targetValue);
這段代碼雖然沒錯(cuò),但是有額外的性能損耗,正常情況下,不用將其再轉(zhuǎn)換為 set,直接這么做就好了:
return Arrays.asList(arr).contains(targetValue);
或者使用下面這種方式(窮舉法,循環(huán)判斷)
for(String s: arr){ if(s.equals(targetValue)) return true; } return false;
上面第一段代碼比第二段更具有可讀性。
錯(cuò)誤三:在 List 中循環(huán)刪除元素
這個(gè)錯(cuò)誤我相信很多小伙伴都知道了,在循環(huán)中刪除元素是個(gè)禁忌,有段時(shí)間內(nèi)我在審查代碼的時(shí)候就喜歡看團(tuán)隊(duì)的其他小伙伴有沒有犯這個(gè)錯(cuò)誤。
說到底,為什么不能這么做(集合內(nèi)刪除元素)呢?且看下面代碼
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);
這個(gè)輸出結(jié)果你能想到么?是不是蠢蠢欲動(dòng)想試一波了?
答案其實(shí)是 [b,d]
為什么只有兩個(gè)值?我這不是循環(huán)輸出的么?
其實(shí),在列表內(nèi)部,當(dāng)你使用外部 remove 的時(shí)候,一旦 remove 一個(gè)元素后,其列表的內(nèi)部結(jié)構(gòu)會(huì)發(fā)生改變,一開始集合總?cè)萘渴?4,remove 一個(gè)元素之后就會(huì)變?yōu)?3,然后再和 i 進(jìn)行比較判斷。。。。。。所以只能輸出兩個(gè)元素。
你可能知道使用迭代器是正確的 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() 方法,結(jié)果。。。。。。ConcurrentModificationException
為啥呢?
那是因?yàn)槭褂?ArrayList 中外部 remove 元素,會(huì)造成其內(nèi)部結(jié)構(gòu)和游標(biāo)的改變。
在阿里開發(fā)規(guī)范上,也有不要在 for-each 循環(huán)內(nèi)對(duì)元素進(jìn)行 remove/add 操作的說明。
所以大家要使用 List 進(jìn)行元素的添加或者刪除操作,一定要使用迭代器進(jìn)行刪除。也就是
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() 之前調(diào)用。 在 foreach 循環(huán)中,編譯器會(huì)在刪除元素的操作后調(diào)用 .next(),導(dǎo)致ConcurrentModificationException。
錯(cuò)誤四:Hashtable 和 HashMap
這是一條算法方面的規(guī)約:按照算法的約定,Hashtable 是數(shù)據(jù)結(jié)構(gòu)的名稱,但是在 Java 中,數(shù)據(jù)結(jié)構(gòu)的名稱是 HashMap,Hashtable 和 HashMap 的主要區(qū)別之一就是 Hashtable 是同步的,所以很多時(shí)候你不需要 Hashtable ,而是使用 HashMap。
錯(cuò)誤五:使用原始類型的集合
這是一條泛型方面的約束:
在 Java 中,原始類型和無界通配符類型很容易混合在一起。以 Set 為例,Set 是原始類型,而 Set<?> 是無界通配符類型。
比如下面使用原始類型 List 作為參數(shù)的代碼:
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); }
這段代碼會(huì)拋出 java.lang.ClassCastException 異常,為啥呢?
使用原始類型集合是比較危險(xiǎn)的,因?yàn)樵碱愋蜁?huì)跳過泛型檢查而且不安全,Set、Set<?> 和 Set<Object> 存在巨大的差異,而且泛型在使用中很容易造成類型擦除。
大家都知道,Java 的泛型是偽泛型,這是因?yàn)?Java 在編譯期間,所有的泛型信息都會(huì)被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java 的泛型基本上都是在編譯器這個(gè)層次上實(shí)現(xiàn)的,在生成的字節(jié)碼中是不包含泛型中的類型信息的,使用泛型的時(shí)候加上類型參數(shù),在編譯器編譯的時(shí)候會(huì)去掉,這個(gè)過程成為類型擦除。
如在代碼中定義List<Object>和List<String>等類型,在編譯后都會(huì)變成List,JVM 看到的只是List,而由泛型附加的類型信息對(duì) JVM 是看不到的。Java 編譯器會(huì)在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯(cuò)的地方,但是仍然無法在運(yùn)行時(shí)刻出現(xiàn)的類型轉(zhuǎn)換異常的情況,類型擦除也是 Java 的泛型與 C++ 模板機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別。
比如下面這段示例
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()); } }
在這個(gè)例子中,我們定義了兩個(gè)ArrayList數(shù)組,不過一個(gè)是ArrayList<String>泛型類型的,只能存儲(chǔ)字符串;一個(gè)是ArrayList<Integer>泛型類型的,只能存儲(chǔ)整數(shù),最后,我們通過list1對(duì)象和list2對(duì)象的getClass()方法獲取他們的類的信息,最后發(fā)現(xiàn)結(jié)果為true。說明泛型類型String和Integer都被擦除掉了,只剩下原始類型。
所以,最上面那段代碼,把 10 添加到 Object 類型中是完全可以的,然而將 Object 類型的 "10" 轉(zhuǎn)換為 String 類型就會(huì)拋出類型轉(zhuǎn)換異常。
錯(cuò)誤六:訪問級(jí)別問題
我相信大部分開發(fā)在設(shè)計(jì) class 或者成員變量的時(shí)候,都會(huì)簡(jiǎn)單粗暴的直接聲明 public xxx,這是一種糟糕的設(shè)計(jì),聲明為 public 就很容易赤身裸體,這樣對(duì)于類或者成員變量來說,都存在一定危險(xiǎn)性。
錯(cuò)誤七:ArrayList 和 LinkedList
哈哈哈,ArrayList 是我見過程序員使用頻次最高的工具類,沒有之一。
當(dāng)開發(fā)人員不知道 ArrayList 和 LinkedList 的區(qū)別時(shí),他們經(jīng)常使用 ArrayList(其實(shí)實(shí)際上,就算知道他們的區(qū)別,他們也不用 LinkedList,因?yàn)檫@點(diǎn)性能不值一提),因?yàn)榭雌饋?ArrayList 更熟悉。。。。。。
但是實(shí)際上,ArrayList 和 LinkedList 存在巨大的性能差異,簡(jiǎn)而言之,如果添加/刪除操作大量且隨機(jī)訪問操作不是很多,則應(yīng)首選 LinkedList。如果存在大量的訪問操作,那么首選 ArrayList,但是 ArrayList 不適合進(jìn)行大量的添加/刪除操作。
錯(cuò)誤八:可變和不可變
不可變對(duì)象有很多優(yōu)點(diǎn),比如簡(jiǎn)單、安全等。但是不可變對(duì)象需要為每個(gè)不同的值分配一個(gè)單獨(dú)的對(duì)象,對(duì)象不具備復(fù)用性,如果這類對(duì)象過多可能會(huì)導(dǎo)致垃圾回收的成本很高。在可變和不可變之間進(jìn)行選擇時(shí)需要有一個(gè)平衡。
一般來說,可變對(duì)象用于避免產(chǎn)生過多的中間對(duì)象。 比如你要連接大量字符串。 如果你使用一個(gè)不可變的字符串,你會(huì)產(chǎn)生很多可以立即進(jìn)行垃圾回收的對(duì)象。 這會(huì)浪費(fèi) CPU 的時(shí)間和精力,使用可變對(duì)象是正確的解決方案(例如
String result=""; for(String s: arr){ result = result + s; }
所以,正確選擇可變對(duì)象還是不可變對(duì)象需要慎重抉擇。
錯(cuò)誤九:構(gòu)造函數(shù)
首先看一段代碼,分析為什么會(huì)編譯不通過?
發(fā)生此編譯錯(cuò)誤是因?yàn)槲炊x默認(rèn) Super 的構(gòu)造函數(shù)。 在 Java 中,如果一個(gè)類沒有定義構(gòu)造函數(shù),編譯器會(huì)默認(rèn)為該類插入一個(gè)默認(rèn)的無參數(shù)構(gòu)造函數(shù)。 如果在 Super 類中定義了構(gòu)造函數(shù),在這種情況下 Super(String s),編譯器將不會(huì)插入默認(rèn)的無參數(shù)構(gòu)造函數(shù)。 這就是上面 Super 類的情況。
要想解決這個(gè)問題,只需要在 Super 中添加一個(gè)無參數(shù)的構(gòu)造函數(shù)即可。
public Super(){ System.out.println("Super"); }
錯(cuò)誤十:到底是使用 "" 還是構(gòu)造函數(shù)
考慮下面代碼:
String x = "abc"; String y = new String("abc");
上面這兩段代碼有什么區(qū)別嗎?
可能下面這段代碼會(huì)給出你回答
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
這就是一個(gè)典型的內(nèi)存分配問題。
后記
今天我給你匯總了一下 Java 開發(fā)中常見的 10 個(gè)錯(cuò)誤,雖然比較簡(jiǎn)單,但是很容易忽視的問題,細(xì)節(jié)成就完美,看看你還會(huì)不會(huì)再犯了,如果再犯,嘿嘿嘿。
到此這篇關(guān)于Java開發(fā)中最讓人頭疼的十個(gè)bug的文章就介紹到這了,更多相關(guān)Java開發(fā)中的bug內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java弱引用(WeakReference)的理解與使用
這篇文章主要介紹了Java弱引用(WeakReference)的理解與使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Spring Boot示例分析講解自動(dòng)化裝配機(jī)制核心注解
這篇文章主要分析了Spring Boot 自動(dòng)化裝配機(jī)制核心注解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-07-07Java線程池隊(duì)列PriorityBlockingQueue和SynchronousQueue詳解
這篇文章主要為大家介紹了Java線程池隊(duì)列PriorityBlockingQueue和SynchronousQueue詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Spring?Boot實(shí)現(xiàn)文件上傳下載
這篇文章主要為大家詳細(xì)介紹了Spring?Boot實(shí)現(xiàn)文件上傳下載,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08