一、List
1、ArrayList
(1)底层数据结构
底层数据结构为数组。数组是一种用连续的内存空间存储相同数据类型数据的线性数据结构。
Q:为什么数组索引下标从0开始?
A:从0开始,对应寻址公式:a[i] = baseAddress + i * dataTypeSize;
如果从1开始,则变为:a[i] = baseAddress + (i-1)* dataTypeSize;需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高。
其中,baseAddress: 数组的首地址,dataTypeSize:代表数组中元素类型的大小,int型的数据,dataTypeSize=4个字节
(2)实现原理
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10;
ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数。
【添加逻辑】
- 添加过程确保数组已使用长度(size)加1之后足够存下下一个数据 ;
- 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
- 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
(3)数组和List转换
- 数组转List ,使用JDK中java.util.Arrays工具类的 asList 方法;
- lList转数组,使用 List 的 toArray 方法。无参 toArray 方法返回 Objec t数组,传入初始化长度的数组对象,返回该对象数组。
Q:① 用Arrays.asList转List后,如果修改了数组内容,list受影响吗;② List用toArray转数组后,如果修改了List内容,数组受影响吗
A:
- Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址;
- list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。
2、LinkedList
LinkedList 是双向链表的数据结构实现。
3、线程安全的List
<Vector、synchronizedList、CopyOnWriteArrayList>
(1)Vector
- 使用synchronized修饰主要的方法;
- 对整个list对象加锁。
(2)synchronizedList
通过Collections的静态方法创建。Collections.synchronizedList(List<T> list)
- 使用synchronized修饰代码块;
- 读写操作都会加锁。
(3)CopyOnWriteArrayList
读读操作及读写操作均不互斥,读操作不加锁,写操作(set、add、remove等)使用ReentrantLock加锁。
- 列表内的数组“array”使用volatile修饰,保证不同线程间的可见性;
- 写操作创建新的“数组”复制旧“数组”的数据,在新数组上进行修改,修改后调用“setArray”方法,将列表引用的数组指向到新数组;
- 写操作加了“独占锁”,写操作本身不会出现线程安全问题。
【总结】:
- 线程安全的List可以通过Vector、Collections.synchronizedList()方法、CopyOnWriteArrayList三种方式实现;
- 读多写少的情况下,推荐使用CopyOnWriteArrayList;
- 读少写多的情况下,推荐使用Collections.synchronizedList()。
二、Map
1、HashMap
散列表(Hash Table)又名哈希表/Hash表,是根据键(Key)直接访问在内存存储位置的值(Value)的数据结构,由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性。
将键(key)映射为数组下标的函数叫做散列函数。可以表示为:hashValue = hash(key)
散列函数的基本要求:
- 散列函数计算得到的散列值必须是大于等于0的正整数,因为hashValue需要作为数组的下标;
- 如果key1==key2,那么经过hash后得到的哈希值也必相同即:hash(key1) == hash(key2);
- 如果key1 != key2,那么经过hash后得到的哈希值也必不相同即:hash(key1) != hash(key2);
散列冲突,不同的key经过hash运算得到相同的值。
散列冲突的解决,拉链法:
在散列表中,数组的每个下标位置我们可以称之为 桶(bucket)或者 槽(slot),每个桶(槽)会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
(1)实现原理
数据结构:数组+链表+红黑树
位置:根据key的hash值确定在数组中的位置,方法“hash(key) & (n-1)”,其中 n 为数组长度。
链表与红黑树转换:链表长度到 8,且数组长度达到64,转为红黑树;减少到 6,转为链表。
1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
2. 存储时,如果出现hash值相同的key,此时有两种情况。
a. 如果key相同,则覆盖原始值;
b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
(2)put方法执行流程
- 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化);
- 根据键值key计算hash值得到数组索引((n-1) & hash);
- 如果 table[i]==null,直接新建节点添加;
- 如果 table[i]!=null,
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value;
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对;
- 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value;
- 插入成功后,判断实际存在的键值对数量size是否超过了最大容量 threshold(数组长度*0.75),如果超过,进行扩容。
注:扩容因子为0.75,即数组中存储数据量达到总容量的0.75出发扩容。
(3)扩容机制
- 在添加元素或初始化的时候会调用resize方法进行扩容,首次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75);
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中;
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置;
- 如果是红黑树,走红黑树的添加;
- 如果是链表,则需要遍历链表,可能需要拆分链表。判断(e.hash & oldCap)是否为0,为0则停留在原始下标位置,不为0则移动到 原始位置+旧数组大小 这个位置上。
Q:为何HashMap的数组长度一定是2的次幂(2倍扩容)?
A:
(1)计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模;
(2)扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
注:oldCap表示旧数组长度。
Q:hashMap的寻址算法?
A:
(1)计算对象的 hashCode();
(2)再进行调用 hash() 方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更为均匀;
(3)最后 (capacity – 1) & hash 得到索引。
(3)jdk1.7多线程情况下死循环问题
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环。
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。
2、hashTable 与 hashMap 区别
1)继承的父类
HashMap继承自AbstractMap;HashTable继承自Dictionary类,该类已经被标记为废弃。
2)线程安全
hashTable线程安全,hashMap线程不安全;
3)解决hash冲突的方式
- hashMap在1.7及之前,使用链表;1.8开始,在链表长度到 8 且数组长度到 64,转为红黑树;节点数减少到 6,转回链表;
- hashTable使用链表。
4)扩容方式
hashMap 初始大小为16,2倍扩容;
hashTable 初始大小为11,2n+1;
5)是否允许null值;
HashMap键和值都运行为null;HashTable都不允许为null,否则会抛出空指针异常。
参考:
https://www.bilibili.com/video/BV1yT411H7YK;
https://zhuanlan.zhihu.com/p/646536067 ;