[关闭]
@myecho 2019-03-18T16:11:51.000000Z 字数 13807 阅读 1018

数据结构总结

Java


Collections继承体系
Collections集合中的共有方法:
* size()
* isEmpty()
* iterator()
* toArray()
* toArray(T[] a)
* add(E e)
* remove(Object o)
* clear()
* containsAll(Collection c)
* addAll(Collection c)加入的元素都必须是E的子类
* retainAll(Collection c)保留在集合中存在的
* removeIf(Predicate filter) jdk1.8 Predicate保证是实现的E的超类,因为其中会调用到E的一些方法
* removeAll(Collection c)
* spliterator() 返回一个并行的iterator(实际底层实现就是将一个集合拆成几个小部分)
* stream(返回一个串行)和parallelStream(可能返回串行也可能返回并行)中都会用到spliterator

  1. 为什么有了parallelStream还需要使用stream.parallel()?
    https://stackoverflow.com/questions/24603186/why-does-collection-parallelstream-exist-when-stream-parallel-does-the-sa
  2. 关于modCount和expectedCount的区别:https://www.zhihu.com/question/24086463
  3. 底层的数据存储部分一般都设为transient,所以序列化时要仔细处理

Map继承体系
通用方法:
* size()
* isEmpty()
* clear()
* containsKey(Object key)
* containsValue(Object value)
* get(Object key)
* V put(K key, V value)
* V remove(Object key)
* putAll(Map m)
* Set keySet() Views,在上边的操作都会映射到Map上去 通过让返回的Set持有一个底层table数组的迭代器来实现可见性和同步性 用Set装是因为本来就不会出现重复的key值
* Collection values() Views, 在上边的操作都会映射到Map上去 value里边有重复值,因为必须用Collection装
* Set> entrySet()
* default V getOrDefault(Object key, V defaultValue)
* default void forEach
* default void replaceAll
* default V putIfAbsent
* default boolean remove(Object key, Object value)key&value都相等才删除
* default boolean replace(K key, V oldValue, V newValue)
* default V replace(K key, V value)
* computeIfAbsent
* computeIfPresent
* merge 最终的value有重合的两个value得到

整个的设计体系层级关系:
1. 首先是interface层,设计所有的通用方法
2. 紧接着是abstract层,实现部分interface中的通用方法(比如size empty等) 同时实现部分方法的默认逻辑,比如add(E e) {throw new UnsupportedOperationException();} 如果子类的集合类不重写这个方法就代表不支持这个方法,直接抛出异常
还会剩下一些抽象方法交给子类实现,能够实现的方法就通过这些没实现的来实现即可
public abstract Iterator iterator();
public abstract int size();
3. 每个类别的iterator都不一样,都有其自己特色的实现方式

List

基本方法
List接口下的数据结构应该一般是允许null值存放的。
除此之外,java8还提供了两个默认方法replaceAll以及sort方法。

注意
1. sort方法基本上就是转化为数组,然后数组调用Arrays.sort()方法进行排序,然后再调用listIterator的set方法转换回来
2. subList也只是一个view视图,对其的操作会影响底层的结构
与set只提供了一个iterator方法不同,List还额外提供了一个listIterator方法,不仅支持后向迭代,而且支持前向迭代。
3. 还有一个重要的listIterator(index)可以指定迭代器的起始位置,并有removeRange(int fromIndex, int toIndex)方法可以调用

the ArrayList class(线程不安全)\LinkedList(线程不安全)\Vector(线程安全) are implementations of the List interface.

stack class是vector class的子类,也是比较古老的集合类,不推荐使用,如果需要使用栈这种数据结构时,推荐后边将要介绍的ArrayDeque.ArrayDeque也是list的实现类,底层也是基于数组实现,因此性能也很好。

ArrayList与vector都是基于数组实现的两个类,封装了一个动态的允许再分配的Object[]数组,使用initialCapacity来设置数组的长度,如果已知数组的大小时可调用ensureCapacity来手动指定数组长度,同时可以调用trimToSize()来缩减数组占用的内存空间。扩容的基本策略是不足3/2时,增长为3/2,如果超过Interger.MAX_VALUE-8(8个字节给数组头部使用)则无法进行增长

