Java集合

117 阅读6分钟

大体结构图

list、set、map的特点

  • list:有序、可重复

  • LinkedList: 底层链表实现,增删快,查询慢,线程不安全,效率高。链表实现,无扩容

  • ArrayList:底层数组实现,增删慢,查询快,线程不安全,效率高。初始容量0,添加一个元素之后容量为10,之后每次扩容为原来的1.5倍

  • Vector:底层数组实现,增删慢,查询快,线程安全,效率低。初始容量10,每次扩容为原来的2倍

  • set:不可重复

  • HashSet:底层其实是添加在HashMap的key中,value为相同的一个静态Object类,允许一个空值

  • LinkedHashSet:有序,底层其实是添加在LinkedHashMap的key中

  • TreeSet:底层实现为二叉树,有序,不允许空值

  • map:Key-Value键值对 

  • HashMap:数组+链表+红黑树实现,线程不安全

  • TreeMap:红黑树实现,有序,非线程安全

  • LinkedHashMap:双向链表和哈希表实现,有序

  • HashTable:线程安全

  • ConcurrentHashMap:线程安全

HashMap(线程不安全)

1.HashMap底层原理

JDK1.8以前,HashMap底层实现由hash表(数组)+链表实现。当发生Hash冲突的时候采用链地址法(拉链法),在冲突的地方生成链表存储数据。负载因子默认0.75,每次扩容2倍。多线程使用因为链表采用头插法的方式存储,扩容的时候容易发生链表死循环,数据覆盖等问题。

JDK1.8后HashMap底层实现由hash表(数组)+链表+红黑树实现,产生Hash冲突的时候使用链地址法(拉链法)解决冲突,当链表的长度>=8且数组的长度>=64的时候转成红黑树,当红黑树的长度<=6的时候重新转回链表。1.8采用尾插法,解决了多线程死循环的问题,但是多线程的时候还是会产生数据覆盖的问题,所以还是线程不安全的

2.HashMap  put(key,value)、get(key)的过程(JDK1.8)

  • put

  • 通过计算key的hashcode & (length - 1) 得到桶的位置

  • 如果当前位置为空,创建Node存放进当前位置。

  • 如果不为空(hash冲突)

  • 如果当前位置为Node则调用equals方法判断是否是同一个对象,如果是则覆盖,如果不是则创建链表存入

  • 如果当前位置为链表则遍历链表判断是否链表中有相同的对象,有则覆盖。没有则采用尾插法插入链表中。插入成功后判断是否达到阈值(8),hashMap的总长度是否大于等于64,是的话链表转成红黑树

  • 如果当前位置为红黑树则调用红黑色的插入方法

  • 插入成功后判断是否组要扩容

  • get

  • 通过计算key的hashcode & (length - 1) 得到桶的位置

  • 如果当前位置为null,直接返回null

  • 通过equals方法判断所要获取的值

  • 如果当前位置是Node则直接返回当前位置的值

  • 如果是链表则用链表的方式查询

  • 如果是红黑树则用红黑树的方式查询

3.为什么1.8要引入红黑树

链表查询速度是On,红黑树的查询速度是O(logn)。当Hash冲突严重的时候红黑树可以大大加快查询的效率。

4.HashMap为什么是线程不安全的

在多线程中如果有线程A,计算某个Key的hash位置为A,此时A位置为空,是可以直接放置进去的。这时线程B获取到了CPU执行权,计算Key的hash位置也为A,A的位置为空,直接将value放置进A位置,线程B执行完毕。这时A获取到了执行权,因为之前判断A位置是可放入的,直接将value放置进A位置,就导致了B线程的数据被A线程给覆盖了。

5.为什么负载因子是0.75

这时时间与空间权衡取舍的结果,假设负载因为为0.5,更早的扩容可以减少hash冲入,但是必然导致过早的扩容浪费空间。假设负载因子为1.0,必须每个桶的位置都存满才进行扩容,势必导致hash冲突严重,曾删的速度必然大大降低。所以负载因为0.75是个权衡取舍的结果。

6.为什么HashMap的长度必须达到64才会加入红黑树

这是因为当HashMap长度小于64的时候,执行resize()的成本是比执行红黑树转化来得低的,红黑树转化的时候需要经过左旋、右旋、变色等操作,是比较耗性能的。所以当冲突不严重的时候使用链表,hashmap长度不长的时候直接resize的成本是比较低的

7.为什么hashMap扩容都是2的N次方

for (int j = 0; j < oldCap; ++j) {
    Node e = oldTab[j];
    newTab[e.hash & (newCap - 1)] = e;
}

因为hash算法计算位置都是使用hashCode & (length -1)计算桶的位置。

当length都为2的N次幂的时候比如16,N-1为15即二进制位1111,1 & 1 为1,当扩容为32的时候为11111

旧的hash值多一个高位进行位置计算,新进高位如果为0则方在原位置,新进高位如果为1则为原位置+原长度的位置,减少了hash值的重复计算,这种计算方法也可以大大减少Hash冲突的概率,使元素可以均匀的分布,大大提升效率。

8.HashMap与HashTable的区别

  • HashMap:线程不安全,效率高,初始化容量16,每次扩容2倍

  • HashTable:方法基本都由synchronized修饰,初始容量11,每次扩容2N+1,synchronized锁住整个hash表,效率极地,基本被废弃(ConcurrentHashMap代替)

ConcurrentHashMap

代替HashTable实现线程安全的Map集合。

HashTable的synchronized锁住的是整个hash表,效率极地。ConcurrentHashMap 1.8之前采用Segment分段锁技术,每个Segment相当与一个HashTable,每个Segment的锁相互独立,互不影响,Segment初始大小16,相对于HashTable效率提高了16倍。1.8版本采用数组+链表+红黑树的数据结构,线程安全采用CAS+Synchronized来实现,总体来说像是优化过的HashMap.

LinkedHashMap

基本 = HashMap+LinkedList

key和value都允许空,线程不安全,通过维护双向链表实现数据有序。支持LRU算法,最近访问的数据放在队尾,最少使用的在队头,删除的时候从队头删除。