Java常用集合知识

132 阅读21分钟

集合类关系图

简图: image.png 详图: image.png

1. List

1.1 ArrayList

简介:

ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。

底层实现:

顾名思义,ArrayList就是用数组实现的列表。

默认大小:

10。private static final int DEFAULT_CAPACITY=10;

自动扩容: (通过add方法理解)

  • 初始容量:创建ArrayList对象时,会分配一个初始容量,默认为10。
  • 扩容触发条件:当ArrayList的size超过当前容量时,就会触发扩容操作。
  • 扩容策略:ArrayList在扩容时,会创建一个新的更大容量的数组,并将原有元素复制到新数组中。
  • 扩容大小:根据添加的元素个数计算出最小容量minCapacity(原数组元素个数+添加元素个数),计算出最小增长minGrowth(最小容量-原数组长度)和首选增长prefGrowth(原数组长度 >> 1,即1.5倍扩容),再计算出首选长度prefLength(原数组长度 + Math.max(minGrowth,prefGrowth))。对首选长度校验(0< prefLength && prefLength <=SOFT_MAX_ARRAY_LENGTH),是则返回首选长度作为扩容后的大小,否则进一步计算。
// 扩容核心代码
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

Fail-Fast机制:

ArrayList也采用了快速失败的机制,通过记录修改计数器modCount来实现。首先在构造迭代器的时候,将当前的修改计数器的值保存,之后进行遍历的时候,每访问一个数据,都要检查当前集合的修改次数是否合法,如果有其他线程修改了集合,那么modCount就会被修改,当前修改计数器的值与之前保存的值(即期望值)不同,那么将抛出ConcurrentModificationException。

使用注意事项:

  • 尽量预估初始容量:如果能够预先知道大致需要存储多少个元素,可以通过指定初始容量来减少扩容次数,提高性能。
  • 避免频繁插入和删除操作:由于插入和删除操作的性能较低,如果需要频繁进行这些操作,建议考虑其他数据结构,如LinkedList。
  • 不支持并发,如需要并发操作,需要使用Collections.synchronizedList或者CopyOnWriteArrayList来替代ArrayList。

1.2 LinkedList

简介:

LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。

底层实现:

顾名思义,LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。

核心代码:增删的核心方法unlink()、linkLast()

//unlink是remove内部实现方法
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}
//linkLast是add内部实现方法
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

方法区分:

peek(): 查看第一个元素

poll(): 移除第一个元素并返回,链表为空返回null

pop(): 弹出第一个元素,链表为空抛出异常

push( E e): 从头部插入元素

offer(E e): 从尾部插入元素

Queue接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertion, extractioninspection操作。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。

Throws exceptionReturns special value
Insertadd(e)offer(e)
Removeremove()poll()
Examineelement()peek()

Deque是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持insert, remove和examine操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下:

First Element - HeadLast Element - Tail
Throws exceptionSpecial valueThrows exceptionSpecial value
InsertaddFirst(e)offerFirst(e)addLast(e)offerLast(e)
RemoveremoveFirst()pollFirst()removeLast()pollLast()
ExaminegetFirst()peekFirst()getLast()peekLast()

1.3 Vector

简介:

Vector实现了List接口,与ArrayList类似,底层通过数组实现。与ArrayList不同之处是Vector的方法都是线程安全的,这是通过在每个方法上使用同步锁synchronized来实现的,确保在同一时刻只有一个线程能够修改Vector。

底层实现:

底层通过数组实现。

默认大小:

10。

public Vector() { this(10); }

自动扩容: (通过add方法理解)

  • 初始容量:创建Vector对象时,会分配一个初始容量,默认为10。
  • 扩容触发条件:当Vector的size超过当前容量时,就会触发扩容操作。
  • 扩容策略:Vector在扩容时,会创建一个新的更大容量的数组,并将原有元素复制到新数组中。
  • 扩容大小:扩容逻辑与ArrayList类似,不同之处是首选增长prefGrowth(默认2倍扩容)。
// 扩容核心代码
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = ArraysSupport.newLength(oldCapacity,
            minCapacity - oldCapacity, /* minimum growth */
            capacityIncrement > 0 ? capacityIncrement : oldCapacity
                                       /* preferred growth */);
    return elementData = Arrays.copyOf(elementData, newCapacity);
}