LinkedList is implemented as a double linked list. Its performance on add() and remove() is better than the performance of Arraylist. The get() and set() methods have worse performance than the ArrayList, as the LinkedList does not provide direct access to its members.

Arrays.asList("iPhone", "Ubuntu", "Android", "Mac OS X");提供了一个固定长度的list为我们使用,这个集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays内部类ArrayList的实例。不可增加、删除。

New feature in java8:

  1. package com.vogella.java.collections.list;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public class ListSorter {
  5. public static void main(String[] args) {
  6. System.out.println("Sorting with natural order");
  7. List<String> l1 = createList();
  8. l1.sort(null);
  9. l1.forEach(System.out::println);
  10. System.out.println("Sorting with a lamba expression for the comparison");
  11. List<String> l2 = createList();
  12. l2.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); // sort ignoring case
  13. l2.forEach(System.out::println);
  14. System.out.println("Sorting with a method references");
  15. List<String> l3 = createList();
  16. l3.sort(String::compareToIgnoreCase);
  17. l3.forEach(System.out::println);
  18. l1.removeIf(s-> s.toLowerCase().contains("x"));//new features
  19. }
  20. private static List<String> createList() {
  21. return Arrays.asList("iPhone", "Ubuntu", "Android", "Mac OS X");
  22. }
  23. }

Concurrent包内还包含了一些真正的并发类,如CopyOnWriteArrayList,使用阶段锁的机制,提高数据结构的并发度。

Map

HashMap与Hashtable的区别:
区别

HashMap与Hashtable判断两个key相等与HashSet相同:必须equals与hashCode()同时相等
HashMap与Hashtable判断两个value相等的标准更简单:只要两个对象通过equals()方法比较返回true即可
同时尽量避免将可变对象作为Map的key,并且不要在程序中修改可变对象。

LinkedHashMap使用双向链表来维护key-value对的次序,迭代顺序与插入顺序一致。

Properities类是Hashtable的子类,通常用来读写属性文件

TreeMap是一个红黑树的数据结构,每个key-value对的Entry就是树中的一个节点,通过compareTo()判断key是否相等

WeakHashMap中的每个key对象只持有对实际对象的弱引用,因此当垃圾回收回收了该key所对应的实际对象后,weakHashMap会自动删除该key对应的key-value。形如"java"的字符字面量其实是系统在hold住的强引用,一个集合类元素持有一个对象,也为强引用,无论其指向什么

EnumMap基本上对应EnumSet,在创建时必须制定Enum类
基本特征

  1. //数据结构转换
  2. private static String[] keysAsArray(Map<String, String> map) {
  3. return map.keySet().toArray(new String[map.keySet().size()]);
  4. }
  5. // assumes the key is of type String
  6. private static List<String> keysAsList(Map<String, String> map) {
  7. List<String> list = new ArrayList<String>(map.keySet());
  8. return list;
  9. }

java8添加的新方法

Set

HashSetHashSet判断两个对象是否相等的依据是equals()方法与hashCode()均相等,根据对象hashCode的值来计算出其储存位置。The add, remove, and contains methods have constant time complexity O(1).
hashCode重写的方法:
1. 首先计算对象中基本变量的hashCode值
基本变量hashCode计算
2. 利用1中计算出来的hashCode值组合出一个hashCode进行返回

向HashSet中添加可变对象中,必须十分小心。如果修改hashset集合中的对象,有可能导致与集合中已有对象相等,进而导致无法准确访问对象。

LinkedHashSet是HashSet的子类,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序来保存的。The time complexity of basic methods is O(1).

TreeSet是SortedSet接口的实现类,可以确保集合元素处于排序状态。The elements in a set are sorted, but the add, remove, and contains methods has time complexity of O(log (n)).
TreeSet额外方法
1. 自然排序,调用集合元素的compareTo()方法,根据这个方法的结果来决定其在红黑树的结果,因此最好集合元素都是同一类元素
2. 定制排序,在TreeSet构造时传入comparator(可以是一个lambda表达式)

