一、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。