2. Queue

2.1 PriorityQueue

简介:

PriorityQueue,即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素,C++的优先队列每次取最大元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator,类似于C++的仿函数)。

Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。

底层实现:

通过数组表示的小顶堆实现。小顶堆父子节点下标关系:

leftNo = parentNo*2+1

rightNo = parentNo*2+2

parentNo = (nodeNo-1)/2

核心代码:

添加元素:(add(E)和offer(E)方法,add内部调用的offer)

offer(E):

先判断是否需要扩容,如果需要先扩容。先将插入的元素放置在数组尾部,然后向上调整顶堆位置。从数组尾部元素开始进行循环,找到其父节点并将插入元素与其元素比较,如果小于将父节点的元素与插入元素调换位置,继续循环上述动作。如果大于(调整完毕)退出循环,直接插入到此位置。

// 添加元素
public boolean offer(E e) {
    if (e == null)//不允许放入null元素
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);//自动扩容
    siftUp(i, e);//把e放到数组i位置,然后向上调整
    size = i + 1;
    return true;
}
// 自动扩容:如果原容量小于64就2倍扩容,否则1.5倍扩容
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = ArraysSupport.newLength(oldCapacity,
            minCapacity - oldCapacity, /* minimum growth */
            oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1
                                       /* preferred growth */);
    queue = Arrays.copyOf(queue, newCapacity);
}
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x, queue, comparator);
    else
        siftUpComparable(k, x, queue);
}
// 向上调整顶堆
private static <T> void siftUpComparable(int k, T x, Object[] es) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = es[parent];
        if (key.compareTo((T) e) >= 0)
            break;
        es[k] = e;
        k = parent;
    }
    es[k] = key;
}

删除元素:(poll()、remove(Object)、removeEq(Object)、removeAt(int)等)

poll():

把数组尾部元素放在头部,将尾部元素置空并将元素个数减一,然后向下调整顶堆位置。从当前位置(顶堆顶部)开始进行循环,找出左右子节点中较小的一个,与它调换位置,直到当前节点是叶子结点或者当前节点的值小于或等于插入的元素 x 的值(调整完毕)退出循环。

// 删除元素
public E poll() {
    final Object[] es;
    final E result;

    if ((result = (E) ((es = queue)[0])) != null) {
        modCount++;
        final int n;
        final E x = (E) es[(n = --size)];
        es[n] = null;
        if (n > 0) {
            final Comparator<? super E> cmp;
            if ((cmp = comparator) == null)
                siftDownComparable(0, x, es, n);
            else
                siftDownUsingComparator(0, x, es, n, cmp);
        }
    }
    return result;
}
// 向下调整顶堆的代码
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
    // assert n > 0;
    Comparable<? super T> key = (Comparable<? super T>)x;//将传入的元素 x 强制转换为 Comparable 接口,以便后续可以使用 compareTo 方法来比较元素
    int half = n >>> 1; //将堆的大小 n 除以 2,得到非叶子节点的数量
    while (k < half) {//直到当前位置 k 不再是非叶子节点
        int child = (k << 1) + 1; //计算当前节点的左孩子的索引
        Object c = es[child];//获取左孩子节点的值
        int right = child + 1;//计算当前节点的右孩子的索引
        if (right < n &&
            ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
            c = es[child = right];//检查右孩子是否存在且比左孩子小,如果右孩子存在且比左孩子小,则将 c 更新为右孩子的值,并将 child 更新为右孩子的索引
        if (key.compareTo((T) c) <= 0)
            break;//如果当前节点的值小于或等于插入的元素 x 的值,则退出循环
        es[k] = c;//将当前节点的值更新为孩子节点的值
        k = child;//将当前位置 k 更新为孩子节点的位置,以便继续向下迭代
    }
    es[k] = key;//将插入的元素 x 放置到最终位置 k 上,以保持堆的有序性
}

removeAt(int):

remove(Object)、removeEq(Object)内部都是调用removeAt

先将数据元素个数减一,然后将数组尾部的元素放到要删除的下标位置,将数组尾部置空;从删除下标位置向下调整顶堆位置;

