1. 程式人生 > >阿里巴巴開發手冊解析個人筆記(三)集合處理

阿里巴巴開發手冊解析個人筆記(三)集合處理

文章目錄

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

  1. 【強制】使用集合轉陣列的方法,必須使用集合的 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);