HashMap ArrayList 等一些常用集合总结

img

一、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,则为链表。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值