阿里巴巴開發手冊解析個人筆記(三)集合處理
文章目錄
1. 【強制】 關於 hashCode 和 equals 的處理,遵循如下規則:
1) 只要重寫 equals,就必須重寫 hashCode。
2) 因為 Set 儲存的是不重複的物件,依據 hashCode 和 equals 進行判斷,所以 Set 儲存的
物件必須重寫這兩個方法。
3) 如果自定義物件作為 Map 的鍵,那麼必須重寫 hashCode 和 equals。
說明: String 重寫了 hashCode 和 equals 方法,所以我們可以非常愉快地使用 String 物件
作為 key 來使用。
解析:
例項程式
public static void main(String[] args) { HashSet a = new HashSet(); a.add("test"); a.add("test"); a.add("test"); a.add("test"); }
在Set中有這樣的原始碼
Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
可見一直在判斷e.hash和key是否Equals,所以看到重寫的重要性
下一個
2. 【強制】 ArrayList的subList結果不可強轉成ArrayList,否則會丟擲 ClassCastException 異常, 即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。 說明: subList 返回的是 ArrayList 的內部類 SubList,並不是 ArrayList 而是 ArrayList 的一個檢視,對於 SubList 子列表的所有操作最終會反映到原列表上。 3. 【強制】在 subList 場景中, 高度注意對原集合元素的增加或刪除, 均會導致子列表的遍歷、 增加、刪除產生 ConcurrentModificationException 異常
我們來檢視一下ArrayList.subList的原始碼
例項程式
public static void main(String[] args) {
ArrayList array = new ArrayList();
array.add(1);
array.add(2);
array.add(3);
array.add(4);
List s =array.subList(0, 3);
System.out.println(s);
}
原始碼
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
檢視繼承機構確實SubList是ArrayList的內部類,必須依賴外部介面的實現。
但他返回的是一個AbstractList物件,ArrayList也是返回了一個public class ArrayList extends AbstractList物件
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
這算是一個介面卡模式,要注意的是
SubList也實現了add,remove方法
檢視add方法
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
checkForComonfication()會檢查是否有別的執行緒修改了內容
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
ArrayList的add和remove方法
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
modCount 是檢查是否有其他執行緒同時修改了裡面的內容
所以要subList時, 不能夠在原來的ArrayList隨意的remove和add
詳細閱讀:https://www.jianshu.com/p/c5b52927a61a
- 【強制】使用集合轉陣列的方法,必須使用集合的 toArray(T[] array),傳入的是型別完全
一樣的陣列,大小就是 list.size()。
正解
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
下一個
6. 【強制】泛型萬用字元<? extends T>來接收返回的資料,此寫法的泛型集合不能使用 add 方
法, 而<? super T>不能使用 get 方法,作為介面呼叫賦值時易出錯。
我也不會
下一個
例項程式
public class MainTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("1")) {
iterator.remove();
}
}
list.add("1");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
}
}
檢視位元組碼發現
上面呼叫的是iterator.remove()方法,foreach下面呼叫的是List.remove方法
我們來看看裡面的內容
List.remove方法裡有
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("1")) {
iterator.remove();
}
}
list.add("1");
Iterator<String> iterator2 = list.iterator();
while (iterator2.hasNext()) {
String item = iterator.next();
if (item.equals("1")) {
list.remove(item); //這裡變化了
}
}
}
iterator裡有一個運算元記錄器modCount=0;
而list也有一個操作記錄數modCount=0; list.remove(item)中並沒有修改到Iteartor的修改書modCount,所以Iterator以為是併發修改了數組裡的內容,所以發生了
Exception in thread "main" java.util.ConcurrentModificationException
下一個
8. 【強制】 在 JDK7 版本及以上, Comparator 實現類要滿足如下三個條件,不然 Arrays.sort,
Collections.sort 會報 IllegalArgumentException 異常。Collections.sort 會報 IllegalArgumentException 異常。
說明: 三個條件如下
1) x, y 的比較結果和 y, x 的比較結果相反。
2) x>y, y>z, 則 x>z。
3) x=y, 則 x, z 比較結果和 y, z 比較結果相同
這個裡面的內容比較複雜,我沒看懂。。
實踐程式也正常
public class MainTest {
public static void main(String[] args) {
ArrayList<Student> a = new ArrayList<Student>();
Student s = new Student(1,"david");
Student s2 = new Student(1,"s2");
Student s3 = new Student(2,"s3");
Student s4 = new Student(3,"s4");
Student s5 = new Student(4,"s5");
Student s6 = new Student(5,"s6");
a.add(s);
a.add(s2);
a.add(s3);
a.add(s4);
a.add(s5);
a.add(s6);
Collections.sort(a,new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
});
System.out.println(a);
}
}
class Student {
Integer id;
String name;
public Student(Integer id, String name) {
super();
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + "]";
}
}
9. 【推薦】 集合泛型定義時, 在 JDK7 及以上,使用 diamond 語法或全省略。
說明: 菱形泛型,即 diamond, 直接使用<>來指代前邊已經指定的型別。
正例:
// <> diamond 方式
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
個人解析:
感覺還是寫全語法比較好
下一個
10. 【推薦】集合初始化時, 指定集合初始值大小。
說明: HashMap 使用 HashMap(int initialCapacity) 初始化。
正例:initialCapacity = (需要儲存的元素個數 / 負載因子) + 1。注意負載因子(即 loader
factor) 預設為 0.75, 如果暫時無法確定初始值大小,請設定為 16(即預設值) 。
反例: HashMap 需要放置 1024 個元素, 由於沒有設定容量初始大小,隨著元素不斷增加,容
量 7 次被迫擴大, resize 需要重建 hash 表,嚴重影響效能。
已下降為解析轉載來自:
https://cloud.tencent.com/info/9bdbd5c9a03e27a5b6005c005d854829.html
例項程式
int aHundredMillion = 10000000;
Map<Integer, Integer> map = new HashMap<>();
long s1 = System.currentTimeMillis();
for (int i = 0; i < aHundredMillion; i++) {
map.put(i, i);
}
long s2 = System.currentTimeMillis();
System.out.println("未初始化容量,耗時 : " + (s2 - s1));
Map<Integer, Integer> map1 = new HashMap<>(aHundredMillion / 2);
long s5 = System.currentTimeMillis();
for (int i = 0; i < aHundredMillion; i++) {
map1.put(i, i);
}
long s6 = System.currentTimeMillis();
System.out.println("初始化容量5000000,耗時 : " + (s6 - s5));
Map<Integer, Integer> map2 = new HashMap<>(aHundredMillion);
long s3 = System.currentTimeMillis();
for (int i = 0; i < aHundredMillion; i++) {
map2.put(i, i);
}
long s4 = System.currentTimeMillis();
System.out.println("初始化容量為10000000,耗時 : " + (s4 - s3));
檢視原始碼發現
為什麼是7次?
他的初始容量是16,每次增大兩倍並且重組hashMap
所以16+32+64+128+256+512=944 +1024 才能夠裝1024個數據,但重組了7次浪費了很多的效率
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 判斷是否超過最大容量值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 容量擴大為原來的兩倍,oldCap大於等於16
newThr = oldThr << 1; // 雙倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 當沒設定值時初始化變數
newCap = DEFAULT_INITIAL_CAPACITY; //預設值是16,下面是16*0.75=12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
....
下一個
11. 【推薦】使用 entrySet 遍歷 Map 類集合 KV,而不是 keySet 方式進行遍歷。
說明: keySet 其實是遍歷了 2 次,一次是轉為 Iterator 物件,另一次是從 hashMap 中取出
key 所對應的 value。而 entrySet 只是遍歷了一次就把 key 和 value 都放到了 entry 中,效
率更高。如果是 JDK8,使用 Map.foreach 方法。
正例: values()返回的是 V 值集合,是一個 list 集合物件; keySet()返回的是 K 值集合,是
一個 Set 集合物件; entrySet()返回的是 K-V 值組合集合。
參考了文章https://blog.csdn.net/chajinglong/article/details/79194967
例項程式
Map map = new HashMap();
map.put("1", "david");
map.put("2", "way");
map.put("3", "test");
map.put("4", "hehe");
map.put("5", "bgg");
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
System.out.println("key=");
}
Map map2 = new HashMap();
Iterator iter2 = map2.keySet().iterator();
while (iter2.hasNext()) {
Object key = iter.next();
Object val = map.get(key);
}
12.【推薦】高度注意 Map 類集合 K/V 能不能儲存 null 值的情況,如下表格:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20181210003549800.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA0OTg3NTM=,size_16,color_FFFFFF,t_70)
後面探討一下為什麼執行緒不安全和不能為null
13.【參考】合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和
不穩定性(unorder)帶來的負面影響。
說明: 有序性是指遍歷的結果是按某種比較規則依次排列的。 穩定性指集合每次遍歷的元素次
序是一定的。 如: ArrayList 是 order/unsort; HashMap 是 unorder/unsort; TreeSet 是
order/sort。
這個我沒看懂
下一個
14.【參考】利用 Set 元素唯一的特性,可以快速對一個集合進行去重操作,避免使用 List 的
contains 方法進行遍歷、對比、 去重操作。
這個不太需要解析,但提供一個程式碼說明
List list = new ArrayList(set);
Set set = new HashSet(list);