【搞定Java基础-集合】第十篇:Java 集合类总结篇

目录

序言: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 总结篇

前面已经充分介绍了有关于 List 接口的大部分知识,如 ArrayList、LinkedList、Vector、Stack,通过这几个知识点可以对List 接口有了比较深的了解了。只有通过归纳总结的知识才是你的知识。所以下面就List接口做一个总结。推荐阅读:

【搞定Java基础-集合篇】第二篇 源码ArrayList、LinkedList和Vector的区别

 1、List 接口描述

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 方法将返回一系列的连续元素;

二、Map 总结篇

在前面文章中已经详细介绍了 HashMapHashTableTreeMap 的实现方法,从数据结构、实现原理、源码分析三个方面进行阐述,对这个三个类应该有了比较清晰的了解,下面就对 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 的元素都连在一个链表上,可以直接迭代然后插入。

2.0 HashMap 和 TreeMap的不同点

1、HashMap 通过 hashCode 对其内容进行快速查找,而 TreeMap 中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。HashMap 中元素的排列顺序是不固定的)。

2、在 Map 中插入. 删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap 会更好。使用 HashMap 要求添加的键类明确定义了 hashCode() 和 equals() 的实现。 这个 TreeMap 没有调优选项,因为该树总处于平衡状态。

 

2.1、Map 概述

首先先看 Map 的结构示意图:

Map: “键值对” 映射的抽象接口。该映射不包括重复的键,一个键对应一个值;

SortedMap: 有序的键值对接口,继承 Map 接口;

NavigableMap: 继承 SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法的接口;

AbstractMap: 实现了 Map 中的绝大部分函数接口。它减少了 “Map的实现类” 的重复编码;

Dictionary: 任何可将键映射到相应值的类的抽象父类。目前被 Map 接口取代;

TreeMap: 有序散列表,实现 SortedMap 接口,底层通过红黑树实现;

HashMap: 是基于“拉链法”实现的散列表。底层采用 “数组+链表” 实现;

WeakHashMap: 基于“拉链法”实现的散列表;

HashTable: 基于“拉链法”实现的散列表。

它们之间的区别:

2.2、内部哈希:哈希映射技术

几乎所有通用 Map 都使用哈希映射技术。对于我们程序员来说我们必须要对其有所了解。

哈希映射技术是一种将元素映射到数组的非常简单的技术。由于哈希映射采用的是数组结构,那么必然存在一种用于确定任意键访问数组的索引机制,该机制能够提供一个小于数组大小的整数,我们将该机制称之为哈希函数。在 Java 中我们不必为寻找这样的整数而大伤脑筋,因为每个对象都必定存在一个返回整数值的 hashCode 方法,而我们需要做的就是将其转换为整数,然后再将该值除以数组大小取余即可。如下

int hashValue = Maths.abs(obj.hashCode()) % size;

2.3  Map 优化

首先我们这样假设,假设哈希映射的内部数组的大小只有1,所有的元素都将映射该位置(0),从而构成一条较长的链表。由于我们更新、访问都要对这条链表进行线性搜索,这样势必会降低效率。我们假设,如果存在一个非常大数组,每个位置链表处都只有一个元素,在进行访问时计算其 index 值就会获得该对象,这样做虽然会提高我们搜索的效率,但是它浪费了空间。诚然,虽然这两种方式都是极端的,但是它给我们提供了一种优化思路:使用一个较大的数组让元素能够均匀分布。 在 Map 有两个会影响到其效率,一是容器的初始化大小、二是负载因子

2.3.1  调整容器初始化的大小

在哈希映射表中,内部数组中的每个位置称作 “存储桶” (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<K,V> e : table) {
		while(null != e) {
			Entry<K,V> 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;
		}
	}
}

2.3.2  调整负载因子

为了确认何时需要调整 Map 容器,Map 使用了一个额外的参数并且粗略计算存储容器的密度。在 Map 调整大小之前,使用”负载因子”来指示 Map 将会承担的“负载量”,也就是它的负载程度,当容器中元素的数量达到了这个“负载量”,则 Map 将会进行扩容操作。

例如:如果负载因子大小为 0.75,默认容量为11(HashTable),则 11 * 0.75 = 8.25 = 8,所以当我们容器中插入第 8 个元素的时候,Map 就会调整容器的大小。

