8.复习-Java集合

8.复习-Java集合
咸鱼Java集合
1、常见的集合类
java中提供了大量的集合类,主要分为两类:
1、Collection 属于单列集合 有两个接口List 和 Set
List常见的:ArrayList 和 LinkedList
Set常见的:HashSet(无序) 和 TreeSet(需要排序)
2、Map 属于双列集合
常见的有HashMap 、 TreeMap
线程安全的有:ConcurrentHashMap
2、ArrayList
- 底层数据结构
ArrayList底层是用动态的数组实现的
- 初始容量
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- 扩容逻辑
ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
添加逻辑
确保数组已使用长度(size)加1之后足够存下下一个数据
计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
返回添加成功布尔值。
ArrayList 与 LinkedList的区别
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList效率优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。
从内存空间占用来说
ArrayList底层是数组,内存连续,节省内存
LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
从线程安全来说,ArrayList和LinkedList都不是线程安全的
解决ArrayList和LinkedList都不是线程安全的问题:
第一:我们使用这个集合,优先在方法内使用,定义为局部变量,这样的话,就不会出现线程安全问题。
第二:如果非要在成员变量中使用的话,可以使用线程安全的集合来替代
ArrayList可以通过Collections 的 synchronizedList 方法将 ArrayList 转换成线程安全的容器后再使用。
LinkedList 换成ConcurrentLinkedQueue来使用
3、HashMap
HashMap的实现原理
1、底层使用hash表数据结构,体现为数组+(链表|红黑树)
2、添加数据时,先计算key的值确定元素在数组的下标
然后key如果相同则替换
不同则存入链表或红黑树中
3、获取数据通过key的hash计算数组下标获得元素
HashMap的jdk1.7和1.8的区别
- jdk1.8之前采用拉链法,即数组 + 链表
- jdk1.8之后采用数组 + (链表 | 红黑树),当链表长度大于8且数组长度大于64则会从链表转化为红黑树
HashMap的put方法具体流程
1、判断键值对数组 table 是否为空或者为null,是则执行初始化resize()进行扩容
2、根据键值key计算hash值得到数组索引
3、判断table[i] == null ,成立则直接新建节点添加
4、如果table[i] == null 不成立
4.1、判断table[i]的首个元素是否和key相同,相同则覆盖
4.2、判断table[i]是否为treeNode,即判断是否为红黑树,是则直接在树中插入键值对
4.3、遍历table[i],key相同则覆盖,最终没有匹配则插入到尾部,然后判断链表长度是否大于8,大于8则将链表转换为红黑树。
5、插入成功后,判断实际存在的键值对数量是否超过了最大容量threshold(数组长度*0.75),如果超过就进行扩容
源码:
1 | public V put(K key, V value) { |
HashMap的扩容机制
- 在添加元素或初始化的时候需要调用resize()进行扩容,第一次添加数据初始化数组长度为16,之后每次触发扩容都是达到了扩容阈值,即数组长度*0.75
- 每次扩容都是之前容量的2倍
- 扩容之后会新建一个数组,需要把老数组中的数据挪动到新数组中
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树就走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断 e.hash & oldCap 是否为0,为0则该元素位置停留在原始位置,不为0则移动到原始位置 + oldCap的位置上
- 源码:
1 | //扩容、初始化数组 |
HashMap的寻址算法
Hash方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位异或(^)运算得到最后的hash值
1 | (h = key.hashCode()) ^ (h >>> 16) |
在putValue方法中,计算数组下标的时候使用hash值和数组长度取模得到存储数据下标的位置,其中hashmap为了性能并没有采取取模的方式,而是使用了数组长度-1 与hash值进行按位与(&)运算,最后得到数组的位置
HashMap的数组长度为什么是2的次幂?
1、计算索引时效率更高:2的n次幂可以使用位与运算代替取模( 体现index = hash & (N - 1)而不是使用% )
2、扩容时重新计算索引效率更高:在进行扩容时需要计算 e.hash & oldCap 是否为0 ,为0则该元素位置停留在原始位置,不为0则移动到原始位置 + oldCap 的位置上
关于HashMap在jdk1.7情况下的多线程死循环问题
jdk1.7的数据结构为 数组+链表
在数组进行扩容的时候因为链表是头插法,在数据迁移的过程中可能导致死循环
例如:
链表中存在A、B两个节点 A->B (A.next = B)
线程1和线程2的变量e和next都引用了这两个节点
1、线程2先根据头插法完成了挪动 B->A,线程2结束 (B.next = A)
2、由于头插法,链表顺序颠倒,但是线程1的临时变量e和next还引用了这两个节点
3、线程1也按照头插法进行挪动,先A后B,因为线程二中 B.next = A ,所以线程1又插入了一次A,此时A.next = B ,这时就出现了A ->B -> A的循环
HashMap线程安全相关
HashMap并不是线程安全的
为了使用线程安全的map,可以采用ConcurrentHashMap(相关文章见Java多线程)
HashSet与HashMap的区别?
- HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法.
- 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象.
- 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
HashTable与HashMap的区别
第一,数据结构不一样,hashtable是数组+链表,hashmap在1.8之后改为了数组+链表+红黑树
第二,hashtable存储数据的时候都不能为null,而hashmap是可以的
第三,hash算法不同,hashtable是用本地修饰的hashcode值,而hashmap经常了二次hash
1 | (h = key.hashCode()) ^ (h >>> 16) |
第四,扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻倍
第五,hashtable是线程安全的,操作数据的时候加了锁synchronized,hashmap不是线程安全的,效率更高一些
在实际开中不建议使用HashTable,在多线程环境下可以使用ConcurrentHashMap类