一、hashMap底层
底层使用哈希表(数组+链表),当链表过长会将链表转成红黑树以实现O(logn)时间复杂度的查找
二、hash函数是怎么实现的?
源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.高16位不变,低16位与高16位做一个异或
2.然后再通过h & (table.length -1)来得到该对象在数据中保存的位置。
三、hash冲突的解决方式
开放定址法:开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址相同的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
再哈希法:当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到没有冲突为止。
建立公共溢出区:将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
四、HashMap的put过程
1.先对key求hash值,然后计算下标。
2.如果没有碰撞,直接放入桶中
3.如果碰撞了,先判断当前的key与旧的key是否相等,如果相等则进行替换旧值,否则判断是否是红黑树,若是,则对红黑树进行操作,否则对链表进行操作,操作完后,若链表长度不小于8,则将链表转为红黑树
4.判断新的key与红黑树或者链表中的节点key是否相等,如果相等找出相等的节点,进行替换操作
5.最后如果桶满了就需要调用resize方法进行扩容
// 参数onlyIfAbsent表示是否替换原值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 空表,需要初始化
if ((tab = table) == null || (n = tab.length) == 0)
// resize()不仅用来调整大小,还用来进行初始化配置
n = (tab = resize()).length;
// (n - 1) & hash这种方式也熟悉了吧?都在分析ArrayDeque中有体现
//这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
if ((p = tab[i = (n - 1) & hash]) == null)
// 将元素直接插进去
tab[i] = newNode(hash, key, value, null);
else {
//这时就需要链表或红黑树了
// e是用来查看是不是待插入的元素已经有了,有就替换
Node<K,V> e; K k;
// p是存储在当前位置的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //要插入的元素就是p,这说明目的是修改值
// p是一个树节点
else if (p instanceof TreeNode)
// 把节点添加到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 这时候就是链表结构了,要把待插入元素挂在链尾
for (int binCount = 0; ; ++binCount) {
//向后循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表比较长,需要树化,
// 由于初始即为p.next,所以当插入第8个元素才会树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了对应元素,就可以停止了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 继续向后
p = e;
}
}
// e就是被替换出来的元素,这时候就是修改元素值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 默认为空实现,允许我们修改完成后做一些操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size太大,达到了capacity的0.75,需要扩容
if (++size > threshold)
resize();
// 默认也是空实现,允许我们插入完成后做一些操作
afterNodeInsertion(evict);
return null;
}
五、如何保证数组大小是2的幂次方
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
该方法将一个二进制数第一位1后边的数字全部变成1,然后再加1,这样这个二进制数就一定是100…这样的形式。
五.什么时候需要进行扩容,扩容resize()又是如何实现的?
调用场景:
1.初始化数组table
2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中
capacity负载因子
实现过程:(细讲)
1.通过判断旧数组的容量是否大于0来判断数组是否初始化过
否:进行初始化
- 判断是否调用无参构造器,
- 是:使用默认的大小和阙值
- 否:使用构造函数中初始化的容量,当然这个容量是经过tableSizefor计算后的2的次幂数
是,进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中
概括的讲:扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。
PS:可见底层数据结构用到了数组,到最后会因为容量问题都需要进行扩容操作
六、hashMap的get是如何实现
对key进行hash运算,然后与长度-1,进行与运算,计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。
七.为什么不直接将key作为哈希值而是与高16位做异或运算?
因为数组位置的确定用的是与运算,仅仅最后几位有效,设计者将key的哈希值与高16为做异或运算使得在做与运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。
八、传统hashMap的缺点(为什么引入红黑树?)
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
九、LinkedHashMap
LinkedHashMap的特点:
LinkedHashMap 继承HashMap,实现Map接口
1.LinkedHashMap集合底层是哈希表+链表(保证迭代的顺序)
2.linkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的
十、HashSet
无序不重复的集合
# HashSet内部其实是一个HashMap
public HashSet() {
map = new HashMap<>();
}
可见HashSet的add方法,插入的值会作为HashMap的key,所以是HashMap保证了不重复。map的put方法新增一个原来不存在的值会返回null,如果原来存在的话会返回原来存在的值
十一、HashTable
与HashMap的差别
HashTable是不允许Key和value 为null的。
HashTable是线程安全的,HashMap不是线程安全的 map可以使用Collections.synchronizedMap方法
HashTable继承了DIctionary抽象类,HashMap继承了AbstractMap抽象类
十二、ArrayList
1.扩容情况
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容是对原有大小进行一个1.5倍的扩充。
然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
数组采用Arrays.copyOf进行复制,底层调用System.arraycopy
容量拓展,是创建一个新的数组,然后将旧数组上的数组copy到新数组,这是一个很大的消耗,所以在我们使用ArrayList时,最好能预计数据的大小,在第一次创建时就申请足够大小
2.add添加元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
在添加元素是,会先进行判断当前容量是否已经满了,如果满了需要进行扩容
十三、ConcurrentHashMap
1、spread 计算hash值
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
高16位与低16位做一个异或后,在于HASH_BITS进行一个与运算
2、put过程
第一次插入时初始化数组table(为volatile修饰),其大小默认为16。插入元素时先获得键的hash值,然后找到数组索引插入,插入时先看数组是否为空。
为空:初始化table:先看sizeCtl是否为小于0,为-1则其他线程占有锁。不为则初始化其为-1,然后默认数组大小为16。sizeCtl为16*0.75。
然后判断数组第一个位置是否为空,
为空,采用casTab()插入。
不为空,判断其hash是否为-1。
为-1,判断是否在扩容,不是扩容直接返回table。
不为空:
加锁:synchronized (f)
看是为链表还是红黑树,然后插入。头结点的hash>0,则为链表。