目录
集合框架图
Collection接口
描述
Collection是最基本的集合接口,它是单列数据集合。在JDK中不提供Collection接口的任何直接实现,它只提供了更具体的子接口(即继承自Collection接口),例如List列表,Set集合,Queue队列,然后再由具体的类来实现这些子接口。通过具体类实现接口之后它们的特征就得以凸显出来,有些集合中的元素是有序的,而其他的集合中的元素则是无序的;有些集合允许重复的元素,而其他的集合则不允许重复的元素;有些集合允许排序,而其他的集合则不允许排序。
常用方法
-
add()
添加单个元素 -
remove()
删除指定元素remove(index)
删除指定索引元素remove(Object)
删除指定某个元素 -
contains()
查找元素是否存在 -
size()
获取元素个数 -
isEmpty()
判断是否为空 -
clear()
清空 -
addAll(Collection)
添加多个元素 -
containsAll(Collection)
查找多个元素是否都存在 -
removeAll(Collection)
删除多个元素
Iterator接口
描述
Iterator 对象称为迭代器, 主要仅用于遍历 Collection 集合中的元素,Iterator 本身并不存放对象。所有实现了Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了Iterator 接口的对象, 即可以返回一个迭代器。
常用方法
-
literator()
得到一个集合的迭代器 -
hasNext()
判断是否还有下一个元素 -
next()
下移并将下移以后集合位置上的元素返回 -
remove()
移除集合元素
遍历实现
public static void main(String[] args) { Collection<String> collections = new ArrayList<String>(){ { add("张三"); add("李四"); add("王五"); } }; // 遍历方式一 Iterator<String> iterator = collections.iterator(); while(iterator.hasNext()) { String next = iterator.next(); log.info("{}", next); } // 遍历方式二 for(String str : collections) { log.info("{}", str); } // 遍历方式三 collections.forEach(System.out::println); // 遍历方式四 collections.stream().forEach(data -> { log.info("{}", data); }); }
List接口
描述
List接口直接继承了Collection接口,它对Collection进行了简单的扩充,从而让集合凸显出它们各自的特征。在List中所有元素的存储都是有序的,而且是可重复存储的。用户可以根据元素存储位置的索引来操作元素。实现了List接口的集合主要有以下几个:ArrayList、LinkedList、Vector和Stack。
特点
-
List集合所有的元素是以一种线性方式进行存储的。
-
它是一个元素存取有序的集合。即元素的存入顺序和取出顺序有保证。
-
它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
-
集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
常用方法
-
add()
添加单个元素 -
add(int index, Object ele)
在 index 位置插入 ele 元素 -
addAll(int index, Collection eles)
从 index 位置开始将 eles 中的所有元素添加进来 -
get(int index)
获取指定 index 位置的元素 -
indexOf(Object obj)
返回 obj 在集合中首次出现的位置 -
lastIndexOf(Object obj)
返回 obj 在当前集合中末次出现的位置 -
remove(int index)
移除指定 index 位置的元素,并返回此元素 -
set(int index, Object ele)
设置指定 index 位置的元素为 ele , 相当于是替换 -
subList(int fromIndex, int toIndex)
返回从 [fromIndex,toIndex) 位置的子集合
ArrayList
描述
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null
元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。ArrayList的特点是:随机访问、查询快,增删慢,轻量级,线程不安全,初始化容量是10,增量是原来的1.5倍。
特点
-
ArrayList 底层实现是一个
Object
数组。 -
ArrayList 只能存储对象引用类型,也就是说当我们需要装载的数据是诸如
int
、float
等基本数据类型的时候,必须把它们转换成对应的包装类。 -
ArrayList 可以加入 null,并且可以多个 null。
-
ArrayList线程不安全,在多线程情况下,不建议使用 ArrayList。
扩容机制
ArrayList 中维护了一个 Object 类型的数组 elementData。创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0,第 1 次添加,则扩容 elementData 为 10,如需再次扩容,则扩容 elementData 为 1.5 倍。创建 ArrayList 对象时,如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍。
LinkedList
描述
LinkedList底层是通过双向链表实现的,所以它不能随机访问,而且需要查找元素必须要从开头或结尾(从靠近指定索引的一端)开始一个一个的找。使用双向链表则让增加、删除元素比较的方便,但查询变得困难。
特点
查询慢,增删快,线程不安全。
常用方法
-
void addFirst(E e)
:在该列表开头插入指定的元素。 -
void addLast(E e)
:将指定的元素追加到此列表的末尾。 -
E element()
:检索但不删除此列表的头(第一个元素)。 -
E getFirst()
:返回此列表中的第一个元素。 -
E getLast()
:返回此列表中的最后一个元素。 -
boolean offer(E e)
:将指定的元素添加为此列表的尾部(最后一个元素)。 -
boolean offerFirst(E e)
:在此列表的前面插入指定的元素。 -
boolean offerLast(E e)
:在该列表的末尾插入指定的元素。 -
E poll XXX()
:检索并删除此列表的头(第一个元素)。 -
E peek XXX()
:检索但不删除此列表的头(第一个元素)。 -
E pop()
:从此列表表示的堆栈中弹出一个元素。 -
void push(E e)
:将元素推送到由此列表表示的堆栈上。 -
E remove XXX()
:从列表中删除指定元素的第一个出现(如果存在)。
Vector
描述
Vector和ArrayList几乎一样,它们都是通过Object数组来实现的。但是Vector是线程安全的,和ArrayList相比,Vector中很多方法是用synchronized关键字处理过来保证证数据安全,这就必然会影响其效率,所以你的程序如果不涉及到线程安全问题,那么推荐使用ArrayList集合。其实无论如何大家都会选择ArrayList的,因为Vector已经很少用了,几乎面临淘汰。在多线程中更多的是使用JUC集合类的并发容器。
特点
-
Vector 底层也是一个对象数组和ArrayList几乎一样。
-
Vector是线程同步的,即线程安全,Vector 类的操作方法带有 synchronized。
扩容机制
1、创建 Vector 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0
-
第 1 次添加,则扩容 elementData 为 10
-
如需再次扩容,则扩容 elementData 为 2 倍
2、创建 Vector 对象时,如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小
-
如果需要扩容,则直接扩容 elementData 为 2倍
3、创建 Vector 对象时,如果使用的是指定大小和增量的构造器(自定义扩容),则初始 elementData 容量为指定大小
-
如果需要扩容,则直接扩容原容量+容量增量
Set接口
描述
Set和List一样都是继承自Collection接口,无序(添加和取出顺序不一致),取出的顺序的顺序虽然不是添加的顺序,但是每次取出顺序是固定的。不允许重复元素,因此最多只包含一个 null。不能使用索引的方式来获取。Set 接口实现类常用的有HashSet
、LinkedHashSet
、TreeSet
。
HashSet
描述
HashSet是 按照哈希算法(hashCode)来存储集合中的对象,所以它是无序的,同时也不能保证元素的排列顺序。 其底层是包装了一个HashMap去实现的,所以其查询效率非常高。而且在增加和删除的时候由于运用hashCode的值来比较确定添加和删除元素的位置,所以不存在元素的偏移,效率也非常高。因此HashSet的查询、增加和删除元素的效率都是非常高的。但是HashSet增删的高效率是通过花费大量的空间换来的:因为空间越大,取余数相同的情况就越小。HashSet这种算法会建立许多无用的空间。使用HashSet接口时要注意,如果发生冲突,就会出现遍历整个数组的情况,这样就使得效率非常的低。
特点
-
HashSet 实现了 Set 接口。
-
HashSet 底层实际上是 HashMap,HashSet的构造方法中实际创建的是HashMap,HashMap 底层是数组 + 链表 + 红黑树(JDK1.8)。
-
HashSet 可以存放 null 值,但是只能有一个 null,不能有重复元素/对象。
-
HashSet 不保证元素是有序的(不保证存放元素的插入顺序和取出顺序一致)。
LinkedHashSet
描述
LinkedHashSet继承自HashSet类,它不仅实现了哈希算法(hashCode),还实现了链表的数据结构,提供了插入和删除的功能。他有HashSet全部特性,但它新增了一个重要特性,就是元素按照插入的顺序存储。所以当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。正是因为多加了这样一种数据结构,所以它的效率较低,不建议使用,如果要求一个集合急要保证元素不重复,也需要记录元素的先后添加顺序,才选择使用LinkedHashSet。
特点
-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
-
LinkedHashSet 不允许添重复元素。
TreeSet
描述
TreeSet的底层是 基于TreeMap中实现的,都是基于红黑树实现的,红黑树是一种平衡二叉树,查询效率高于链表。它不仅能保证元素的唯一性,还能对元素按照某种规则进行排序。它继承了AbstractSet抽象类,实现了NavigableSet ,Cloneable,可序列化接口。而NavigableSet 又继承了SortedSet接口,此接口主要用于排序操作,即实现此接口的子类都属于排序的子类,有可排序的功能。TreeSet中的元素支持2种排序方式:自然排序或者定制排序,使用方式具体取决于我们使用的构造方法(默认使用自然排序)。
特点
-
元素唯一。
-
实现排序(取出的元素是经过排序的)。
Queue接口
描述
Queue接口与List、Set是同一级别的,都继承了Collection接口。Queue表示的是队列,它的特点是:先进先出(FIFO,First-in-First-Out) 。队列主要分为两大类:一类是BlockingDeque阻塞队列(Queue的子接口),它的主要实现类包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一类是Deque双端队列(也是Queue的子接口),支持在头部和尾部两端插入和移除元素,主要实现类包括:ArrayDeque、LinkedList。
阻塞队列
-
ArrayBlockingQueue:基于数组实现的有界阻塞队列。它的容量是固定的,一旦达到容量上限,插入操作将会被阻塞。
-
LinkedBlockingQueue:基于链表实现的可选有界或无界阻塞队列。如果初始化时指定了容量,则为有界队列;否则为无界队列。
-
PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列。元素按照优先级进行排序,每次获取操作都会返回优先级最高的元素。
-
SynchronousQueue:一个没有存储元素的阻塞队列。每个插入操作必须等待一个对应的删除操作,反之亦然。
Map接口
描述
Map接口与Collection接口是完全不同的。Map中保存的是具有“映射关系”的数据,即是由一系列键值对组成的集合,提供了key到value的映射,也就是说一个key对应一个value,其中key和value都可以是任何引用数据类型。但是Map中不能存在相同的key值,对于相同key值的Map对象会通过equals()方法来判断key值是否相等,只要该方法的结果是true,Map就不会再次接收这个对象了,当然value值可以相同。实现了Map接口的类主要的有以下几个:HashMap、Hashtable、LinkedHashMap、WeakHashMap、TreeMap、IdentifyHashMap、EnumMap。其中Map集合还和Set集合有着非常紧密的联系,因为很多Set集合中底层就是用Map来实现的。
HashMap
描述
HashMap是Map集合中使用最多的,也是集合中最最最重要的一个类,它是Hashtable的一个轻量级版本,它是继承了Abstractmap类。JDK1.7 HashMap底层基于数组+链表;JDK1.8 HashMap是基于数组+链表+红黑树,当链表长度大于8时数组长度大于等于64时会将链表转化成红黑树。内部所有方法是不同步的,即线程不安全,但是效率高。在 HashtMap 中 key 和 value 均均可为 null。
特点
-
存储的元素为Key-Value的形式。
-
Key可以为null,Value也可以为null。
-
Key的值是唯一的,key不能重复,但是值可以重复,允许使用 null 键和 null 值。
-
元素是无序的。
-
线程不安全。
数据结构
JDK1.7
HashMap底层基于数组+链表,插入数据使用头插法(头插法多线程情况下可能死循环)。
JDK1.8
HashMap是基于数组+链表+红黑树,当链表长度大于8时并且当容量大于64时会将链表转化成红黑树,插入数据使用尾插发。
扩容机制
如果链表太长,也会影响查询效率,所以JDK8后,引入了红黑树,以进一步提升查询效率。当链表长度达到8时(并且数组长度>=64),链表转为红黑树,当树节点数量降为6时再次退化成链表。当元素数量继续上升,红黑树节点数量还会不断增加,查询效率还是逐渐降低,这时会进行数组扩容,重新分布元素,以降低树的高度,提升查询效率。
默认加载因子为0.75,容器中元素个数达到容器容量*0.75
时,如16*0.75=12
时,数组进行扩容,新容量为原来的2倍,这时会将所有元素重新计算哈希,重新分布元素位置。总之扩容很消耗性能,所以在预知元素个数的情况下,尽量指定合适容器容量,以减少扩容操作。
使用案例
Map<String, String> map = new HashMap<String, String>(){ { put("zhangsan", "张三"); put("lisi", "李四"); put("wangwu", "王五"); } }; // 遍历方式一(推荐) Set<Map.Entry<String, String>> entries = map.entrySet(); for(Map.Entry<String, String> entry : entries) { log.info("Key:[{}];Value:[{}]", entry.getKey(), entry.getValue()); } // 遍历方式二(不推荐,遍历一遍Set<String>集合,存储的是Key,再通过Key获取Value,相当于遍历两边) Set<String> keys = map.keySet(); for(String key : keys) { log.info("Key:[{}];Value:[{}]", key, map.get(key)); } // 方式三(推荐) Iterator<Map.Entry<String, String>> iteratorMap = entries.iterator(); while(iteratorMap.hasNext()) { Map.Entry<String, String> next = iteratorMap.next(); log.info("Key:[{}]-Value:[{}]", next.getKey(), next.getValue()); } // 方式四(不方便DeBug),forEach的参数是BiConsumer函数式接口 map.forEach((k,v) -> { log.info("Key:[{}]-Value:[{}]", k, v); }); // 方式五(不方便DeBug) map.entrySet().stream().forEach(data -> { log.info("Key:[{}]-Value:[{}]", data.getKey(), data.getValue()); });
总结
-
底层结构哈希表,数组+链表+红黑树,增删改查综合效率较高。
-
key是唯一,需要重写hashCode和equals。
-
初始化容量,jdk8中第一次添加元素时,扩容容量为16,早期版本(jdk8之前)创建集合对象时初始化容量为16。
-
如果通过有参构造,指定初始容量为cap,实际初始容量为2的n次幂,并且大于等于cap。
-
当然元素个数达到临界值,即加载因子0.75*容量时,进行扩容,扩容为原来的2倍。
-
链表大于8时,并且数组容量达到64,链表转为红黑树,链表小于6时,红黑树转为链表。(注意:当链表大于8时,而数组没有达到64则继续扩容数组至64为止)。
-
采用尾插发插入元素。
Hashtable
特点
-
存放的元素是键值对:即 K-V。
-
Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException。
-
Hashtable 使用方法基本上和 HashMap 一样。
-
Hashtable 是线程安全的 (synchronized), HashMap 是线程不安全的。
Hashtable和HashMap对比
类名称 | 版本 | 线程安全(同步) | 效率 | 允许null键null值 |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 高 | 可以 |
Hashtable | 1.0 | 安全 | 较低 | 不可以 |
Properties
特点
-
Properties 类继承自 Hashtable 类并且实现了 Map 接口,也是使用一种键值对的形式来保存数据。
-
Properties 使用特点和 Hashtable 类似,key 和 value 不能为 null。
-
Properties 还可以用于从 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改。
-
xxx.properties 文件通常作为配置文件。
使用案例
Properties properties = new Properties(); properties.load(BaseJavaSEApplication.class.getClassLoader() .getResourceAsStream("config/application.properties")); properties.forEach((k,v) -> { log.info("Key:[{}]-Value:[{}]", k, v); });
LinkedHashMap
特点
LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
TreeMap
描述
TreeMap底层是用红黑树算法实现,它实现SortMap接口,所以其内部元素默认按照所有的key进行排序。当然也支持2种排序方式:自然排序或者定制排序。其中TreeMap中的key不可null,而它非线程安全的。使用可以参考上面的TreeSet示例,使用方式差不多。
WeakHashMap
描述
WeakHashMap与HashMap的用法基本相似。区别在于,HashMap的key保留了对实际对象的"强引用",这意味着只要该HashMap对象不被销毁,该HashMap所引用的对象就不会被垃圾回收。但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,当垃圾回收了该key所对应的实际对象之后,WeakHashMap也可能自动删除这些key所对应的key-value对。
IdentityHashMap
描述
IdentityHashMap是一种可重复key的集合类。它和HashMap类似,所以我们一般都是拿IdentityHashMap和HashMap来进行比较,因为它们两者判断重复key的方式不一样。IdentifyHashMap中判断重复key相等的条件是:(k1= =k2),也就是说它只比较普通值是否相等,不比较对象中的内容。而HashMap中类判断重复key相等的条件是: (k1= =null?k2==null:k1.equals(k2))= =true),它不仅比较普通值,而且比对象中的内容是否相等。
EnumMap
描述
EnumMap这个类是专门为枚举类而设计的有键值对的集合类。集合中的所有键(key)都必须是单个同一个类型的枚举值,创建EnumMap时必须显式或隐式指定它对应的枚举类。当EnumMap创建后,其内部是以数组形式保存,所以这种实现形式非常紧凑高效。EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护来维护key-value对的次序。可以通过keySet()、entrySet()、values()等方法来遍历EnumMap即可看到这种顺序。EnumMap不允许使用null作为key值,但允许使用null作为value。如果试图使用null作为key将抛出NullPointerException异常。如果仅仅只是查询是否包含值为null的key、或者仅仅只是使用删除值为null的key,都不会抛出异常。
Collections
描述
Collections 是一个操作 Set、List 和 Map 等集合的工具类。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
常用方法
-
public static boolean addAll(Collection<? super T> c,T... elements)
将所有指定元素添加到指定 collection 中。 -
public static int binarySearch(List<? extends Comparable<? super T>> list,T key)
在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。 -
public static int binarySearch(List<? extends T> list,T key,Comparator<? super T> c)
在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且集合也事先必须是按照c比较器规则进行排序过的,否则结果不确定。 -
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
在coll集合中找出最大的元素,集合中的对象必须是T或T的子类对象,而且支持自然排序 -
public static T max(Collection<? extends T> coll,Comparator<? super T> comp)
在coll集合中找出最大的元素,集合中的对象必须是T或T的子类对象,按照比较器comp找出最大者 -
public static void reverse(List<?> list)
反转指定列表List中元素的顺序。 -
public static void shuffle(List<?> list)
List 集合元素进行随机排序,类似洗牌 -
public static <T extends Comparable<? super T>> void sort(List list)
根据元素的自然顺序对指定 List 集合元素按升序排序 -
public static void sort(List list,Comparator<? super T> c)
根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 -
public static void swap(List<?> list,int i,int j)
将指定 list 集合中的 i 处元素和 j 处元素进行交换 -
public static int frequency(Collection<?> c,Object o)
返回指定集合中指定元素的出现次数 -
public static void copy(List<? super T> dest,List<? extends T> src)
将src中的内容复制到dest中 -
public static boolean replaceAll(List list,T oldVal,T newVal)
使用新值替换 List 对象的所有旧值 -
Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
-
Collections类中提供了多个unmodifiableXxx()方法,该方法返回指定 Xxx的不可修改的视图。
Arrays
描述
Arrays类是Java标准库中提供的一个工具类,用于操作数组。它提供了一系列静态方法,用于对数组进行排序、搜索、比较等操作。
常用方法
-
sort(T[] a)
:对指定数组进行升序排序。 -
sort(T[] a, Comparator<? super T> c)
:使用指定的比较器对数组进行排序。 -
binarySearch(T[] a, T key)
:在已排序的数组中使用二分查找算法查找指定元素的索引。 -
equals(T[] a, T[] a2)
:比较两个数组是否相等。 -
fill(T[] a, T val)
:将指定值填充到数组的每个元素中。 -
copyOf(T[] original, int newLength)
:将原始数组复制到一个新数组中,新数组的长度为指定长度。 -
asList(T... a)
:将指定的元素转换为一个固定大小的列表。
注意事项
Arrays.asList()
方法生成的 ArrayList
是 Arrays
静态内部类的ArrayList
同时也 extends AbstractList
类,但是AbstractList类内部对 set()
、add()
、remove()
方法内部直接throw new UnsupportedOperationException()
。所以在使用 Arrays.asList()
处理集合时重新构造ArrayList集合 List<String> list = new ArrayList<>(asLit)
;
快速失败机制
描述
快速失败(fail-fast)机制是集合框架的一种设计策略,旨在提高集合的安全性和一致性。它通过在每个集合上维护一个修改计数器(modCount)来实现。当集合发生结构性修改时,例如添加或删除元素,修改计数器会递增。而在迭代器遍历集合时,迭代器会检查修改计数器的值是否与迭代开始时的值相等,如果不相等,则说明集合已经被修改,迭代器会立即抛出ConcurrentModificationException异常。
源码
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
快速失败场景
错误案例一:
使用forEach遍历删除集合时会触发快速失败机制。
List<String> list = new ArrayList<String>(){ { add("张三"); add("李四"); add("王五"); } }; for(String str : list) { if(Objects.equals(str, "张三")) { list.remove(str); } }
错误案例二:
使用Iterator遍历删除集合时会触发快速失败机制。
List<String> list = new ArrayList<String>(){ { add("张三"); add("李四"); add("王五"); } }; Iterator<String> iterator1 = list.iterator(); while (iterator1.hasNext()) { String next = iterator1.next(); if(Objects.equals(next, "张三")) { list.remove(next); } }
正确案例:
使用Iterator遍历,迭代器删除集合元素。
List<String> list = new ArrayList<String>(){ { add("张三"); add("李四"); add("王五"); } }; Iterator<String> iterator1 = list.iterator(); while (iterator1.hasNext()) { String next = iterator1.next(); if(Objects.equals(next, "张三")) { iterator1.remove(); } }
数据结构
List集合
List | 结构 | 图 |
---|---|---|
ArrayList | Object数组 | |
LinkedList | 双向链表 | |
Vector | Object数组 | 同 ArrayList |
Map集合
Map | 结构 | 图 |
---|---|---|
HashMap | 数组+链表+红黑树 | |
Hashtable | 数组+链表 | |
LinkedHashMap | 数组+双向链表+红黑树 | |
TreeMap | 红黑树 |