负载因子本身就是在空间和时间之间的折衷:

当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的空间,使得数组中的大部分空间没有得到利用,元素分布比较稀疏,同时由于 Map 频繁的调整大小,可能会降低性能。

但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值 0.75。

2.4、HashMap 面试“明星”问题汇总

你知道 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 时会讲到,这也是面试时拓展的一个点)

1、size 必须是 2 的整数次方原因;

原因是:

  •  1、* CPU对位运算支持较好,即位运算速度很快当 n 是 2 的整数次幂时:hash & (n - 1) 与 hash % n 是等价的,但是两者效率来讲是不同的,位运算的效率远高于取余 % 运算。****所以,HashMap中使用的是 hash & (n - 1)
  •  2、在1.8中,这还带来了一个好处,就是将旧数组中的 Node 迁移到扩容后的新数组中的时候有一个很方便的特性:【索引为 i 的节点,rehash后的索引只可能是 i 或者 i+oldcap,也就是我们可以这样处理:把 table[i] 这个桶中的 node 拆分为两个链表 l1 和 l2:如果hash & n == 0,那么当前这个 node 被连接到 l1 链表;否则连接到 l2 链表。这样下来,当遍历完 table[i] 处的所有 node 的时候,我们得到两个链表 l1 和 l2,这时我们令 newtab[i] = l1,newtab[i + n] = l2,这就完成了 table[i] 位置所有 node 的迁移(rehash),这也是 HashMap 中容量一定的是2的整数次幂带来的方便之处。 (因为Java8 是尾插,如果你一个一个的来放置的话,那么每个位置你都要遍历到该位置链表的尾部才能插入,耗时长【自己理解的】)】

2、get 和 put 方法流程;

java1.7的hashmap

Java1.8的hashMap【实质扩容的条件和1.7一样的,1.7是插入之前>=阈值就扩容再插入,1.8是插入之后发现 此时size > 阈值就扩容,所以都是此次会超过阈值就扩容】

3、resize 方法;

有两种情况会调用resize:

  • 1、之前说过 HashMap 是懒加载,第一次调用 HashMap 的 put 方法的时候 table 还没初始化,这个时候会执行 resize,进行table 数组的初始化,table 数组的初始容量保存在 threshold 中(如果从构造器中传入的一个初始容量的话),如果创建HashMap 的时候没有指定容量,那么 table 数组的初始容量是默认值:16。即,初始化 table 数组的时候会执行 resize 函数。
  • 2、扩容的时候会执行 resize 函数,插入元素后,当 size 的值 > threshold 的时候会触发扩容(1.8,先插入后发现>threshold),即执行 resize 方法,这时 table 数组的大小会翻倍。
  • 注意我们每次扩容之后容量都是翻倍( * 2),所以HashMap的容量一定是2的整数次幂。

4、影响 HashMap 的性能因素(key 的 hashCode 函数实现、loadFactor、初始容量);

  • 如果我们开始知道 Map 的预期大小值,将 Map调整的足够大,则可以大大减少甚至不需要重新调整大小,这会提高速度【避免了扩容】。
    • 假如你预先知道最多往 HashMap 中存储 64 个元素,那么你在创建 HashMap 的时候:如果选用无参构造器:默认容量16,在存储 16*loadFactor 个元素之后就要进行扩容(数组扩容涉及到连续空间的分配,Node 节点的 rehash,代价很高,所以要尽量避免扩容操作)。如果给构造器传入的参数是 64,这时 HashMap 中在存储 64 * loadFactor 个元素之后就要进行扩容;但是如果你给构造器传的参数为:(int)(64/0.75) + 1,此时就可以保证 HashMap 不用进行扩容,避免了扩容时的代价。
  • 负载因子本身就是在空间和时间之间的折衷:最好还是采用默认0.75

当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的空间,使得数组中的大部分空间没有得到利用,元素分布比较稀疏,同时由于 Map 频繁的调整大小,可能会降低性能。

但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值 0.75。

