扯淡 Java 集合

此次必定?
此次必定?

大體分類:List、Set、Queue、Maphtml

Iterable

Collection 接口中繼承 Iterable 接口。這個接口爲 for each 循環設計、接口方法中有返回Iterator對象java

public interface Iterable<T> {
  Iterator<T> iterator();   default void forEach(Consumer<? super T> action) {  Objects.requireNonNull(action);  for (T t : this) {  action.accept(t);  }  }   default Spliterator<T> spliterator() {  return Spliterators.spliteratorUnknownSize(iterator(), 0);  } } 複製代碼

咱們看個例子來理解一下上面的話web

LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1); linkedList.add(2); linkedList.add(3);  for (Integer integer : linkedList) {  System.out.println(integer); } 複製代碼

反編譯以後算法

LinkedList<Integer> linkedList = new LinkedList();
linkedList.add(1); linkedList.add(2); linkedList.add(3); Iterator var4 = linkedList.iterator();  while(var4.hasNext()) {  Integer integer = (Integer)var4.next();  System.out.println(integer); } 複製代碼

Iterator

在 Iterable 接口中出現了這麼一個迭代器數組

public interface Iterator<E> {
  boolean hasNext();   E next();   default void remove() {  throw new UnsupportedOperationException("remove");  }   default void forEachRemaining(Consumer<? super E> action) {  Objects.requireNonNull(action);  while (hasNext())  action.accept(next());  } } 複製代碼

主要是爲了統一遍歷方式、使集合的數據結構和訪問方式解耦安全

咱們來看看最多見的 ArrayList 類中的內部類數據結構

private class Itr implements Iterator<E> {
 int cursor; // 下一次要返回的下標  int lastRet = -1; // 這一次next 要返回的下標  int expectedModCount = modCount; // 修改次數   public boolean hasNext() {  return cursor != size;  }   @SuppressWarnings("unchecked")  public E next() {  checkForComodification();  int i = cursor;  if (i >= size)  throw new NoSuchElementException();  Object[] elementData = ArrayList.this.elementData;  if (i >= elementData.length)  throw new ConcurrentModificationException();  cursor = i + 1;  return (E) elementData[lastRet = i];  }   public void remove() {  if (lastRet < 0)  throw new IllegalStateException();  checkForComodification();   try {  ArrayList.this.remove(lastRet);  cursor = lastRet;  lastRet = -1;  expectedModCount = modCount;  } catch (IndexOutOfBoundsException ex) {  throw new ConcurrentModificationException();  }  }   @Override  @SuppressWarnings("unchecked")  public void forEachRemaining(Consumer<? super E> consumer) {  Objects.requireNonNull(consumer);  final int size = ArrayList.this.size;  int i = cursor;  if (i >= size) {  return;  }  final Object[] elementData = ArrayList.this.elementData;  if (i >= elementData.length) {  throw new ConcurrentModificationException();  }  while (i != size && modCount == expectedModCount) {  consumer.accept((E) elementData[i++]);  }  // update once at end of iteration to reduce heap write traffic  cursor = i;  lastRet = i - 1;  checkForComodification();  }   final void checkForComodification() {  if (modCount != expectedModCount)  throw new ConcurrentModificationException();  } } 複製代碼

咱們都知道在 ArrayList 中 forEach 中的時候 remove 會致使 ConcurrentModificationException多線程

ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1); arrayList.add(1); arrayList.add(1);  for (Integer integer : arrayList) {  arrayList.remove(integer); } 複製代碼
Exception in thread "main" java.util.ConcurrentModificationException
複製代碼

而咱們使用 Iterator 進行 remove 的時候就不會有這個問題、併發

public void remove() {
 if (lastRet < 0)  throw new IllegalStateException();  checkForComodification();   try {  ArrayList.this.remove(lastRet);  cursor = lastRet;  lastRet = -1;  expectedModCount = modCount;  } catch (IndexOutOfBoundsException ex) {  throw new ConcurrentModificationException();  } } 複製代碼

List

ArrayList

  • 動態數組
  • 線程不安全
  • 元素容許爲 null
  • 實現了 List、RandomAccess、Cloneable、Serializable
  • 連續的內存空間
  • 增長和刪除都會致使 modCount 的值改變
  • 默認擴容爲一半

