面试题开路
面试的时候,经常会被问:
- List、Set、Map、底层用什么实现的?有哪些典型实现?
- ArrayList、Linkedlist、Hashset区别?
- HashMap、Hashtable、ConcurrentHashMap实现原理?
要解决这个问题,一定要回到原理开始说起,说清楚的结构也很简单:
- 数据结构
- “增删改查”效率,这个主要包含“随机访问效率” + “增删改效率”2个指标来看;
- 内存空间占用
- 线程安全
最后还要来一个“总结”
List、Set、Map的关系
抛开其他的因素,看表中的关系,就知道!!
开始分析,这里如果有表格就更好了
ArrayList
- 数据结构:动态数组,有序
- “增删改查”效率:因为是数组(有下标),所以查找快。 增删很慢,动一个会带来元素移动(其他的元素要改下标)。插入时扩容还需要复制数组;
- 内存空间:低
- 线程安全:不保证线程安全;
LinkedList
- 数据结构:双向链表,有序
- “增删改查”效率:因为是链表,增删快,只需要改指前和指后的指针就行了,但查找很慢,需要通过指针一个个地循环;
- 内存空间:比ArrayList大,因为有item+双指针
- 线程安全:不保证线程安全;
HashMap
- 数据结构:1.8之后是数组+链表+红黑树,无序
- “增删改查”效率:相对很巴适,一直在优化
- 内存空间:缝合怪,这还用说
- 线程安全:不保证线程安全
这里有好几个考点
-
HashMap的底层原理
-
HashMap的putVal实现
-
HashMap的get实现
- 通过key的hash值找到该key映射到的桶
- 如果该桶上首个元素first的key就是要查找的key,即直接命中,则直接返回
- 如果该桶上的首个元素first的key不是要查找的key,则需要查看后续节点
- 如果first属于树节点,则该桶位已经升级成了红黑树,将从红黑树中找我们需要的key
- 否则,后续节点就是链表形式,通过遍历链表来寻找该key 这里感兴趣可以去看源码
- HashMap是如何解决Hash冲突的
- 什么是hashCode
Object类中有一个方法
public native int hashCode();(用native修饰,是一个本地方法,通常用c或c++写成,在java中可以去调用它)
调用这个方法会生成一个int型的整数,和调用它的对象地址和内容有关。把任意长度 的二进制映射为固定长度较小的二进制值。
- 为什么扩容是2倍? HashMap两个重要的参数:初始容量大小和加载因子,初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到一个值:12。内容超过12就会进行扩容。
一旦数组中存储的元素个数超过该值就会调用rehash方法将扩容到原来的2倍。扩容会生成一个新数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能。
先看一下HashMap中的putVal方法(存值的)和resize方法(扩容的),之所以HashMap扩容是2的n次幂和这两个方法有千丝万缕的联系。 putVal方法可以看出来HashMap在存值时会先把key的hash值和扩容后的长度进行一次按位与运算; resize方法可以看出来扩容时会新建一个tab,然后遍历旧的tab,将旧的元素进行e.hash & (newCap - 1)的计算添加进新的tab中,也就是(n - 1) & hash的计算方法; HashMap以2倍扩容,目的就是减少hash碰撞,使元素分配均匀。
ConcurrentHashMap
- 线程安全:保证线程安全 其他跟HashMap一样
这里的考点
- ConcurrentHashMap的底层原理
JDK1.7
先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
JDK1.8(TreeBin: 红黑二叉树节点 Node: 链表节点)
采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,只有hash冲突的时候,才会占用锁。这样只要hash不冲突,就不会产生并发,效率又提升N倍。(这里看不懂的话 前面看HashMap是如何解决Hash冲突的)
HashSet
- 数据结构:HashSet 基于 HashMap 来实现的,不允许有重复元素的集合
- “增删改查”效率:
- 内存空间:
- 线程安全:不保证线程安全
HashTable
- 数据结构:基于 HashMap 实现,基本已被淘汰
- 线程安全:线程安全 单线程转为使用HashMap,多线程使用ConcurrentHashMap。