首先,在研究HashMap的底层实现原理之前,我们先来看看equals和hashcode,hash碰撞这些问题。
疑问1:equals和 == 的区别?
(1)equals比较的时候,如果是引用类型(除了String类型),那么使用的是object中的equals方法,比较的是地址是否一样;
如果是String类型的对象,那么String类重写了Object中的equals方法,首先比较地址是否一样,一样的话返回true,不一样再比较是否是为同种类型,不是同类型就返回false,同类型之后再看内容是否一样;内容一样返回true,不一样返回false。
如果是基本数据类型对应的包装类,使用的equals也不是Object中的equals方法,而是重写了这个方法,首先比较的是否是同类型,不是同类型,直接返回false,是同类型再比较内容是否一致,一致返回true。
(2)==比较的是地址,引用类型(除了String类型)的时候,只要不是同一个对象那么不是一个地址,String类型的时候如果new出来两个对象,那么也是不同地址,如果不new,那么就是同一个地址,因为都是存放在字符串常量池中,如果是基本数据类型,都是放在常量池中,所以地址都是一样的。
疑问2:为什么重写equals还要重写hashcode?
Hashcode相等,内容不一定相等,但内容相等,hashcode一定相等。
定律:
如果两个对象的hashcode值相等的情况下,对象的内容值不一定相等(会发生hash碰撞的问题)
如果使用equals方法去比较两个对象内容值,相等的情况下,则两个对象的hashcode相同。
将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals()判断要放入对象与该存储区域的任意一个对象是否相等,如果equals()判断不相等,直接将该元素放入到集合中,否则不放入。
同样,在使用get()查询元素的时候,集合类也先调key.hashCode()算出数组下标,然后看equals()的结果,如果是true就是找到了,否则就是没找到。
疑问3:如何理解hashcode碰撞问题?
定义对象作为key的时候,一定要重写equals方法和hashcode方法,保证对象key不重复创建。
1、hashmap和hashtable的区别
hashmap线程不安全,允许存放key为空,放在数组下标为0的位置。
hashtable线程安全,不允许存放key为空,底层原理:先获取synchronize锁,如果下标一样,则覆盖。
2、hashmap底层如何降低hash冲突概率
通过二进制去计算hashcode,这样就可以保证均匀存放每个下标的位置,可以直接根据下标位置定位到该元素,这样时间复杂度就是O(1)。
3、hashmap的底层实现原理
(1)基于arraylist集合方式实现
优点:不需要考虑hash碰撞问题,因为它是从头查到尾。
缺点:效率比较低,从头查到尾,时间复杂度为o(n)。
(2)基于数组 + 链表方式(jdk1.7)
hashmap中key如果没有发生碰撞问题,get查询时间复杂度为O(1).
数组初始容量(默认是16),在0.75*16=12的时候进行扩容,之后扩容会按照1.5倍进行扩容,
比如存第17个数据时,数组长度变为16*1.5=24,
存第25个数据的时候,数组长度变为24*1.5=36
(3)基于数组 + 链表 + 红黑树(jdk1.8)
当数组容量 > 64 链表长度 > 8 时,转换成红黑树进行存储,
当红黑树节点个数小于6时,转换成链表进行存储。
4、hashmap1.7 和 hashmap1.8的区别
(1)hashmap1.7是基于数组 + 链表实现的(时间复杂度为O(n)),
hashmap1.8是基于数组 + 链表 + 红黑树实现的(时间复杂度为O(LogN))。
(2)hashmap1.7在进行扩容的时候,把老的链表存放到新的table中时,采用的是头插入法,源码的写法比较简单,在多线程的情况下,会造成死循环的现象。
hashmap1.8在进行扩容的时候,采用尾插法,写法比较高大上,使用"与运算",产生高位链表和低位链表,最终将两个链表放入到table中,将链表长度缩短,能够降低key对应的index发生冲突,能够提高链表的查询效率。
5、concurentHashmap的底层实现原理
1.7实现原理:
将一个大的ConcurrentHashmap集合拆分成n多个不同的小的hashtable(16个,Segment(底层采用lock锁)),在每个小的hashtable中都有自己独立的table数组,每次扩容的时候只会扩容自己独立的hashtable,扩容原理和hashmap是一样的。锁的实现采用的lock锁 + cas乐观锁 + UNSAFE类,似于 synchronized 锁的升级过程。支持多个Segment同时扩容
大致原理就是将一个大的 HashMap 分成 n 多个不同的小的 HashTable,通过不同的 key 计算 index, 如果没有发生冲突 则存放到不同的小的 HashTable 中 ,从而可以实现多线程,但是如果多个线程同时做 put 操作 key 发 生了 index 冲突,落到同一个小的 HashTable 中还是会发生竞争锁。
底层原理:
1、由多个不同的segment对象组成
2、锁:lock锁
3、unsafe查询主内中最新的数据
4、使用cas做修改
1.8实现原理
由数组 + 链表 + 红黑树组成的。取消了segments分段设计。
ConcurrentHashmap是对index下标对应的node节点进行上锁,多个线程同时put key的时候,如果多个key都落入到同一个index node节点的时候,则需要做锁的竞争。
使用cas锁:index没有发生冲突,多个线程同时赋值的时候。
使用synchronized锁:index已经发生冲突,使用synchronized锁对该node节点上锁。
为什么1.8要去掉Segment分段锁?
因为1.7需要计算两次index,第一次计算存放在哪个segment对象里,第二次计算在哪个HashEntry里,
而1.8只需要计算一次index值。
为什么1.8使用synchroized锁,而不用lock锁?
lock锁是不带自旋功能的,synchorized自1.6后就自带短暂的自旋功能。
6、hashmap8核心参数
加载因子(default_load_factor=0.75f)用于提前扩容
初始容量(default_initial_capacity=1最大限制容量Maximum_capacity=1
链表长度(Treeify_threshold=8),链表长度如果大于8,则转换成红黑树
红黑树(untreeify_threshold=6),红黑树长度如果小于6,则转换成链表结构
数组容量(min_treeify_capacity=64)
modCount:遍历hashmap集合时,防止多线程对数据集合进行篡改
7、arraylist源码解读
1)扩容和缩容,数组默认初始容量为10.,扩容是1.5倍。
2)存放元素是有序的,线程不安全,没加锁的机制
3)get方法(index下标查询)、add方法。
4)数据结构基于数组的形式实现,类型是object类型,默认容量
8、arraylist和vector的区别
相同点:都是基于数组实现,默认初始容量也是10。
不同点:arraylist线程不安全,vector加的是synchroized锁,线程安全。
arraylist扩容是原容量的1.5倍,vector扩容是原容量的2倍。
9、hashset底层原理
底层基于hashmap集合实现(key是存储的这个对象,value是个空object),存储无序的,
底层数据结构也是 数组 + 链表 + 红黑树
hashmap集合中key是否允许重复?
不允许,比较hash值,hash值相等,并且equals不等的情况下,才能把这个值存进去。
10、linkedlist 原理
底层基于链表数据结构实现,底层是双向链表,保证下标有序性,采用尾插法,查找是折半算法(时间复杂度:log(n))
数组和链表的区别:
数组:保证元素有序性,可以基于下标查询,时间复杂度为O(1),适合于基于下标查询的场景,增删会对数组实现移动,效率低。
链表:单向链表、双向链表,可以基于下标查询,时间复杂度为O(n),适合于增删,只需要改引用指针的关系,查询的时候都得从头到尾查询。