EnumSet是一个专门为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显示指定或者隐式的指定,集合中的元素也是有序的,以枚举值在Enum内的定义顺序来决定集合元素的次序。内部以位向量的形式存储,不允许加入null元素
构造EnumSet

必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。通常可以通过Collections工具类的synchronized*方法来包装集合,该操作最好在创建时进行。
synchronizedSet/synchronizedList/synchronizedMap
synchronizedSortedSet/synchronizedSortedMap

这里要注意的包装后的效果实际上为集合操作包装了同步方法,例子如下:
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}

因此当如果只是在多线程环境下调用单一集合操作时不必加同步操作,但如果进行多步操作比如迭代集合时还是需要进行同步操作的。
https://stackoverflow.com/questions/9468187/collections-synchronizedlist-and-synchronized

Queue

常见操作如下:
* boolean add(E e) 如果没有超过队列容量限制的情况下成功,否则抛出异常
* boolean offer(E e);如果 也是添加一个元素,但和上一个相比不会抛出异常
* E remove()队列为空时抛出异常
* E poll()队列为空时不会排除异常,会返回一个null
* E element()只是返回队头元素,但是不删除,属于会抛出异常的版本
* E peek()返回队首元素,但并不删除,队列为空返回null

Queue还有一个Deque子接口,Deque代表一个"双端队列",双端队列可以从两端来进行添加或者删除操作,因此Deque的实现既可以当做队列使用,也可以当做栈使用。

实际上将上述Queue的操作扩展到了双端:
* void addFirst(E e);
* void addLast(E e);
* 上述的Queue的操作均有两种对应的形式,并且还包括单独的形式如add、poll等
* stack methonds: push or pop 都是会抛出异常的版本
* Collections methonds:boolean remove(Object o);boolean contains(Object o);public int size();Iterator iterator();Iterator descendingIterator();
* boolean removeFirstOccurrence(Object o);
* boolean removeLastOccurrence(Object o);

PriorityQueue
* 构造函数PriorityQueue(Collection c)若c中是如TreeSet等自带排序器的集合类,会一起将其排序器一起复制过来
* 比较特殊的其特供了删除其中某个中间元素的操作remove,并且siftDown或者siftUp进行调整堆的操作

You can choose between the following Queue implementations in the Java Collections API:
* java.util.LinkedList,因为实现Deque接口又实现了List接口,也可以被当做栈来使用,可以允许插入null值 其中的Node节点为双向链表节点 有first和last两个Node指示整个链表结构
* java.util.PriorityQueue,不允许插入null
* java.util.ArrayDequeue 底层使用循环数组实现,有head和tail控制数组长度,当head == tail时,数组长度扩展为原来的2倍。不允许插入null

实现分析

List

1.1 ArrayList
以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组。因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

按数组下标访问元素-get(i)、set(i,e) 的性能很高,这是数组的基本优势。

如果按下标插入元素、删除元素-add(i,e)、 remove(i)、remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了。

1.2 LinkedList
以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,每插入一个元素都要构造一个额外的Node对象,也需要额外的链表指针操作。

按下标访问元素-get(i)、set(i,e) 要悲剧的部分遍历链表将指针移动到位 (如果i>数组大小的一半,会从末尾移起)。

只有在链表两头的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指针的移动。
Apache Commons 有个TreeNodeList,里面是棵二叉树,可以快速移动指针到位。

1.3 CopyOnWriteArrayList
并发优化的ArrayList。基于不可变对象策略,在修改时先复制出一个数组快照来修改,改好了,再让内部指针指向新数组。

因为对快照的修改对读操作来说不可见,所以读读之间不互斥,读写之间也不互斥,只有写写之间要加锁互斥,使用volatile关键字保证切换过程对读进程立即可见。但复制快照的成本昂贵,典型的适合读多写少的场景。

虽然增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。

缺点也很明显,一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

