Java——集合
目录
1.Java中的集合类
2.List
2.1ArrayList和LinkedList有什么区别?
ArrayList和LinkedList的用途的不同?
ArrayList和LinkedList是否支持随机访问?
ArrayList和LinkedList内存占用有何不同?
链表和数组有什么区别?
2.2ArrayList的扩容机制了解吗?
2.3快速失败fail-fast了解吗?
2.3什么是安全失败(fail-safe)呢?
2.4有哪几种实现ArrayList线程安全的方法?
2.5ArrayList和Vector的区别?
2.6CopyOnWriteArrayList了解多少?
3.Map
3.1可以说一下HashMap的底层数据结构吗?
3.2你对红黑树了解多少?
为什么不用二叉树?
为什么不用平衡二叉树?
为什么用红黑树?
红黑树是怎么保持平衡的?
3.3HashMap的put流程知道吗?
只重写元素的equals方法没有重写hashCode,put的时候会发生什么?
3.4HashMap怎么查找元素的呢?
13.HashMap的hash函数是怎么设计的?
14.为什么hash函数能减少哈希冲突?
15.为什么HashMap的容量是2的幂次方?
16.如果初始化HashMap,传一个17的容量,它会怎么处理?
18.讲解哈希冲突有哪些方法?
20.HashMap扩容发生在什么时候呢?
21.HashMap的扩容机制了解吗?
22.JDK8对HashMap做了哪些优化呢?
23.你能自己设计实现一个HashMap吗?
1.Java中的集合类
Java中的集合类主要分为两大类:Collection和Map接口。前者是存储对象的集合类,后者存储的是键值对(key-value)。
Collection接口又分为List,Set和Queue接口。每个接口有其具体实现类。以下主要的集合类:
PS:可尝试结合类图,对常见的实现类关系重点记忆。
2.List
2.1ArrayList和LinkedList有什么区别?
ArrayList是基于数组实现的,LinkedList是基于链表实现的。
ArrayList和LinkedList的用途的不同?
ArrayList和LinkedList是否支持随机访问?
ArrayList和LinkedList内存占用有何不同?
链表和数组有什么区别?
2.2ArrayList的扩容机制了解吗?
超过当前数组,1.5倍
2.3快速失败fail-fast了解吗?
fail——fast是Java集合的一种错误检测机制。
在用迭代器遍历集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改,就会抛出Concurrent Modification Exception。
迭代器在遍历时直接访问集合中的内容。
并且在遍历过程中使用一个
modCount
变量。集合在被遍历期间如果内容发生变化,就会改变modCount
的值。每当迭代器使用hashNext()/next()
遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。异常的抛出条件是检测到
modCount!=expectedmodCount
这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如 ArrayList 类。
2.3什么是安全失败(fail-safe)呢?
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如 CopyOnWriteArrayList 类。
2.4有哪几种实现ArrayList线程安全的方法?
在 Java 中,
RandomAccess
是一个标记接口(Marker Interface),它本身不包含任何方法,仅用于标识实现该接口的集合类支持快速随机访问操作。常用的有两种
可以使用 Collections.synchronizedList() 方法,它可以返回一个线程安全的 List。
java

SynchronizedList list = Collections.synchronizedList(new ArrayList());
内部是通过 synchronized 关键字

加锁来实现的。
也可以直接使用 CopyOnWriteArrayList

,它是线程安全的 ArrayList,遵循写时复制的原则,每当对列表进行修改时,都会创建一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然在原有的列表上进行。
java