// 删除指定下标元素
E removeAt(int i) {
    // assert i >= 0 && i < size;
    final Object[] es = queue;
    modCount++;
    int s = --size;
    if (s == i) // removed last element
        es[i] = null;//检查是否删除的是最后一个元素。如果是最后一个元素,直接将该位置的元素设为 null。
    else {
        E moved = (E) es[s];
        es[s] = null;
        siftDown(i, moved);//将moved元素放在i下标位置,然后向下调整顶堆
        if (es[i] == moved) {//如果向下调整完,moved元素还在i下标位置,就向上调整顶堆
            siftUp(i, moved);
            if (es[i] != moved)
                return moved;
        }
    }
    return null;
}

2.2 ArrayDque

简介:

ArrayDeque是Deque的实现,官方推荐使用AarryDeque用作栈和队列。

底层实现:

ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null元素。

默认大小:

16。

public ArrayDeque() { elements = new Object[16 + 1]; }

元素定位:

使用了位操作,非常巧妙的省去了边界处理等问题,简化了操作。

add相关方法,通过位运算定位添加元素,之后检查是否需要扩容;

poll相关方法,置空对应位置,位运算定位,返回元素;

自动扩容: (通过add方法理解)

  • 扩容触发条件:当添加元素后发现head == tail,就会触发扩容操作。
  • 扩容策略:ArrayDeque在扩容时,会创建一个新的更大容量的数组,并将原有元素复制到新数组中。
  • 扩容大小:
// 扩容核心代码
private void grow(int needed) {
    // overflow-conscious code
    final int oldCapacity = elements.length;
    int newCapacity;
    // Double capacity if small; else grow by 50%
    int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);//计算扩容的步长。如果当前容量小于64,扩容步长为当前容量加2,否则为当前容量的一半。
    if (jump < needed
        || (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
        newCapacity = newCapacity(needed, jump);//检查是否需要进行扩容。如果步长小于所需的额外容量(needed),或者新容量减去超过了最大数组容量限制,则调用 newCapacity(needed, jump) 方法计算新容量。
    final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
    // Exceptionally, here tail == head needs to be disambiguated
    if (tail < head || (tail == head && es[head] != null)) {//判断队列是否发生了循环,即队列的头部在数组的末尾,如果发生了循环,就需要进行一些额外的处理
        // wrap around; slide first leg forward to end of array
        int newSpace = newCapacity - oldCapacity;
        System.arraycopy(es, head,
                         es, head + newSpace,
                         oldCapacity - head);//将原数组中头指针 head 到末尾的元素复制到新数组的末尾。这是为了处理队列循环的情况,将头部的元素“移动”到数组的末尾
        for (int i = head, to = (head += newSpace); i < to; i++)
            es[i] = null;//循环遍历数组中原头指针 head 到新头指针位置 to 之间的元素,并将它们在新数组中的位置设为 null,以释放对这些对象的引用,帮助垃圾回收系统回收不再需要的内存。
    }
}

3. Map

3.1 HashMap

简介:

HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现,Value 随意。

底层实现:

jdk 8 之前,其内部是由数组+链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树(时间复杂度由O(N)降低为 O(logN))。大致的数据存储形式如下:

默认大小:

16。static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

自动扩容: (通过put方法理解)

核心方法resize(),初始化表或将表扩容两倍。代码分为两块,前半段确定新表的大小和阈值,后半段创建新表并迁移数据。

后半段迁移数据,原数据中的链表根据算法((e.hash & oldCap) == 0) 被一分为二迁到新数组中。

//put方法中插入后,判断是否需要扩容
if (++size > threshold)//threshold为扩容阈值,threshold = capacity * load factor
    resize();


//扩容函数:初始化表大小或将表大小加倍
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)
            // 将阈值扩大一倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
        newCap = oldThr;
    else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    // 用新的数组大小初始化新的数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可

    if (oldTab != null) {
        // 开始遍历原数组,进行数据迁移。
        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 {
                    // 这块是处理链表的情况,
                    // 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
                    // loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的
                    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) {//lo链表:低位链表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {//hi链表:高位链表
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        // 第一条链表
                        newTab[j] = loHead;//(e.hash & oldCap) == 0
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 第二条链表的新的位置是 j + oldCap,这个很好理解
                        newTab[j + oldCap] = hiHead;//(e.hash & oldCap) == 1
                    }
                }
            }
        }
    }
    return newTab;
}