1.4 遗憾
无论哪种实现,按值返回下标contains(e), indexOf(e), remove(e) 都需遍历所有元素进行比较,性能可想像的不会太好。
没有按元素值排序的SortedList。
除了CopyOnWriteArrayList,再没有其他线程安全又并发优化的实现如ConcurrentLinkedList。凑合着用Set与Queue中的等价类时,会缺少一些List特有的方法如get(i)。如果更新频率较高,或数组较大时,还是得用Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全。

Map

2.1 HashMap
以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。

插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。
JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。

在JDK8里,新增默认为8的閥值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。

当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。
取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方(tableSizeFor函数控制), 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。
iterator()时顺着哈希桶数组来遍历,看起来是个乱序。
另外要注意hashMap的数据添加顺序不一定和数据遍历的顺序一致。

2.2 LinkedHashMap

扩展HashMap,每个Entry增加双向链表,号称是最占内存的数据结构。

支持iterator()时按Entry的插入顺序来排序(如果设置accessOrder属性为true,则所有读写访问都排序)。

插入时,Entry把自己加到Header Entry的前面去。如果所有读写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己,所以此时读操作也是线程不安全的了。
LinkedHashMap 是 Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

LinkedHashMap 实现与 HashMap 的不同之处在于,LinkedHashMap 维护着一个运行于所有条目的双向链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。

2.3 TreeMap

以红黑树实现,红黑树又叫自平衡二叉树:

对于任一节点而言,其到叶节点的每一条路径都包含相同数目的黑结点。

上面的规定,使得树的层数不会差的太远,使得所有操作的复杂度不超过 O(lgn),但也使得插入,修改时要复杂的左旋右旋来保持树的平衡。

比较适合做范围查询,比如lowerEntry这类的操作
SortedMap定义的通用方法:
* SortedMap subMap(K fromKey, K toKey);
* SortedMap headMap(K toKey);
* SortedMap tailMap(K fromKey);
* K firstKey();
* K lastKey();
* Set keySet();
* Collection values();
* Set> entrySet();

2.4 EnumMap

EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组。性能与内存占用俱佳。

美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断,在JMC里录得不低的采样命中频率。

public V put(K key, V value) {
    typeCheck(key);

    int index = key.ordinal();
    Object oldValue = vals[index];
    vals[index] = maskNull(value);
    if (oldValue == null)
        size++;
    return unmaskNull(oldValue);
}
private void typeCheck(K key) {
    Class<?> keyClass = key.getClass();
    if (keyClass != keyType && keyClass.getSuperclass() != keyType)
        throw new ClassCastException(keyClass + " != " + keyType);
}

2.5 ConcurrentHashMap

并发优化的HashMap。

在JDK7里的经典设计,默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。

也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。

但在JDK8里,Segment[]的设计被抛弃了,改为精心设计的,只在需要锁的时候加锁。

支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。

2.6 ConcurrentSkipListMap

JDK6新增的并发优化的SortedMap,以SkipList结构实现。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。

原理上,可以想象为多个链表组成的N层楼,其中的元素从稀疏到密集,每个元素有往右与往下的指针。从第一层楼开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走。

典型的空间换时间。每次插入,都要决定在哪几层插入,同时,要决定要不要多盖一层楼。
image_1b9smnoci1l9gf8f148pfs9c9m2a.png-68.6kB
它的size()同样不能随便调,会遍历来统计。

插入时:随机获取一个level,然后在“跳表”的第1层~第level层之间,每一层都插入节点z;在第level层之上就不再插入节点了。若level数值大于“跳表的层次”,则新建一层。

Set

所有Set几乎都是内部用一个Map来实现, 因为Map里的KeySet就是一个Set,而value是假值,全部使用同一个Object即可。

Set的特征也继承了那些内部的Map实现的特征。

HashSet:内部是HashMap。

LinkedHashSet:内部是LinkedHashMap。

TreeSet:内部是TreeMap的SortedSet。

ConcurrentSkipListSet:内部是ConcurrentSkipListMap的并发优化的SortedSet。不允许存在null值

