面試官:集合使用時應該注意哪些問題?我:應該注意該注意的問題!

JavaBuild發表於2024-03-04

寫在開頭

面試官:“小夥子,java的集合學過嗎?”
我:“肯定學過呀!”,這時候的我自信滿滿,手撕集合八股文嘛,早已背的滾瓜爛熟了呀。
面試官:“那你來講講集合使用時,應該注意哪些問題吧”
我:“額,這,我想想哈。”,什麼!這面試官不按套路出牌,上來就問注意事項,打我一個措手不及啊。
我:“嗯 ~,我覺得應該注意該注意的問題!”
面試官:“下一位!”

集合使用注意事項

經過了十幾篇部落格的總結,java集合部分的知識點,大致上就學完了,當然,Collection與Map擁有著大量的子集,我們無法透過短短的五六萬字就可以全部講解完,後續會持續性的完善,現階段呢,我們就先講那麼多哈。
今天,我們結合《阿里巴巴 Java 開發手冊》,來對集合日常開發使用過程中的注意事項進行總結,大致可以分為以下幾點。

集合判空

判空是集合在使用時必須要做的操作,我們得保證我們所建立的,或者所呼叫的別人建立的集合物件可用(不為null,不為空),才能進行下一步業務邏輯的開發。
那麼,如何進行判空處理呢?我們這裡以ArrayList為例,去列舉一下它的判空處理方式。

【程式碼示例1】

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        //方式一,list != null && !list.isEmpty()
        if (list != null && !list.isEmpty()) {
            for (Integer integer : list) {
                System.out.println("方式1:"+list);
            }
        } else {
            System.out.println("ArrayList讀取異常!");
        }
        //方式二,list != null && list.size() > 0
        if (list != null && list.size() > 0) {
            for (Integer integer : list) {
                System.out.println("方式2:"+list);
            }
        } else {
            System.out.println("ArrayList讀取異常!");
        }
        //方式三,org.apache.commons.collections包下的 CollectionUtils工具類
        if (CollectionUtils.isNotEmpty(list)) {
            for (Integer integer : list) {
                System.out.println("方式2:"+list);
            }
        } else {
            System.out.println("ArrayList讀取異常!");
        }
    }
}

我們在這裡列舉了3種判空方式,那這3種方式之間又有何區別呢?讓俺來分析一波。

第一點: 我們要知道null與空的區別,這是兩個概念,很多初學者會混淆,為null表示這個list還沒有分配記憶體,也就在堆中不存在,而空表示list的初始化工作已經完成,只不過裡面沒有任何元素。
我們在判空的時候需要注意,!=null 要放在&&邏輯與的前面判斷,因為,我們首先要保證list的初始化完成,才能去判斷集合元素的是否存在,否則會報nullException。
第二點: list.isEmpty() 與 list.size() == 0功能實現上一致,但在《阿里巴巴 Java 開發手冊》中指出:

判斷所有集合內部的元素是否為空,使用 isEmpty() 方法,而不是 size()==0 的方式

這是因為 isEmpty() 方法的可讀性更好,並且時間複雜度為 O(1)。絕大部分我們使用的集合的 size() 方法的時間複雜度也是 O(1),不過,也有很多複雜度不是 O(1) 的,比如 java.util.concurrent 包下的某些集合(ConcurrentLinkedQueue、ConcurrentHashMap)。
以ConcurrentHashMap為例,我們可以看一下它底層關於size()與isEmpty()的實現

【原始碼解析1】

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
public boolean isEmpty() {
    return sumCount() <= 0L; // ignore transient negative values
}

集合去重

很多場景下,我們都要求資料的唯一性,也就是不可重複,所以集合的去重本領我們也要掌握,在《阿里巴巴 Java 開發手冊》中這樣說道:

可以利用 Set 元素唯一的特性,可以快速對一個集合進行去重操作,避免使用 List 的 contains()
進行遍歷去重或者判斷包含操作。

這是為什麼呢?我們依舊需要透過原始碼去分析問題,分別選擇HashSet和ArrayList,其實兩者的差別主要體現在對contains()的實現上。

【HashSet去重核心】

private transient HashMap<E,Object> map;
public boolean contains(Object o) {
    return map.containsKey(o);
}

HashSet 的 contains() 方法底部依賴的 HashMap 的 containsKey() 方法,時間複雜度接近於 O(1)(沒雜湊衝突下)。

【ArrayList去重核心】

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

而對於ArrayList來說,它的contains是透過遍歷元素實現,時間複雜度O(n),兩者一比,高下立現!

集合遍歷

集合元素的遍歷,可以說是隻要用集合,就無法避免的,之前寫了一篇關於HashMap的遍歷,還有一篇關於java中迭代器的文章,推薦大家去看看。
《HashMap的7種遍歷方式》
《java中的迭代器實現原理》

不過對於集合遍歷,在手冊中有個額外的規約

不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對Iterator 物件加鎖。

強行修改,會導致Iterator遍歷出錯,報ConcurrentModificationException異常。

集合轉陣列

對於集合轉為陣列的場景,《阿里巴巴 Java 開發手冊》也給了要求,如下:

使用集合轉陣列的方法,必須使用集合的 toArray(T[] array),傳入的是型別完全一致、長度為 0 的空陣列。

【程式碼示例2】

String [] s= new String[]{
    "I Love", "JavaBuild"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
//沒有指定型別的話會報錯
s=list.toArray(new String[0]);

注意:new String[0]就是起一個模板的作用,指定了返回陣列的型別,0 是為了節省空間,因為它只是為了說明返回的型別。

集合轉Map

集合除了會轉為陣列外,還可能會轉為Map,所以,我們在轉Map的時候,《阿里巴巴 Java 開發手冊》也給了約束。

在使用 java.util.stream.Collectors 類的 toMap() 方法轉為 Map 集合時,一定要注意當 value 為
null 時會拋 NPE 異常。

class Person {
    private String name;
    private String phoneNumber;
     // getters and setters
}
//test main()
List<Person> bookList = new ArrayList<>();
bookList.add(new Person("1","JavaBuild"));
bookList.add(new Person("2",null));
// 空指標異常
bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber));

這是為啥呢,我們跟入toMap中發現,內部呼叫了Map的merge()方法,跟入這個方法後,我們會發現

【原始碼解析】

default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

這原始碼裡首先執行了 Objects.requireNonNull(remappingFunction);這一句程式碼,用來判斷value值非空,並且做了丟擲NPE處理。

總結

以上就是結合開發手冊和自己平時開發經驗,寫的六點注意事項,希望所有小夥伴都能夠在日後的開發工作中,保持良好的開發規範與習慣,強烈建議每個人必看《阿里巴巴 Java 開發手冊》,這是很多網際網路企業,新員工入職必看書籍,雖然裡面有些內容,個人感覺有點矯枉過正,但90%以上的約定都非常必要!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

如果您想與Build哥的關係更近一步,還可以關注俺滴公眾號“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

相關文章