CopyOnWriteArrayList list = new CopyOnWriteArrayList();
通俗的讲,CopyOnWrite 就是当我们往一个容器添加元素的时候,不直接往容器中添加,而是先复制出一个新的容器,然后在新的容器里添加元素,添加完之后,再将原容器的引用指向新的容器。多个线程在读的时候,不需要加锁,因为当前容器不会添加任何元素。这样就实现了线程安全。
2.5ArrayList和Vector的区别?
2.6CopyOnWriteArrayList了解多少?
3.Map
3.1可以说一下HashMap的底层数据结构吗?
JDK8中HashMap的数据结构是数组+链表+红黑树。
数组用来存储键值对,每个键值对可以通过索引直接拿到,索引是通过对键的哈希值进行进一步的hash()处理得到的。
当多个键进过哈希处理后得到相同的索引时,需要通过链表来讲解哈希冲突——将具有相同索引的键值对通过链表存储起来。
不过链表过长时,查询效率会比较低,于是当链表的长度超过8时(且数组的长度大于64),链表就会转换为红黑树。红黑树的查询效率是O(logn),比链表的 O(n) 要快。
hash() 方法的目标是尽量减少哈希冲突,保证元素能够均匀地分布在数组的每个位置上。
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
如果键的哈希值已经在数组中存在,其对应的值将被新值覆盖。
HashMap 的初始容量是 16,随着元素的不断添加,HashMap 就需要进行扩容,阈值是
capacity * loadFactor
,capacity 为容量,loadFactor 为负载因子,默认为 0.75。扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中。
3.2你对红黑树了解多少?
为什么不用二叉树?
为什么不用平衡二叉树?
为什么用红黑树?
链表的查找时间复杂度n,红黑树是一种折中方案,查找,插入,删除的时间复杂度都是O(log n)
红黑树是怎么保持平衡的?
3.3HashMap的put流程知道吗?
在Java中,HashMap的put方法用于将键值对存储在哈希表中。以下是HashMap的put方法的详细流程:
1.计算hash值:
首先调用key的hashCode()方法获取key的hash码,然后通过HashMap内部的hash方法对哈希码进行扰动计算,以减少哈希冲突。
2.确定桶的位置:
根据计算得到的哈希值,通过(n - 1)&hash(n为哈希表的容量,是2的幂次方)来确定该键值对应该存储在哪个桶(数组的哪个位置)中。例如,如果哈希表容量
n = 16
,那么(16 - 1) & hash
相当于hash % 16
,但按位与操作效率更高。3.检查桶是否为空:
如果桶为空,直接在该位置创建一个新的节点(链表的头节点)并插入键值对。
if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);
4.处理桶不为空的情况:
- 检查是否为相同节点:如果桶不为空,首先检查桶的第一个节点(链表头节点)的键是否与要插入的键相同。如果相同,则直接更新值。
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;
处理链表或红黑树:如果桶中的节点不是要找的节点,且该桶是链表结构,则遍历链表查找是否有相同键的节点。如果找到,则更新值;如果遍历链表未找到,则在链表末尾插入新节点。
如果链表长度大于等于
TREEIFY_THRESHOLD
(默认值为 8)且哈希表容量大于等于MIN_TREEIFY_CAPACITY
(默认值为 64),链表会转换为红黑树结构,通过红黑树的插入操作来插入新节点。5.更新操作:
如果在上诉过程中找到键的节点,则更新该节点的值,并返回旧值。
if (e!= null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue; }
6.增加元素个数并检查扩容:
如果没有找到相同键的节点,说明是新插入的键值对,HashMap的元素个数size增加。然后检查是否需要扩容。如果size大于等于threshold(
threshold = loadFactor * capacity
,loadFactor
默认值为 0.75),则进行扩容操作。扩容会创建一个新的更大的哈希表,并将旧哈希表中的所有键值对重新计算哈希并插入到新哈希表中。++modCount; if (++size > threshold)resize(); afterNodeInsertion(evict); return null;
3.4HashMap怎么查找元素的呢?
通过哈希值定位索引——》定位桶——》检查第一个节点——》遍历链表或红黑树查找——》返回结果。
13.HashMap的hash函数是怎么设计的?
14.为什么hash函数能减少哈希冲突?
15.为什么HashMap的容量是2的幂次方?
HashMap采用2的n次方倍作为容量,主要是为了提高哈希值的分布均匀性和哈希计算的效率。
HashMap通过(n - 1)&hash来计算元素的存储索引位置,这种位运算只有在数组容量是2的n次方时才能确保索引均匀分布。位运算效率高于取模运算,提高哈希计算的速度。
HashMap扩容时,通过容量为2的n次方,扩容时只需要通过简单的位运算判断是否需要迁移,减少重新计算哈希值的开销,提升了rehash的效率。
16.如果初始化HashMap,传一个17的容量,它会怎么处理?
18.讲解哈希冲突有哪些方法?
再hash,开放地址法和拉链法
20.HashMap扩容发生在什么时候呢?
21.HashMap的扩容机制了解吗?
HashMap的扩容是负载因子来决定的,扩容为原来的两倍
rehashing:
jdk1.7之前
jdk1.8之后优化:
16:010000 32:100000
15:001111 31:011111
拿老数组长度判断高位是否是1,
ctrl + F12
属性
内部类
创建一个空参对象只会给负载因子赋值,底层数组没有赋值,添加第一个元素才会创建
源码讲解:
table 哈希表结构中数组的名字
Default_initial_capacity; 数组默认长度16;
DEFAULT_FACTOR; 默认加载因子0.75;
创建对象
添加元素
数组位置为null
数组位置不为null,键不重复挂在下面形成链表或红黑树
数组位置不为null,键重复,元素覆盖
22.JDK8对HashMap做了哪些优化呢?
1.红黑树,优化查询性能,避免链表过长
2.优化扰动函数,
3.优化扩容机制
4.头插法变为尾插法
23.你能自己设计实现一个HashMap吗?
24.HashMap是线程安全的吗?
25.怎么解决HashMap线程不安全的问题呢?
26.CopyOnWriteArrayList和Collections.synchronizedList有什么区别?分别有什么优缺点?