核心方法:put(K key, V value)、get(K key)、remove(Object key)

put方法:

定位table位置,然后判断所在bucket的数据结构类型。链表就遍历链表,如果已存在key,则更新值,未存在插入到链表尾部;红黑树就遍历红黑树,如果已存在key,则更新值,未存在插入到树中。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    // 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第五个参数 evict 我们这里不关心
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
        // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        else {// 数组该位置有数据
            Node<K,V> e; K k;
            // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
            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) {
                    // 插入到链表的最后面(Java7 是插入到链表的最前面)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个
                        // 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果在该链表中找到了"相等"的 key(== 或 equals)
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                        break;
                    p = e;
                }
            }
            // e!=null 说明存在旧值的key与要插入的key"相等"
            // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

get方法:

定位table位置,然后判断所在bucket的数据结构类型。链表就遍历链表,如果已存在key,则返回对应的值,未存在返回null;红黑树就遍历红黑树,如果已存在key,则返回对应的值,未存在返回null。

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;
}

remove方法:

定位table位置,然后判断所在bucket的数据结构类型。无论是链表还是红黑树,都分为两步,第一步定位找到对应的key,第二步删除对应key的节点。如果找不到,直接返回null。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

3.2 LinkedHashMap

简介:

LinkedHashMapHashMap的直接子类,二者唯一的区别是LinkedHashMapHashMap的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。

与HashMap的不同之处:

  1. 结构不同:

HashMap和双向链表的密切配合和分工合作造就了LinkedHashMap。特别需要注意的是,next用于维护HashMap各个桶中的Entry链,before、after用于维护LinkedHashMap的双向链表,虽然它们的作用对象都是Entry,但是各自分离,是两码事儿。

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

  1. 代码不同:

LinkedHashMap相比HashMap,新增了双向链表的头节点、尾节点、访问顺序成员变量。

 /**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;

/**
* The iteration ordering method for this linked hash map: { @code true}
* for access-order, { @code false} for insertion-order.
*
* @serial
*/
final boolean accessOrder;

如果accessOrder为true,put时将新插入的元素放入到双向链表的尾部,get时将当前访问的Entry移到双向链表的尾部。当标志位accessOrder的值为false时,put时将新插入的元素放入到双向链表的尾部,get时对当前访问的Entry不做处理。

当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。

在HashMap中最常用的两个操作就是:put(Key,Value) 和 get(Key)。同样地,在 LinkedHashMap 中最常用的也是这两个操作。对于put(Key,Value)方法而言,LinkedHashMap完全继承了HashMap的 put(Key,Value) 方法,只是对putVal(hash,key, value, onlyIfAbsent,evict)方法所调用的afterNodeAccess方法和afterNodeInsertion方法进行了重写;对于get(Key)方法,LinkedHashMap则直接对它进行了重写。

//重写HashMap的get方法
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
//可见仅有accessOrder为true时,且访问节点不等于尾节点时,该方法才有意义。通过before/after重定向,将新访问节点链接为链表尾节点。
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
//putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) 
// afterNodeInsertion方法由于removeEldestEntry方法默认一直返回的false而无执行意义。也就意味着如果想要让它有意义必须重写removeEldestEntry方法。
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);
    }
}
 * @param    eldest 最老的节点(即头节点)
 * @return   如果map应该删除头节点就返回true,否则返回false
 */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

3.3 TreeMap

底层实现为红黑树。比较复杂,参考: juejin.cn/post/684490…

4. Set

4.1 HashSet

简介:

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

4.2 LinkedHashSet

简介:

LinkedHashSet是HashSet的直接子类,二者唯一的区别是LinkedHashSet在HashSet的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。

构造函数调用父类HashSet的构造方法,new了一个LinkedHashMap。

4.3 TreeSet

TreeSet底层使用NavigableMap和TreeMap来实现的。参考: juejin.cn/post/684490…

补充知识:

hash方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

满二叉树:

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

完全二叉树:

对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。