集合框架
Collection 单列集合
List 有序,可重复
Vector 数组结构,线程安全
ArrayList 数组结构,非线程安全
LinkedList 链表结构,非线程安全
Set 无序,唯一
HashSet 哈希表结构体
LinkedHashSet 哈希表和链表结构
TreeSet 红黑树结构
Map 双列集合
HashTable 哈希表结构,线程安全
HashMap 哈希表结构,非线程安全
ConcurrentHashMap 哈希表结构,线程安全
TreeMap 红黑树结构
ArrayList底层
数据结构-数组
数组(Array)是一种用连续的内存空间存储相同数据类型数据的线性数据结构。
数组下标为什么从0开始
寻址公式是:baseAddress+ i * dataTypeSize,计算下标的内存地址效率较高。
查找的时间复杂度
随机(通过下标)查询的时间复杂度是O(1)。
查找元素(未知下标)的时间复杂度是O(n)。
查找元素(未知下标但排序)通过二分查找的时间复杂度是O(logn)。
插入和删除时间复杂度
插入和删除的时候,为了保证数组的内存连续性,需要挪动数组元素,平均时间复杂度为O(n)。
ArrayList源码分析

无参构造函数,默认创建空集合,添加元素时才初始化数组长度为默认值10。

ArrayList底层的实现原理是什么
底层数据结构
ArrayList底层是用动态的数组实现的。
初始容量
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10。
扩容逻辑
ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。
添加逻辑
确保数组已使用长度(size)加1之后足够存下下一个数据。
计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)。
确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
返回添加成功布尔值。
如何实现数组和List之间的转换
数组转List ,使用JDK中java.util.Arrays工具类的asList方法。Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。
List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组。list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。
数据结构-链表
单向链表
链表中的每一个元素称之为结点(Node)。
物理存储单元上,非连续、非顺序的存储结构。
每个结点包括两个部分,一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。记录下个结点地址的指针叫做后继指针next。
双向链表
顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
ArrayList和LinkedList的区别是什么?
底层数据结构
ArrayList底层数据结构是动态数组,LinkedList底层数据结构是双向链表。
操作数据效率
ArrayList按照下标查询的时间复杂度O(1),LinkedList不支持下标查询。
查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)。
新增和删除:
ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)。
LinkedList头尾结点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)。
内存空间占用
ArrayList底层是数组,内存连续,节省内存。
LinkedList底层是双向链表,需要存储数据,和两个指针,更占用内存。
线程安全
ArrayList和LinkedList都不是线程安全的。
如果需要保证线程安全,有两种方案:
在方法内使用,局部变量则是线程安全的。
使用线程安全的ArrayList和LinkedList。
HashMap底层
JDK1.8之前,数组+链表;
JDK1.8之后,数组+链表+红黑树,用于存储键值对。
哈希计算
插入或查询时,先调用key.hashCode()获取原始哈希值,这个哈希值在源码里被赋值给了一个变量h。
扰动处理
扰动处理公式:h ^ (h>>>16),将高位信息混入低位,使哈希值在低位也均匀分布,减少碰撞。
索引定位
使用 (n - 1) & hash 计算bucket下标,效率比取模%更高。
冲突处理
1. 如果桶位置为空,直接放入新节点。
2. 如果存在节点,则逐个比较:若equals()相等则覆盖value,否则追加到链表尾部。
3. 当单个桶中的节点数>8且数组容量>=64时,链表会转成红黑树以降低最坏复杂度O(logn)。
扩容机制
1. 默认初始容量为16,负载因子为0.75。
2. 当元素超过阈值,数组扩容2倍,并重新分配节点位置。
3. 扩容后的新下标计算不是重新算hashCode,而是用(node.hash & oldCap)是否等于0判断要留在原位置还是移动到(原位置+oldCap)。等于0则放到newTable[i],否则放到newTable[i+oldCap]。
底层拆解公式
HashMap索引公式
index = hash & (capacity - 1)
目的:保留正好的hash的低n位,刚好足够表示容量范围内的索引下标。
假设扩容前 扩容后:
oldCap = 16(二进制10000) newCap = 32(二进制100000)
oldCap - 1 = 15(二进制01111) newCap - 1 = 31(二进制011111)
所以 &(capcity - 1)的作用就是:只取hash的低几位来当索引。(capcity为16的时候取低4位,capcity为32的时候取低5位)。
HashMap扩容公式
(node.hash & oldCap == 0) ? 原位置 : 新位置
扩容后多看了1位,HashMap依照这一位的值,决定了节点是留在原桶,还是去新桶。
这样就不用重新算hashCode,也不用重新做%运算,只看多出来的那一位就能快速确定新位置。
为什么数组长度必须是2的n次幂
1. 计算索引时效率更高,如果是2的n次幂可以使用按位与代替取模。
2. 扩容时重新计算索引效率更高,(node.hash & oldCap == 0) ? 原位置 : 新位置。
HashMap在JDK1.7情况下的多线程死循环问题
JDK1.7时的数据结构:数组+链表。
链表是头插法,在多线程场景下,数组扩容时可能导致死循环。


开始时T1和T2都指向A,T1.next和T2.next都指向B,线程T1执行之后,链表中的节点顺序发生了变化,但是线程T2指向的节点引用并未改变,此时发生死循环。T1执行完的顺序是B->A,T2执行的顺序是A->B,这样A节点和B节点就形成了死循环。
解决方案
1. 使用线程安全的ConcurrentHashMap替代HashMap,推荐。
2. 使用线程安全的HashTable替代HashMap,性能低,不推荐。
3. 使用synchronized或reentrantLock加锁,会影响性能,不推荐。
Java集合核心原理详解

被折叠的 条评论
为什么被折叠?



