寫在開頭
面試官:“小夥子,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成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!