目录
序言:Collection
一、List 总结篇
1、List 接口描述
2、使用场景
3、区别
3.1 Aarraylist 和 Linkedlist
3.2 Vector 和 ArrayList 的区别
二、Map 总结篇
2.0 HashMap 和 TreeMap的不同点
2.1、Map 概述
2.2、内部哈希:哈希映射技术
2.3 Map 优化
2.3.1 调整容器初始化的大小
2.3.2 调整负载因子
2.4、HashMap 面试“明星”问题汇总
1、size 必须是 2 的整数次方原因;
2、get 和 put 方法流程;
3、resize 方法;
4、影响 HashMap 的性能因素(key 的 hashCode 函数实现、loadFactor、初始容量);
5、HashMap key 的 hash 值计算方法以及原因(见上面 hash 函数的分析);1.8的
6、1.8 HashMap 内部存储结构:Node 数组 + 链表或红黑树;
7、1.8 table[i] 位置的链表什么时候会转变成红黑树;
8、HashMap 主要成员属性:threshold、loadFactor、HashMap 的懒加载;
9、HashMap 的 get 方法能否判断某个元素是否在 map 中;【不能:null】
10、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?
11、HashMap 的 value 允许为 null,但是 HashTable 和 ConcurrentHashMap 的 value 都不允许为 null,试分析原因?
12、HashMap 中的 hook 函数
三、Set 总结篇
四、对集合的选择
4.1 对 List 的选择
4.2 对 Set 的选择
4.3 对 Map 的选择
五、Comparable 和 Comparator
前面已经充分介绍了有关于 List 接口的大部分知识,如 ArrayList、LinkedList、Vector、Stack,通过这几个知识点可以对List 接口有了比较深的了解了。只有通过归纳总结的知识才是你的知识。所以下面就List接口做一个总结。推荐阅读:
【搞定Java基础-集合篇】第二篇 源码ArrayList、LinkedList和Vector的区别
List 接口,称为有序的 Collection,也就是序列。该接口可以对列表中的每一个元素的插入位置进行精确的控制,同时用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
Collection:Collection 层次结构中的根接口。它表示一组对象,这些对象也称为 Collection 的元素。对于 Collection 而言,它不提供任何直接的实现,所有的实现全部由它的子类负责;
AbstractCollection: 提供 Collection 接口的骨干实现,以最大限度地减少了实现此接口所需的工作(contains,toArray等)。对于我们而言要实现一个不可修改的 Collection,只需扩展此类,并提供 iterator 和 size 方法的实现。但要实现可修改的 Collection,就必须另外重写此类的 add 方法(否则,会抛出 UnsupportedOperationException),iterator 方法返回的迭代器还必须另外实现其 remove 方法;
Iterator: 迭代器;
ListIterator: 列表迭代器,允许程序员按任一方向遍历列表. 迭代期间可修改列表,并获得迭代器在列表中的当前位置;
List: 继承于Collection的接口。它代表着有序的队列;
AbstractList: List 接口的骨干实现,以最大限度地减少实现“随机访问”数据存储(如数组)支持的该接口所需的工作;
Queue: 队列。提供队列基本的插入、获取、检查操作;
Deque: 一个线性 Collection,支持在两端插入和移除元素。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列;
AbstractSequentialList: 提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作。从某种意义上说,此类与在列表的列表迭代器上实现“随机访问”方法;
LinkedList: List 接口的链接列表实现。它实现所有可选的列表操作;
ArrayList: List 接口的大小可变数组的实现。它实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小;
Vector: 实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件;
Stack: 后进先出(LIFO)的对象堆栈。它通过五个操作对类 Vector 进行了扩展 ,允许将向量视为堆栈;
Enumeration: 枚举,实现了该接口的对象,它生成一系列元素,一次生成一个。连续调用 nextElement 方法将返回一系列的连续元素;
在前面文章中已经详细介绍了 HashMap、HashTable、TreeMap 的实现方法,从数据结构、实现原理、源码分析三个方面进行阐述,对这个三个类应该有了比较清晰的了解,下面就对 Map 做一个简单的总结。
推荐阅读:
【搞定Java基础 - 集合篇】第三篇、源码Java7 -HashMap、HashTable、ConCurrentHashMap 【java7都是从头插入,且先扩容再插入,只有hashMap默认容量16允许key为null,hashTable默认容量11和ConCurrentHashMap默认并发数16,entry数组2的key和value都不为null】【HashMap和ConCurrentHashMap都是2倍扩容,hashTable是2倍+1】
【搞定Java基础-集合篇】第四篇 Java8-HashMap和ConCurrentHashMap
【搞定Java基础-集合】第七篇 TreeMap 和红黑树
【搞定Java基础-集合】第六篇:深入理解 LinkedHashMap 和 LRU 缓存
摘要:HashMap 和双向链表合二为一即是 LinkedHashMap
友情提示
1、LinkedHashMap 概述
2、LinkedHashMap 在 JDK 中的定义
2.1 类结构定义
2.2 成员变量定义:增加了两个独有属性:双向链表头结点 header 和 迭代顺序标志位accessOrder【true=按访问顺序排序,false=按插入顺序排序(默认)】
2.3 成员方法定义
2.4 基本元素 Entry:重新定义了Entry,增加了两个指针 before 和 after用于维护双向链表
2.5 LinkedHashMap 的构造函数
2.6 LinkedHashMap 的数据结构
2.7 LinkedHashMap 的快速存取
LinkedHashMap 的存储实现 : put(key, vlaue):在 LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore(head) 方法将其链入到双向链表中。其中,addBefore 方法本质上是一个双向链表的插入操作
LinkedHashMap 的扩容操作 : resize(),扩容为原来的2倍
LinkedHashMap 的读取实现 :get(Object key)
2.8 LinkedHashMap 存取小结
1、LinkedHashMap 的存取过程基本与 HashMap 类似,只是在细节实现上稍有不同,这是由 LinkedHashMap 本身的特性所决定的,因为它要额外维护一个双向链表用于保持迭代顺序。
2、在 put 操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 put 操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore 方法将其链入到双向链表中。
3、在扩容操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 resize 操作,但是鉴于性能和 LinkedHashMap 自身特点的考量,LinkedHashMap 对其中的重哈希过程(transfer方法)进行了重写(照着双向链表的顺序来重哈希)。在读取操作上,LinkedHashMap 中重写了HashMap 中的 get 方法(增加了 recordAccess方法,如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做;如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将 e 移到链表的末尾处),通过 HashMap 中的 getEntry 方法获取 Entry 对象,在此基础上,进一步获取指定键对应的值。
3、LinkedHashMap 与 LRU
3.1 put 操作与标志位 accessOrder:recordAccess 提供了 LRU 算法的实现,它将最近使用的 Entry 放到双向循环链表的尾部。也就是说,当 accessOrder 为 true 时,get 方法和 put 方法(如果不存在一样的key是插入链表尾部,若已经存在一样的key,就是更新,更新后会挪到链表的尾部)都会调用 recordAccess 方法使得最近使用的Entry移到双向链表的末尾;当 accessOrder 为默认值false 时,从源码中可以看出 recordAccess 方法什么也不会做。
3.2 get 操作与标志位 accessOrder
3.3 LinkedListMap 与 LRU 小结【访问标志accessOrder是决定put和get时要不要按访问顺序,removeEldestEntry方法是决定何时删除最近最久未访问节点,默认是返回false,即不会删除,若要删除即要实现LRU,你只需要重写这个方法】
1、使用 LinkedHashMap 实现 LRU 的必要前提是将 accessOrder 标志位设为 true 以便开启按访问顺序排序的模式。我们可以看到,无论是 put 方法还是 get 方法,都会导致目标 Entry 成为最近访问的 Entry,因此就把该 Entry 加入到了双向链表的末尾:get 方法通过调用 recordAccess 方法来实现。
2、put 方法在插入新的 Entry 时,通过createEntry 中的 addBefore 方法来实现插入链表尾部;在覆盖已有 key 的情况下,通过 recordAccess 方法来实现将更新的entry放到链表尾部。get操作也通过recordAccess 方法将该entry放到链表尾部。多次操作后,双向链表前面的 Entry 便是最近没有使用的。
3、在每次put插入新的Entry时,都会根据你重写的removeEldestEntry方法来决定是否要删除最近最久未访问元素(默认返回false,你可以重写成当节点个数大于多少时返回true),这样当节点个数大于某个数时,就会删除最前面的 Entry(head后面的那个Entry),因为它就是最近最久未使用的 Entry。
4、使用 LinkedHashMap 实现 LRU 算法
5、LinkedHashMap 有序性原理分析【利用双向链表进行迭代输出】
6、LinkedHashMap 【JDK1.8】
6.1 构造函数【增加了双向链表的head和tail,以及访问标志accessOrder】
二、get函数
三、afterNodeXXXX命名格式的三个函数在HashMap中只是一个空实现,是专门用来让LinkedHashMap重写实现的hook函数
3.1 afterNodeAccess(Node p) { } //处理元素被访问后的情况:其功能为如果accessOrder为true,则将刚刚访问的元素移动到链表末尾
3.2 afterNodeInsertion(boolean evict) { } //处理元素插入后的情况:即是否要删除最久未访问元素【根据你重写的removeEldestEntry()默认返回false,无需删除,如果你重写的返回true,则在元素插入后会删除最近最久未访问元素。】
3.3 afterNodeRemoval(Node p) { } //处理元素被删除后的情况:在HashMap.removeNode()的末尾处调用, 将e从LinkedHashMap的双向链表中删除
7、总结
1、LinkedHashMap 在 HashMap 的数组加链表结构的基础上,将所有节点连成了一个双向链表。
2、put 方法在插入新的 Entry 时,通过createEntry 中的 addBefore 方法来实现插入链表尾部;在覆盖已有 key 的情况下,通过 recordAccess 方法来实现将更新的entry放到链表尾部。get操作也通过recordAccess 方法将该entry放到链表尾部。多次操作后,双向链表前面的 Entry 便是最近没有使用的。在每次put插入新的Entry时,都会根据你重写的removeEldestEntry方法来决定是否要删除最近最久未访问元素(默认返回false,你可以重写成当节点个数大于多少时返回true),这样当节点个数大于某个数时,就会删除最前面的 Entry(head后面的那个Entry),因为它就是最近最久未使用的 Entry。【实现 LRU 可以直接实现继承 LinkedHashMap 并重写removeEldestEntry 方法来设置缓存大小。JDK 中实现了 LRUCache 也可以直接使用。】
3、LinkedHashMap 的扩容比 HashMap 来的方便,因为 HashMap 需要将原来的每个链表的元素分别在新数组进行插入链化,而 LinkedHashMap 的元素都连在一个链表上,可以直接迭代然后插入。
1、HashMap 通过 hashCode 对其内容进行快速查找,而 TreeMap 中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。HashMap 中元素的排列顺序是不固定的)。
2、在 Map 中插入. 删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap 会更好。使用 HashMap 要求添加的键类明确定义了 hashCode() 和 equals() 的实现。 这个 TreeMap 没有调优选项,因为该树总处于平衡状态。
首先先看 Map 的结构示意图:
Map: “键值对” 映射的抽象接口。该映射不包括重复的键,一个键对应一个值;
SortedMap: 有序的键值对接口,继承 Map 接口;
NavigableMap: 继承 SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法的接口;
AbstractMap: 实现了 Map 中的绝大部分函数接口。它减少了 “Map的实现类” 的重复编码;
Dictionary: 任何可将键映射到相应值的类的抽象父类。目前被 Map 接口取代;
TreeMap: 有序散列表,实现 SortedMap 接口,底层通过红黑树实现;
HashMap: 是基于“拉链法”实现的散列表。底层采用 “数组+链表” 实现;
WeakHashMap: 基于“拉链法”实现的散列表;
HashTable: 基于“拉链法”实现的散列表。
它们之间的区别:
几乎所有通用 Map 都使用哈希映射技术。对于我们程序员来说我们必须要对其有所了解。
哈希映射技术是一种将元素映射到数组的非常简单的技术。由于哈希映射采用的是数组结构,那么必然存在一种用于确定任意键访问数组的索引机制,该机制能够提供一个小于数组大小的整数,我们将该机制称之为哈希函数。在 Java 中我们不必为寻找这样的整数而大伤脑筋,因为每个对象都必定存在一个返回整数值的 hashCode 方法,而我们需要做的就是将其转换为整数,然后再将该值除以数组大小取余即可。如下
int hashValue = Maths.abs(obj.hashCode()) % size;
首先我们这样假设,假设哈希映射的内部数组的大小只有1,所有的元素都将映射该位置(0),从而构成一条较长的链表。由于我们更新、访问都要对这条链表进行线性搜索,这样势必会降低效率。我们假设,如果存在一个非常大数组,每个位置链表处都只有一个元素,在进行访问时计算其 index 值就会获得该对象,这样做虽然会提高我们搜索的效率,但是它浪费了空间。诚然,虽然这两种方式都是极端的,但是它给我们提供了一种优化思路:使用一个较大的数组让元素能够均匀分布。 在 Map 有两个会影响到其效率,一是容器的初始化大小、二是负载因子。
在哈希映射表中,内部数组中的每个位置称作 “存储桶” (bucket),而可用的存储桶数(即内部数组的大小)称作容量 (capacity)。我们为了使 Map 对象能够有效地处理任意数的元素,将 Map 设计成可以调整自身的大小。我们知道当 Map 中的元素达到一定量的时候就会调整容器自身的大小,但是这个调整大小的过程其开销是非常大的。调整大小需要将原来所有的元素插入到新数组中。我们知道 index = hash(key) % length。这样可能会导致原先冲突的键不在冲突,不冲突的键现在冲突的,重新计算、调整、插入的过程开销是非常大的,效率也比较低下。所以,如果我们开始知道 Map 的预期大小值,将 Map调整的足够大,则可以大大减少甚至不需要重新调整大小,这很有可能会提高速度。 下面是 HashMap 调整容器大小的过程,通过下面的代码我们可以看到其扩容过程的复杂性:【1.7】
void resize(int newCapacity) {
Entry[] oldTable = table; // 原始容器
int oldCapacity = oldTable.length; // 原始容器大小
if (oldCapacity == MAXIMUM_CAPACITY) { // 是否超过最大值:1073741824
threshold = Integer.MAX_VALUE;
return;
}
// 新的数组:大小为 oldCapacity * 2
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
/*
* 重新计算阀值 = newCapacity * loadFactor > MAXIMUM_CAPACITY + 1 ?
* newCapacity * loadFactor :MAXIMUM_CAPACITY + 1
*/
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 将元素插入到新数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
为了确认何时需要调整 Map 容器,Map 使用了一个额外的参数并且粗略计算存储容器的密度。在 Map 调整大小之前,使用”负载因子”来指示 Map 将会承担的“负载量”,也就是它的负载程度,当容器中元素的数量达到了这个“负载量”,则 Map 将会进行扩容操作。
例如:如果负载因子大小为 0.75,默认容量为11(HashTable),则 11 * 0.75 = 8.25 = 8,所以当我们容器中插入第 8 个元素的时候,Map 就会调整容器的大小。
负载因子本身就是在空间和时间之间的折衷:
当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的空间,使得数组中的大部分空间没有得到利用,元素分布比较稀疏,同时由于 Map 频繁的调整大小,可能会降低性能。
但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值 0.75。
你知道 HashMap 吗,请你讲讲 HashMap?
这个问题不单单考察你对 HashMap 的掌握程度,也考察你的表达、组织问题的能力。个人认为应该从以下几个角度入手(所有常见 HashMap 的考点问题总结):
1、size 必须是 2 的整数次方原因;
2、get 和 put 方法流程;
3、resize 方法;
4、影响 HashMap 的性能因素(key 的 hashCode 函数实现、loadFactor、初始容量);
5、HashMap key 的 hash 值计算方法以及原因(见上面 hash 函数的分析);
6、HashMap 内部存储结构:Node 数组 + 链表或红黑树;
7、table[i] 位置的链表什么时候会转变成红黑树(上面源码中有讲);
8、HashMap 主要成员属性:threshold、loadFactor、HashMap 的懒加载;
9、HashMap 的 get 方法能否判断某个元素是否在 map 中;【不能:null】
10、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?
11、HashMap 的 value 允许为 null,但是 HashTable 和 ConcurrentHashMap 的 value 都不允许为 null,试分析原因?
12、HashMap 中的 hook 函数(在后面讲解 LinkedHashMap 时会讲到,这也是面试时拓展的一个点)
原因是:
java1.7的hashmap
Java1.8的hashMap【实质扩容的条件和1.7一样的,1.7是插入之前>=阈值就扩容再插入,1.8是插入之后发现 此时size > 阈值就扩容,所以都是此次会超过阈值就扩容】
有两种情况会调用resize:
当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的空间,使得数组中的大部分空间没有得到利用,元素分布比较稀疏,同时由于 Map 频繁的调整大小,可能会降低性能。
但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值 0.75。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap 允许 key 为null,null 的 hash 为 0。非 null 的 key 的 hash 高 16 位和低 16 位分别由:key 的 hashCode 高 16 位 和 hashCode 的高 16 位异或 hashCode 的低16位组成。主要是为了增强 hash 的随机性,减少 hash & (n - 1) 的随机性,即减小 hash 冲突,提高 HashMap 的性能。
MIN_TREEIFY_CAPACITY值是64,也就是当链表长度>8的时候(插入后再转),有两种情况:
如果table数组的长度<64,此时进行扩容操作;
如果table数组的长度>=64,此时进行链表转红黑树结构的操作.
因为HashMap是可以存放值为null的,你并不能分辨到底是 不存在返回null 还是本身值是null;
// 入口,返回对应的value
public V get(Object key) {
Node e;
// hash函数上面分析过了
return (e = getNode(hash(key), key))== null ? null : e.value;
}
public boolean containsKey(Object key) {
// 注意与get函数区分,我们往map中put的所有的都被封装在Node中,
// 如果Node都不存在显然一定不包含对应的key
return getNode(hash(key), key) != null;
}
HashMap 在并发时可能出现的问题主要是两方面:
如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
1.7 (实际是Entry
同样1.8 在尾部插入的时候同时进行也会覆盖另一个线程的put
如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失【这是1.8,如果是1.7头插还会导致死循环】
1.8对table[i]是拆分成两个链表,再挂到table[i]和table[i+oldCap]下,一个线程拆分j位置会让 oldTab[j] = null; 另一个线程可能就认为该位置无元素,该线程把它扩容后的数组赋给table,就会丢失其他线程的扩容
如果扩容的同时也put元素,比如put正在迁移的位置,那么因为最后会把拆分的两个链表赋给那两个位置,所以会导致各自线程put的数据也丢失。
首先要明确 ConcurrentHashMap 和 Hashtable 从技术从技术层面讲是可以允许 value 为 null 。但是它们实际上是不允许的,这肯定是为了解决一些问题,为了说明这个问题,我们看下面这个例子(这里以 ConcurrentHashMap 为例,HashTable 也是类似)。
HashMap 由于允 value 为 null,get 方法返回 null 时有可能是 map 中没有对应的 key;也有可能是该 key 对应的 value 为 null。所以 get 不能判断 map 中是否包含某个 key,只能使用 contains 判断是否包含某个 key。
看下面的代码段,要求完成这个一个功能:如果 map 中包含了某个 key ,则返回对应的 value,否则抛出异常:
if (map.containsKey(k)) {
return map.get(k);
} else {
throw new KeyNotPresentException();
}
1、如果上面的 map 为HashMap,那么没什么问题,因为 HashMap 本来就是线程不安全的,如果有并发问题应该用ConcurrentHashMap,所以在单线程下面可以返回正确的结果。【单线程下,hashmap包含key,返回null或其他值,不包含就抛出异常,能反应真实情况;而ConCurrentHashMap是用于多线程的,在判断key存在后,key能被别的线程删掉了,它会返回null,这就表示不存在能反应真实的情况,所以ConcurrentHashMap不能存放null】
2、如果上面的 map 为ConcurrentHashMap,此时存在并发问题:在 map.containsKey(k) 和 map.get 之间有可能其他线程把这个 key 删除了,这时候 map.get 就会返回 null,而 ConcurrentHashMap 中不允许 value 为 null,也就是这时候返回了 null,一个根本不允许出现的值?
但是因为 ConcurrentHashMap 不允许 value 为 null,所以可以通过 map.get(key) 是否为 null 来判断该 map 中是否包含该 key,这时就没有上面的并发问题了。
三、afterNodeXXXX命名格式的三个函数在HashMap中只是一个空实现,是专门用来让LinkedHashMap重写实现的hook函数
3.1 afterNodeAccess(Node p) { } //处理元素被访问后的情况:其功能为如果accessOrder为true,则将刚刚访问的元素移动到链表末尾
3.2 afterNodeInsertion(boolean evict) { } //处理元素插入后的情况:即是否要删除最久未访问元素【根据你重写的removeEldestEntry()默认返回false,无需删除,如果你重写的返回true,则在元素插入后会删除最近最久未访问元素。】
3.3 afterNodeRemoval(Node p) { } //处理元素被删除后的情况:在HashMap.removeNode()的末尾处调用, 将e从LinkedHashMap的双向链表中删除
Set 就是 HashMap 将 value 固定为一个object,只存 key 元素包装成一个 entry 即可,其他和 Map 基本一样。
所有 Set 几乎都是内部用一个 Map 来实现,因为 Map 里的 KeySet 就是一个Set,而 value 是假值,全部使用同一个Object 即可。
Set 的特征也继承了那些内部的 Map 实现的特征。
HashSet:内部使用 HashMap 来存储元素和操作元素。
LinkedHashSet:内部使用 LinkedHashMap 来存储元素和操作元素。
TreeSet:内部是TreeMap 的 SortedSet。
ConcurrentSkipListSet:内部是 ConcurrentSkipListMap 的并发优化的 SortedSet。
CopyOnWriteArraySet:内部是 CopyOnWriteArrayList 的并发优化的 Set,利用其 addIfAbsent() 方法实现元素去重,如前所述该方法的性能很一般。
好像少了个 ConcurrentHashSet,本来也该有一个内部用 ConcurrentHashMap 的简单实现,但JDK偏偏没提供。Jetty就自己简单封了一个,Guava 则直接用 java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。
1、对于随机查询与迭代遍历操作,数组比所有的容器都要快。所以在随机访问中一般使用 ArrayList
2、LinkedList 使用双向链表对元素的增加和删除提供了非常好的支持,而 ArrayList 执行增加和删除元素需要进行元素位移。
3、对于 Vector 而已,我们一般都是避免使用。
4、将 ArrayList 当做首选,毕竟对于集合元素而已我们都是进行遍历,只有当程序的性能因为List的频繁插入和删除而降低时,再考虑 LinkedList。
1、HashSet 由于使用 HashCode 实现,所以在某种程度上来说它的性能永远比 TreeSet 要好,尤其是进行增加和查找操作。
2、虽然 TreeSet 没有 HashSet 性能好,但是由于它可以维持元素的排序,所以它还是存在用武之地的。
1、HashMap 与 HashSet 同样,支持快速查询。虽然 HashTable 速度的速度也不慢,但是在 HashMap 面前还是稍微慢了些,所以 HashMap 在查询方面可以取代 HashTable。
2、由于TreeMap 需要维持内部元素的顺序,所以它通常要比 HashMap 和 HashTable 慢。
实现 Comparable 接口可以让一个类的实例互相使用 compareTo 方法进行比较大小,可以自定义比较规则;
Comparator 则是一个通用的比较器,比较指定类型的两个元素之间的大小关系。