Java 開發(fā)中十個讓人頭疼的 Bug
那個誰,今天又寫 bug 了,沒錯,他說的好像就是我。。。。。。
作為 Java 開發(fā),我們在寫代碼的過程中難免會產生各種奇思妙想的 bug ,有些 bug 就挺讓人無奈的,比如說各種空指針異常,在 ArrayList 的迭代中進行刪除操作引發(fā)異常,數組下標越界異常等。
如果你不小心看到同事的代碼出現(xiàn)了我所描述的這些 bug 后,那你就把我這篇文章甩給他!!!你甩給他一篇文章,并讓他關注了一波 cxuan,你會收獲他在后面像是如獲至寶并滿眼崇拜大神的目光。
廢話不多說,下面進入正題。
錯誤一:Array 轉換成 ArrayList
Array 轉換成 ArrayList 還能出錯?這是哪個笨。。。。。。
等等,你先別著急說,先來看看是怎么回事。
如果要將數組轉換為 ArrayList,我們一般的做法會是這樣
- List<String> list = Arrays.asList(arr);
Arrays.asList() 將返回一個 ArrayList,它是 Arrays 中的私有靜態(tài)類,它不是 java.util.ArrayList 類。如下圖所示
Arrays 內部的 ArrayList 只有 set、get、contains 等方法,但是沒有能夠像是 add 這種能夠使其內部結構進行改變的方法,所以 Arrays 內部的 ArrayList 的大小是固定的。
如果要創(chuàng)建一個能夠添加元素的 ArrayList ,你可以使用下面這種創(chuàng)建方式:
- ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));
因為 ArrayList 的構造方法是可以接收一個 Collection 集合的,所以這種創(chuàng)建方式是可行的。
錯誤二:檢查數組是否包含某個值
檢查數組中是否包含某個值,部分程序員經常會這么做:
- Set<String> set = new HashSet<String>(Arrays.asList(arr));
- return set.contains(targetValue);
這段代碼雖然沒錯,但是有額外的性能損耗,正常情況下,不用將其再轉換為 set,直接這么做就好了:
- return Arrays.asList(arr).contains(targetValue);
或者使用下面這種方式(窮舉法,循環(huán)判斷)
- for(String s: arr){
- if(s.equals(targetValue))
- return true;
- }
- return false;
上面第一段代碼比第二段更具有可讀性。
錯誤三:在 List 中循環(huán)刪除元素
這個錯誤我相信很多小伙伴都知道了,在循環(huán)中刪除元素是個禁忌,有段時間內我在審查代碼的時候就喜歡看團隊的其他小伙伴有沒有犯這個錯誤。
說到底,為什么不能這么做(集合內刪除元素)呢?且看下面代碼
- 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]
為什么只有兩個值?我這不是循環(huán)輸出的么?
其實,在列表內部,當你使用外部 remove 的時候,一旦 remove 一個元素后,其列表的內部結構會發(fā)生改變,一開始集合總容量是 4,remove 一個元素之后就會變?yōu)?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 元素,會造成其內部結構和游標的改變。
在阿里開發(fā)規(guī)范上,也有不要在 for-each 循環(huán)內對元素進行 remove/add 操作的說明。
所以大家要使用 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 循環(huán)中,編譯器會在刪除元素的操作后調用 .next(),導致ConcurrentModificationException。
錯誤四:Hashtable 和 HashMap
這是一條算法方面的規(guī)約:按照算法的約定,Hashtable 是數據結構的名稱,但是在 Java 中,數據結構的名稱是 HashMap,Hashtable 和 HashMap 的主要區(qū)別之一就是 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 異常,為啥呢?
使用原始類型集合是比較危險的,因為原始類型會跳過泛型檢查而且不安全,Set、Set 和 Set 存在巨大的差異,而且泛型在使用中很容易造成類型擦除。
大家都知道,Java 的泛型是偽泛型,這是因為 Java 在編譯期間,所有的泛型信息都會被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java 的泛型基本上都是在編譯器這個層次上實現(xiàn)的,在生成的字節(jié)碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成為類型擦除。
如在代碼中定義List和List
比如下面這段示例:
- 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
所以,最上面那段代碼,把 10 添加到 Object 類型中是完全可以的,然而將 Object 類型的 "10" 轉換為 String 類型就會拋出類型轉換異常。
錯誤六:訪問級別問題
我相信大部分開發(fā)在設計 class 或者成員變量的時候,都會簡單粗暴的直接聲明 public xxx,這是一種糟糕的設計,聲明為 public 就很容易赤身裸體,這樣對于類或者成員變量來說,都存在一定危險性。
錯誤七:ArrayList 和 LinkedList
哈哈哈,ArrayList 是我見過程序員使用頻次最高的工具類,沒有之一。
當開發(fā)人員不知道 ArrayList 和 LinkedList 的區(qū)別時,他們經常使用 ArrayList(其實實際上,就算知道他們的區(qū)別,他們也不用 LinkedList,因為這點性能不值一提),因為看起來 ArrayList 更熟悉。。。。。。
但是實際上,ArrayList 和 LinkedList 存在巨大的性能差異,簡而言之,如果添加/刪除操作大量且隨機訪問操作不是很多,則應首選 LinkedList。如果存在大量的訪問操作,那么首選 ArrayList,但是 ArrayList 不適合進行大量的添加/刪除操作。
錯誤八:可變和不可變
不可變對象有很多優(yōu)點,比如簡單、安全等。但是不可變對象需要為每個不同的值分配一個單獨的對象,對象不具備復用性,如果這類對象過多可能會導致垃圾回收的成本很高。在可變和不可變之間進行選擇時需要有一個平衡。
一般來說,可變對象用于避免產生過多的中間對象。比如你要連接大量字符串。如果你使用一個不可變的字符串,你會產生很多可以立即進行垃圾回收的對象。這會浪費 CPU 的時間和精力,使用可變對象是正確的解決方案(例如 StringBuilder)。如下代碼所示:
- String result="";
- for(String s: arr){
- result = result + s;
- }
所以,正確選擇可變對象還是不可變對象需要慎重抉擇。
錯誤九:構造函數
首先看一段代碼,分析為什么會編譯不通過?
發(fā)生此編譯錯誤是因為未定義默認 Super 的構造函數。在 Java 中,如果一個類沒有定義構造函數,編譯器會默認為該類插入一個默認的無參數構造函數。如果在 Super 類中定義了構造函數,在這種情況下 Super(String s),編譯器將不會插入默認的無參數構造函數。這就是上面 Super 類的情況。
要想解決這個問題,只需要在 Super 中添加一個無參數的構造函數即可。
- public Super(){
- System.out.println("Super");
- }
錯誤十:到底是使用 "" 還是構造函數
考慮下面代碼:
- String x = "abc";
- String y = new String("abc");
上面這兩段代碼有什么區(qū)別嗎?
可能下面這段代碼會給出你回答
- 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 開發(fā)中常見的 10 個錯誤,雖然比較簡單,但是很容易忽視的問題,細節(jié)成就完美,看看你還會不會再犯了,如果再犯,嘿嘿嘿。
本文轉載自微信公眾號「程序員cxuan」,可以通過以下二維碼關注。轉載本文請聯(lián)系程序員cxuan公眾號。