一、Map

graph TD Map --- HashMap; Map --- HashTable; Map --- TreeMap; Map --- EnumMap; HashMap --- LinkedHashMap; HashTable --- Properties;
key可以为空 value可以为空 key可以重复 value可以重复 是否有序 线程是否安全 底层数据结构
HashMap ✓ ✓ ✗ ✓ 无序 不安全 java7:数组+链表。java8:数组+链表+红黑树
LinkedHashMap ✓ ✓ ✗ ✓ 存取有序 不安全 HashMap基础上+双向链表
TreeMap ✗ ✓ ✗ ✓ 自然顺序排序 不安全 红黑树
Hashtable ✗ ✗ ✗ ✓ 无序 安全 数组 + 链表

在HashMap和LinkedHashMap等实现类中,键可以为null。由于HashMap使用哈希表来存储键值对,允许一个键为null,且将其映射到哈希表的索引0上。

在TreeMap实现类中,键不能为null。TreeMap基于红黑树实现,要求键必须实现Comparable接口,因此不允许为null。

1.1 HashMap

  • 内部结构分析

HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示

1
2
3
4
5
static class Node<K,V> implements Map.Entry<K,V> {
   final int hash;
   final K key;
   V value;
   Node<K,V> next;

Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。

HashMap的数据结构为 数组+(链表或红黑树) ,数组默认长度为16,每次扩容都是2倍,负载因子为0.75,也就说第一次扩容数组长度为16*0.75=12。

1

  1. 每条链表也叫做桶(bucket),红黑树是为了提高查询效率

  2. 存放元素的时候会先根据key的hash值去计算数组下标,如果这个下标下没有元素,就创建一个Node节点放进去

  3. 如果数组下标有数据,先判断是否与链表中的key相同,相同的话替换元素的value;不同的话插在链表的尾节点。注意:同一个链表中的节点只是说数组下标相同,但不一定是发生了hash冲突的,有可能hash值不同

  4. 链表长度大于等于8,且当数组长度大于等于64,会把链表转为红黑树,否则会优先触发扩容(对数组扩容,目的是减少单个桶中元素的数量)。这意味着需要同时满足两个条件,链表才会转化为红黑树。红黑树本质是一颗自平衡的二叉查找树,查找时间复杂度为o(logn)

阈值 8 的原因

  • 如果链表过长,查找性能会退化为 O(n)。
  • 转换为红黑树后,查找性能提升到 O(log n)。
  • 选择 8 作为阈值是基于性能和内存的折中。链表转红黑树会增加一定的内存开销,因此只有链表足够长才转换。
  • put过程分析

图片来自:https://www.jianshu.com/p/d04edc8aaf0f

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素(处理hah冲突)
    else {
        Node<K,V> e; K k;
        // 判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。
        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);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}
  • resize 方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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;
        }
        // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {
        // signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 原索引
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  • Hash冲突

    HashMap使用拉链法(Chaining)来解决哈希冲突。当发生哈希冲突时,即多个键被映射到同一个哈希桶的位置上,HashMap会在该位置上维护一个链表(或者更准确地说,是一个链表数组),将具有相同哈希值的键值对存储在同一个链表中。

    具体的解决过程如下:

    1. 当需要插入一个键值对时,HashMap会先计算键的哈希值,并根据哈希值找到对应的哈希桶(也称为槽位)。
    2. 如果该槽位为空,即没有其他键值对存在,直接将键值对插入到该槽位上。
    3. 如果该槽位已经存在一个或多个键值对,那么会遍历链表,比较键的值:
      • 如果存在键的值与要插入的键值对的键相等,则更新该键值对的值。
      • 如果遍历完链表仍未找到相等的键值对,则将新的键值对追加到链表的末尾。

    在哈希表的查询操作中,HashMap会根据键的哈希值定位到对应的哈希桶,然后遍历链表查找具有相同键的键值对。

    当链表的长度过长时,为了提高查询效率,HashMap会将链表转换为红黑树。当链表长度大于等于8,数组长度大于64时会将链表转换为红黑树,从而提高搜索和插入的性能。当红黑树的节点数量减少到6个以下时,又会将红黑树转换回链表。

    通过使用链表和红黑树来处理哈希冲突,HashMap可以有效地解决哈希冲突并提供高效的键值对存储和检索。

  • get 过程分析

  1. 首先,根据要获取的键通过哈希函数计算出哈希值。

  2. 根据哈希值计算出数组的索引位置。

  3. 在该索引位置上检查是否存在键值对。

  4. 如果该位置为空,则说明没有找到对应的键,返回null。

  5. 如果该位置不为空,则需要进一步判断是否存在相同的键。

  6. 如果找到相同的键,则返回对应的值。

  7. 如果该位置存在多个键值对,则需要遍历链表(或红黑树)来查找对应的键。

  8. 如果找到相同的键,则返回对应的值。

  9. 如果遍历完整个链表(或红黑树)仍然没有找到相同的键,则返回null。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 判断第一个节点是不是就是需要的
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 判断是否是红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);

            // 链表遍历
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
} 

1.2 LinkedHashMap

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

1.3 TreeMap

TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

二、Iterable

graph TD Iterable --- Collection; Collection --- Queue; Collection --- List; Collection --- Set; List --- ArrayList; List --- LinkedList; List --- Vector; Deque --- LinkedList; Set --- HashSet; Set --- TreeSet; Set --- EnumSet; Queue --- Deque; Queue --- PriorityQueue; Vector --- Stack; HashSet --- LinkedHashSet;
值可以为空 值可否重复 是否有序 线程是否安全 底层数据结构 特点
ArrayList ✓ ✓ 存取有序 不安全 数组 查询快, 增删慢
LinkedList ✓ ✓ 存取有序 不安全 双向链表 查询慢, 增删快
Vector ✓ ✓ 存取有序 安全 数组 查询快, 增删慢
HashSet ✓ ✗ 无序 不安全 Hash表结构 底层的实现其实是一个 java.util.HashMap
LinkedHashSet ✓ ✗ 存取有序 不安全 链表和哈希表组合的一个数据存储结构 元素唯一且存取有序
TreeSet ✗ ✗ 自然顺序排序 不安全 二叉树结构 保证元素的唯一, 可以对元素进行排序

2.1 ArrayList

ArrayList允许存放null元素,底层通过数组实现的有序集合。ArrayList底层数组默认初始化长度为10,元素超过此长度后会自动扩容, 容量将扩展到原来的1.5倍。

自动扩容代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

2.2 LinkedList

LinkedList底层通过双向链表实现,双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。
在这里没有所谓的哑元,当链表为空的时候first和last都指向null。

内部类Node:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

2.3 HashSet

HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法,详细请看HashMap。

2.4 LinkedHashSet

LinkedHashSet是对LinkedHashMap的简单包装,对LinkedHashSet的函数调用都会转换成合适的LinkedHashMap方法,详细请看LinkedHashMap。

2.5 TreeSet

前面已经说过TreeSet是对TreeMap的简单包装,对TreeSet的函数调用都会转换成合适的TreeMap方法,详细请看TreeMap。