Vector

  • 線程安全
  • 擴容是上一次的一倍
  • 存在 modCount
  • 每一個操做數組的方法都加上了 synchronized

CopyOnWriteArrayList

  • 寫時複製、加鎖
  • 耗內存
  • 實時性不高
  • 不存在 ConcurrentModificationException
  • 數據量最好不要太大
  • 使用 ReentrantLock 進行加鎖

Collections.synchronizedList

  • synchronized 代碼塊
  • 對象鎖能夠參數傳進去、或者當前對象
  • 須要傳 List 對象進去
SynchronizedList(List<E> list) {
 super(list);  this.list = list; } SynchronizedList(List<E> list, Object mutex) {  super(list, mutex);  this.list = list; } 複製代碼

LinkedList

  • ArrayList 增刪效率低、改查效率高、而 LinkedList剛剛相反
  • 鏈表實現
  • for 循環的時候、根據 index 是靠近前半段仍是後半段來決定是順序仍是逆序
  • 增刪的時候會改變 modCount

Map

常見的四個實現類dom

  • HashMap
  • HashTable
  • LinkedHashMap
  • TreeMap

HashMap

HashMap 是數組+鏈表+紅黑樹(JDK1.8增長了紅黑樹部分)實現的,以下如所示。

transient Node<K,V>[] table;
// 實際存儲的 key-value 的數量 transient int size; // 閾值、當存放在 table 中的 key-value 大於這個值的時候須要進行擴容 int threshold; // 負載因子 由於 threshold = loadFactor * table.length final float loadFactor; 複製代碼

table 的長度默認是 16 、loadFactor 的默認值是 0.75

繼續看看 Node 的數據結構

static class Node<K,V> implements Map.Entry<K,V> {
 final int hash;  final K key;  V value;  Node<K,V> next; } 複製代碼

肯定哈希桶數組索引的位置

方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7  int h;  // h = key.hashCode() 爲第一步 取hashCode值  // h ^ (h >>> 16) 爲第二步 高位參與運算  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 方法二: static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8 直接使用裏面的方法體、沒有定義這個方法  return h & (length-1); //第三步 取模運算 }  JDK 1.8final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  boolean evict) {  Node<K,V>[] tab; Node<K,V> p; int n, i;  if ((tab = table) == null || (n = tab.length) == 0)  n = (tab = resize()).length;  // 這裏  if ((p = tab[i = (n - 1) & hash]) == null)  tab[i] = newNode(hash, key, value, null); ..................... .................... } 複製代碼

這裏的Hash算法本質上就是三步:取key的hashCode值高位運算取模運算

取模運算就是 h & (length - 1 ) 、其實它是等價於 h%length 、由於 length 老是 2 的 n 次方。由於 &比%具備更高的效率

(h = key.hashCode()) ^ (h >>> 16) 將 key 的 hashCode 與 它的高 16 位進行 異或的操做

其實爲啥這麼操做呢、是由於當 table 的數組的大小比較小的時候、key 的 hashCode 的高位信息就會直接被丟棄掉、這個時候就會增長了低位的衝突、因此將高位的信息經過異或保留下來

那其實爲啥要異或呢?雙目運算不是還有 & || 嗎

來自知乎的解答

方法一其實叫作一個擾動函數、hashCode的高位和低位作異或、就是爲了混合原始哈希碼的高位和低位、以此加大低位的隨機性、並且混合後的低位摻雜了高位的部分特徵、這樣高位的信息也被變相地保留下來 、通過擾動以後、有效減小了哈希衝突

至於這裏爲何使用異或運行、由於在雙目運算 & || ^ 中 異或是混洗效果最好的、結果佔雙目運算兩個數的50% 、混洗性是比較好的

https://www.zhihu.com/question/20733617/answer/111577937

https://codeday.me/bug/20170909/69679.html

關於 JDK 1.7 擴容致使循環鏈表問題

下面是 JDK 1.7 的擴容代碼