CopyOnWriteArraySet:内部是CopyOnWriteArrayList的并发优化的Set,利用其addIfAbsent()方法实现元素去重,如前所述该方法的性能很一般。允许存放null值

好像少了个ConcurrentHashSet,本来也该有一个内部用ConcurrentHashMap的简单实现,但JDK偏偏没提供。Jetty就自己简单封了一个,Guava则直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。

Queue

4.1 普通队列

4.1.1 LinkedList
是的,以双向链表实现的LinkedList既是List,也是Queue。

4.1.2 ArrayDeque
以循环数组实现的双向Queue。大小是2的倍数,默认是16。

为了支持FIFO,即从数组尾压入元素(快),从数组头取出元素(超慢),就不能再使用普通ArrayList的实现了,改为使用循环数组。

有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,队尾下标递增。如果加入元素时已到数组空间的末尾,则将元素赋值到数组[0],同时队尾下标指向0,再插入下一个元素则赋值到数组1,队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。

4.1.3 PriorityQueue
用平衡二叉最小堆实现的优先级队列,不再是FIFO,而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。

平衡最小二叉堆,用一个简单的数组即可表达,可以快速寻址,没有指针什么的。最小的在queue[0] ,比如queue4的两个孩子,会在queue[2*4+1] 和 queue[2*(4+1)],即queue9和queue10

入队时,插入queue[size],然后二叉地往上比较调整堆。

出队时,弹出queue[0],然后把queque[size]拿出来二叉地往下比较调整堆。

初始大小为11,空间不够时自动50%扩容。

4.2 线程安全的并发队列

4.2.1 ConcurrentLinkedQueue/Deque
无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。

ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法。

4.3 线程安全的阻塞队列
它会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。
BlockingQueue的队列长度受限,用以保证生产者与消费者的速度不会相差太远,避免内存耗尽。队列长度设定后不可改变。当入队时队列已满,或出队时队列已空,不同函数的效果见下表:
image_1b9snbgu7k5ttikb5r1gal18q12n.png-22.2kB
4.3.1 ArrayBlockingQueue

定长的并发优化的BlockingQueue,也是基于循环数组实现。有一把公共的锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。

4.3.2 LinkedBlockingQueue/Deque

可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE成为无界无等待的。

利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。

4.3.3 PriorityBlockingQueue
无界的PriorityQueue,也是基于数组存储的二叉堆。一把公共的锁实现线程安全。虽然实现了BlockingQueue接口,但因为无界,其实没有任何阻塞队列的特征,空间不够时会自动扩容。

4.3.4 DelayQueue
内部包含一个PriorityQueue,同样是无界的。一把公共的锁实现线程安全。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。

pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。

4.4 同步队列
Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
SynchronousQueue同步队列本身无容量,放入元素时,比如等待元素被另一条线程的消费者取走再返回。JDK线程池里用它。
JDK7还有个LinkedTransferQueue(使用CAS操作,详见William Scherer, Doug Lea, and Michael Scott的论文),在普通线程安全的BlockingQueue的基础上,增加一个transfer(e) 函数,效果与SynchronousQueue一样,但是transfer函数可以确保队列中被传递元素之前的所有元素都能被处理

汇总:http://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html

https://zhuanlan.zhihu.com/p/27148381?utm_source=wechat_session&utm_medium=social

注意事项

image_1b9sjj0vu7eq1cbq19l25jfasa9.png-202.1kB
image_1b9sjjmi4j3n153v1p0816ln2ecm.png-80.8kB
【强制】不要在 foreach 循环里进行元素的 remove / add 操作。 remove 元素请使用 Iterator
方式,如果并发操作,需要对 Iterator 对象加锁。
image_1b9sjknugbqp1j5f85618t4cbj13.png-42.4kB
image_1b9sjl56l484st66g21m5drrt1g.png-152.5kB
image_1b9sjlj991d5t1heks9u6bv1jdp1t.png-52.3kB
LinkedHashMap允许null值和null键
HashMap是不稳定的原因是因为有可能会出现扩容的现象,从而导致其哈希桶内的数据排布顺序发生了变化

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注