JAVA常见集合的底层理解

对一些集合的底层和扩展进行了简单理解和整理。

集合对比

Map接口和Collection接口是所有集合框架的父接口。Collection又包含了Set、List。Set不能包含重复的元素。List是一个有序集合,可以包含重复元素,提供按索引遍历的方式。Map不能包含重复的key,但可以包含相同的value。
在这里插入图片描述

ArrayList
底层实现方式是数组,默认大小是10个,当指定容量大小时用指定大小的容量;当数组满了以1.5倍扩容,特点是查询快,增删改慢。
LinkedList
底层实现方式是双向链表,特点是查询慢,增删改快。
HashMap
底层实现是数组+链表+红黑树,默认大小16,当指定容量大小时,用大于该容量的最小的2的幂次方作为初始容量;当table使用了容量的0.75倍时扩容扩容为2倍,当table容量超过64链表数量大于8时,链表会树化。线程不安全。
HashSet
对HashMap进行了封装,无序且不能重复,其他与HashMap一致。线程不安全。
HashTable
默认初始大小为11,指定大小时直接使用指定大小的容量;当使用了容量的0.75倍时扩容,扩容为2*n+1(n为扩容前容量)。不会树化。线程安全。
TreeMap
底层是红黑树,一种近似平衡的二叉查找树。按key的大小对元素进行排序。线程不安全。
TreeSet
对TreeMap进行了封装。
ConcurrentHashMap
1.7底层是Segment数组+HashEntry数组+链表,每个Segment继承了ReentrantLock。
1.8底层是Node数组+链表+红黑树,使用了CAS和Synchronized保证并发安全。

详细介绍

ArrayList
是顺序容器,底层通过数组实现。这里的数组是Object数组,可以容纳任何类型的对象,允许是null。
扩容机制

  1. ArrayList中维护了一个Object类型的数组elementData. transient Object[] elementData;
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第一次添加,则扩容elementData为10,如果再次扩容,则扩容elementData为1.5倍
  3. 如果使用的是指定大小的构造器,则初始化elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

LinkedList
LinkedList同时实现了List接口和Deque接口,既可以看作顺序容器,也可以看作队列,同时也可以看作一个栈。没有实现同步(synchronized)
双向链表的数据结构。first和last引用分别指向链表的第一个和最后一个元素。没有哑元,当链表为空的时候first和last都指向null

HashMap
数组+链表+红黑树

两个影响HashMap性能的参数:初始容量initial capacity(指定初始table的大小)和负载系数load factor(指定自动扩容的临界值capactiy * load factor)。当entry的数量超过临界值时,容器自动扩容并重新哈希。负载系数默认为0.75,这是在时间和空间的权衡,如果为1则会导致太多的哈希冲突,底层红黑树变得异常复杂,不利于查询,降低了时间效率,如果为0.5又会浪费内存空间,原本存储1M的数据现在需要2M,降低了空间利用率。

两个特别注意的方法hashCode()和equals()。hashCode()决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()决定了这些对象是否是“同一个对象”。所以如果要将自定义对象放入,需要重载这两个方法。只重写hashCode无法保证两个对象的属性是否相同。

  1. 我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中
    的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果
    key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,进一步判断key是否相同,从而找到对应值。
    根据对冲突的处理方式不同,哈希表有两种实现方式:开放地址形式和冲突链表方式。
    在这里插入图片描述

扩容机制
只有在第一次添加数据的时候,HashMap的容量才会确定下来。
如果没指定初始容量大小,HashMap的初始容量会初始为16。
如果指定了初始容量大小,HashMap会将其扩充为2的幂次方大小。指定为2的幂次方大小是因为为了让计算索引时hash(k)&(table.length - 1)与hash(k)%table.length相同,低位都为1,把哈希值的高位都抹掉了,即用位运算替代取模运算。

  1. 添加一个元素时,先得到hash值,然后转换成索引值
  2. 找到存储数据表table,看索引值位置是否有元素,如果没有则直接加入
  3. 如果索引位置有元素,则会调用equals方法进行比较,如果相同就放弃添加,不相同就添加到最后
  4. 如果一条链表的元素个数达到8,并且table的大小>=64,就会进行树化(红黑树),如果table不满64会先扩table。
  5. table的数组扩容:第一次添加时,table数组扩容到16,临界值是160.75=12,如果table数组使用了临界值12,就会扩容到162=32,当table使用了新的临界值32*0.75=24,就会继续扩容,以此类推

HashSet
HashSet实现了Set接口;对HashMap进行了封装;可以存放null,但只能有一个null;不保证元素是有序的,取决于hash后,再确定索引的结果;不能有重复元素。其余与HashMap相同。

Hashtable

  1. 存放的元素是键值对:K-V
  2. 键和值都不能为null
  3. 使用方法和HashMap一样
  4. Hashtable是线程安全的(synchronized),HashMap不是

扩容机制

  1. 不指定初始容量,初始容量大小为11,到达临界值为容量*0.75时,扩容为2n+1。
  2. 如果指定初始容量,直接使用给定容量大小。
  3. 不会进行树化。
  4. 采用头插法迁移数据。

TreeMap
底层通过红黑树实现,会按照key的大小顺序对Map中的元素进行排序。非同步。

TreeSet
对TreeMap进行了封装

ConcurrentHashMap
实现了线程安全。读操作和写操作都能有很高的性能,读操作几乎不需要加锁,写操作通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。到1.8的时候改成了CAS和Synchronized。
1.7
底层数据结构: Segments数组+HashEntry数组+链表。
将数组分成n个Segment,每个Segment里存放的是由HashEntry组成的数组,HashEntry用于存储键值对数据。每个HashEntry之间形成冲突链表。
Segment继承了ReentrantLock。当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment数组元素的锁。每次加锁的操作锁住的是一个Segment,保证了每个Segment是线程安全的。
无参构造默认有16个Segment,传入参数Segment数量为大于参数的最小的2的幂次数。Segment不可以扩容,Segment[i]可以扩容。
Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容。
在这里插入图片描述

1.8底层数据结构: Node数组+链表+红黑树,和HashMap一样,但采用CAS(Compare-and-Swap,比较并替换)和Synchronized来保证并发安全。Synchronized进行了锁升级,所以性能没有问题。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值