void resize(int newCapacity) {   //傳入新的容量
 2 Entry[] oldTable = table; //引用擴容前的Entry數組  3 int oldCapacity = oldTable.length;  4 if (oldCapacity == MAXIMUM_CAPACITY) { //擴容前的數組大小若是已經達到最大(2^30)了  5 threshold = Integer.MAX_VALUE; //修改閾值爲int的最大值(2^31-1),這樣之後就不會擴容了  6 return;  7 }  8  9 Entry[] newTable = new Entry[newCapacity]; //初始化一個新的Entry數組 10 transfer(newTable); //!!將數據轉移到新的Entry數組裏 11 table = newTable; //HashMap的table屬性引用新的Entry數組 12 threshold = (int)(newCapacity * loadFactor);//修改閾值 13 } 複製代碼
void transfer(Entry[] newTable) {
 2 Entry[] src = table; //src引用了舊的Entry數組  3 int newCapacity = newTable.length;  4 for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組  5 Entry<K,V> e = src[j]; //取得舊Entry數組的每一個元素  6 if (e != null) {  7 src[j] = null;//釋放舊Entry數組的對象引用(for循環後,舊的Entry數組再也不引用任何對象)  8 do {  9 Entry<K,V> next = e.next; 10 int i = indexFor(e.hash, newCapacity); //!!從新計算每一個元素在數組中的位置 11 e.next = newTable[i]; //標記[1] 12 newTable[i] = e; //將元素放在數組上 13 e = next; //訪問下一個Entry鏈上的元素 14 } while (e != null); 15 } 16 } 17 } 複製代碼

咱們先看看美團博客上面的例子

單線程環境下是正常完成擴容的、可是有沒有發現、倒置了、key7 在 key3 前面了。這個很關鍵

咱們再來看看多線程下、致使循環鏈表的問題

其實出現循環鏈表這種狀況、就是由於擴容的時候、鏈表倒置了

而 JDK1.8 中、使用兩個變量解決鏈表倒置而發生了循環鏈表的問題

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null; 複製代碼

經過 head 和 tail 兩個變量、將擴容時鏈表倒置的問題解決了、循環鏈表的問題就解決了

可是不管如何、在併發的狀況下、都會發生丟失數據的問題、就好比說上面的例子就丟失了 key5

HashTable

遺留類、不少功能和 HashMap 相似、可是它是線程安全的、可是任意時刻只能有一個線程寫 HashTable、併發性不如 ConcurrentHashMap,由於 ConcurrentHashMap 使用分段鎖。不建議使用

LinkedHashMap

LinkedHashMap繼承自HashMap、在HashMap基礎上、經過維護一條雙向鏈表、解決了HashMap不能隨時保持遍歷順序和插入順序一致的問題

重寫了 HashMap 的 newNode 方法

而且重寫了 afterNodeInsertion 方法、這個方法原本在 HashMap 中是空方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
 LinkedHashMap.Entry<K,V> first;  if (evict && (first = head) != null && removeEldestEntry(first)) {  K key = first.key;  removeNode(hash(key), key, null, false, true);  } } 複製代碼

而方法 removeEldestEntry 在 LinkedHashMap 中返回 false 、咱們能夠經過重寫此方法來實現一個 LRU 隊列的

/**  * The iteration ordering method for this linked hash map: <tt>true</tt>  * for access-order, <tt>false</tt> for insertion-order.  *  * @serial  */ final boolean accessOrder; 複製代碼

默認爲 false 遍歷的時候控制順序

TreeMap

static final class Entry<K,V> implements Map.Entry<K,V> {
 K key;  V value;  Entry<K,V> left;  Entry<K,V> right;  Entry<K,V> parent;  boolean color = BLACK; 複製代碼

TreeMap底層基於紅黑樹實現

Set

沒啥好說的

Queue

PriorityQueue

默認小頂堆、能夠看看關於堆排序的實現 八種常見的排序算法

public boolean offer(E e) {
 if (e == null)  throw new NullPointerException();  modCount++;  int i = size;  if (i >= queue.length)  grow(i + 1);  size = i + 1;  if (i == 0)  queue[0] = e;  else  siftUp(i, e);  return true; } 複製代碼
public boolean add(E e) {
 return offer(e); } 複製代碼

強烈推薦文章參考的美團的這篇文章、關於 HashMap 的

https://tech.meituan.com/2016/06/24/java-hashmap.html

相關文章
相關標籤/搜索