5、HashMap key 的 hash 值计算方法以及原因(见上面 hash 函数的分析);1.8的

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 的性能。

  • (因为如果直接使用 hashCode & (n - 1) 来计算 index,此时 hashCode 的高位随机特性完全没有用到,因为 n 相对于hashCode 的值很小,计算 index 的时候只能用到低 16 位。基于这一点,把 hashCode 高 16 位的值通过异或混合到hashCode 的低 16 位,由此来增强 hashCode 低 16 位的随机性。)

6、1.8 HashMap 内部存储结构:Node 数组 + 链表或红黑树;

7、1.8 table[i] 位置的链表什么时候会转变成红黑树;

MIN_TREEIFY_CAPACITY值是64,也就是当链表长度>8的时候(插入后再转),有两种情况:

  1. 如果table数组的长度<64,此时进行扩容操作;

  2. 如果table数组的长度>=64,此时进行链表转红黑树结构的操作.

8、HashMap 主要成员属性:threshold、loadFactor、HashMap 的懒加载;

  •  HashMap 是懒加载,第一次调用 HashMap 的 put 方法的时候 table 还没初始化,这个时候会执行 resize,进行table 数组的初始化
  • threshold:threshold 也是比较重要的一个属性:创建 HashMap 时,该变量的值是:初始容量(2 的整数次幂),之后 threshold的值是 HashMap 扩容的门限值,即当前 Nodetable 数组的长度 * loadfactor。
  • loadFactor:是空间和时间的一个平衡点。
        * DEFAULT_LOAD_FACTOR较小时,需要的空间较大,但是 put 和 get 的代价较小;
        * DEFAULT_LOAD_FACTOR较大时,需要的空间较小,但是 put 和 get 的代价较大;

9、HashMap 的 get 方法能否判断某个元素是否在 map 中;【不能:null】

因为HashMap是可以存放值为null的,你并不能分辨到底是 不存在返回null 还是本身值是null;

// 入口,返回对应的value
public V get(Object key) {
	Node<K,V> e;
		
	// hash函数上面分析过了
	return (e = getNode(hash(key), key))== null ? null : e.value;
}


public boolean containsKey(Object key) {
	// 注意与get函数区分,我们往map中put的所有的<key,value>都被封装在Node中,
	// 如果Node都不存在显然一定不包含对应的key
	return getNode(hash(key), key) != null;
}

10、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?

HashMap 在并发时可能出现的问题主要是两方面:

  • 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖

    • 1.7 (实际是Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e);)      大致意思就是e.next=table[i];table[i] =e; 

    • 同样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的数据也丢失。

11、HashMap 的 value 允许为 null,但是 HashTable 和 ConcurrentHashMap 的 value 都不允许为 null,试分析原因?

首先要明确 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,这时就没有上面的并发问题了。

12、HashMap 中的 hook 函数

三、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 总结篇

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())  实现。


四、对集合的选择

4.1  对 List 的选择

1、对于随机查询与迭代遍历操作,数组比所有的容器都要快。所以在随机访问中一般使用 ArrayList

2、LinkedList 使用双向链表对元素的增加和删除提供了非常好的支持,而 ArrayList 执行增加和删除元素需要进行元素位移。

3、对于 Vector 而已,我们一般都是避免使用。

4、将 ArrayList 当做首选,毕竟对于集合元素而已我们都是进行遍历,只有当程序的性能因为List的频繁插入和删除而降低时,再考虑 LinkedList。

4.2  对 Set 的选择

1、HashSet 由于使用 HashCode 实现,所以在某种程度上来说它的性能永远比 TreeSet 要好,尤其是进行增加和查找操作。

2、虽然 TreeSet 没有 HashSet 性能好,但是由于它可以维持元素的排序,所以它还是存在用武之地的。

4.3  对 Map 的选择

1、HashMap 与 HashSet 同样,支持快速查询。虽然 HashTable 速度的速度也不慢,但是在 HashMap 面前还是稍微慢了些,所以 HashMap 在查询方面可以取代 HashTable。

2、由于TreeMap 需要维持内部元素的顺序,所以它通常要比 HashMap 和 HashTable 慢。


五、Comparable 和 Comparator

实现 Comparable 接口可以让一个类的实例互相使用 compareTo 方法进行比较大小,可以自定义比较规则;

Comparator 则是一个通用的比较器,比较指定类型的两个元素之间的大小关系。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值