【Java八股面试系列】Arraylist和HashMap的底层原理

本文详细分析了ArrayList的动态扩容机制,HashMap的构造方法、putVal和resize过程,以及为何重写hashCode和equals。讨论了线程安全问题,以及ConcurrentHashMap如何处理并发插入。重点强调了equals和hashCode一致性的重要性。
摘要由CSDN通过智能技术生成

ArrayList源码

总:

**ArrayList**底层是使用名为 **elementDataObject动态数组进行实现,与Java中的数组相比,她的容量能够动态的进行增长。在我们更新元素的时候,我们会通过ensureCapacityInternal()方法来确保我们的容量够用,如果容量不够则调用grow()**方法对我们的数组进行扩容为1.5倍,然后将我们原来的的数组复制过去。

ArrayList 和 Vector 的区别?

  • ArrayListList 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 。
  • VectorList 的古老实现类,底层使用Object[] 存储,线程安全。

ArrayList 可以添加 null 值吗?

ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。

Arraylist 与 LinkedList 区别?

Arraylist 和 LinkedList都是实现了 List接口,都是线程不安全的。

不同点:

  • Arraylist底层是通过object数组进行实现,而LinkedList底层是通过双向链表进行实现
  • AL实现了随机读取接口,能够随机进行读取,用来查询的效率高,LL不能进行随机读取,只能遍历进行读取,但是她的插入效率高
  • 内存空间的占用上,AL的预留空间会占用一定的位置,而LL会占用更多的空间,因为要保存其他的一些位置信息

构造方法

Arraylist有有参构造方法也有无参构造方法,有参的构造方法会将我们的容器大小设置为设置的大小

无参构造方法先是将提前创建好的空数组给他,后续使用的时候在进行扩容操作

扩容机制

这里每次add操作的时候都会使用ensure CapacityInternal()方法进行容量判断,如果不足则会使用grow进行扩容

在这里插入图片描述

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

这里的calculate Capacity方法主要是判断容器是否是已经初始化过

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

这里计算所需的最小容量是否大于当前的容器容量

private void ensureExplicitCapacity(int minCapacity) {
    // 用来记录遍历的时候时候,集合有没有进行改变
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这里先将数组扩容为原来的1.5倍,判断是否够用,或者有没有超过最大的容量

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);
}

hugeCapacity()方法 当我们容量超过了当前容器设置的最大值的时候执行

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?	// 这里的MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }


如果不够的话,也只能最多再添加8个了

remove

Arraylist 集合移除元素时候执行的函数,然后使用 **System.arraycopy()**方法将数组进行复制再将原来位置设置为null方便进行 gc

public E remove(int index) {
    rangeCheck(index); // 判断移除的元素是否越界

    modCount++;  // 判断当前的集合是否被修改
    E oldValue = elementData(index);

    int numMoved = size - index - 1;	// 确定移除位置
    if (numMoved > 0)	
        System.arraycopy(elementData, index+1, elementData, index, numMoved);  
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

HashMap

总:

HashMap在 jdk1.8之前是由Node数组+链表组成的,在jdk1.8以后更改了解决hash冲突的方式,是由Node数组+链表或者红黑树实现。元素添加是通过**putVal()方法添加,HashMap通过扰动函数处理得到Hash值,如果发生hash冲突的时候就会先判断当前节点的链表大小如果超过8,然后调用treeifBin()**如果hashMap总的容量大于64则会转为红黑树进行存储。

为什么不使用多路平衡二叉树?

  1. 红黑树是一种平衡二叉树,多路平衡二叉树需要存储更多的节点信息,空间上会使用更多的空间
  2. 操作的复杂性,红黑树不是严格的平衡二叉树,两边子节点的高度差没有完全的差1,插入的时候更好的处理
  3. 红黑树的查询表现已经可以了,不需要多路平衡二叉树的B+树的范围查询了
构造方法

构造方法有三个,关键就是携带参数的问题,有没有 **initialCapacity初始化容量**和 loadFactor扩容阈值

细节问题

默认的大小是16,AL是10,扩容每次为一倍,AL每次扩容1.5倍

putVal()方法

在这里插入图片描述

resize()方法

计算出新的**Capacity**和 threshold的值,然后创建一个新的数组,将我们原来的数组的值复制进去

**threshold**的值是通过 Capacity的值与设置的阈值0.75相乘得出来的

Hash值

hashCode()方法返回的是int整数类型,其范围为


-(2 ^ 31)~(2 ^ 31 - 1),而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap常见问题

为什么线程不安全

因为可能会造成数据丢失的问题,假如两个线程同时进行插入,并且产生了Hash碰撞,那么线程1判断完成以后插入之前挂起,同时线程2进行插入,这时线程1插入的将会覆盖线程2

ConcurrentHashMap

总:

ConcurrentHashMap是一个并发容器,能够解决多个线程使用HashMap造成的线程安全问题,底层的**putVal函数**是通过 Cas操作和Synchronized操作来保证线程的安全性。其他的结构和HashMap一样。

putVal()方法

和HashMap的过程基本一致

只是如果计算出hash值原位置没有的话,直接使用cas操作进行插入

如果后续碰撞则使用synchronized进行插入

自己的测试

自己使用了四个线程,分别进行100万次随机位置的写入,查看每个线程的完成时间,平均每个线程的完成时间将会降低百分之30左右。

为什么重写HashCode和equals

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

思考:重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

总结

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
  • 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。
  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. HashMap底层原理HashMap底层实现是基于哈希表的数据结构,主要包含数组和链表(或红黑树)。数组用于存储键值对,链表(或红黑树)用于解决哈希冲突。当使用put方法向HashMap中添加元素时,首先会根据键的哈希值计算出在数组中的索引位置,如果该索引位置已经存在元素,就会遍历链表(或红黑树)查找该键是否已经存在,如果存在则更新值,否则就将该元素插入到链表(或红黑树)的末尾。当HashMap中的元素数量超过阈值时,就会触发扩容操作,即将数组容量翻倍,并重新计算所有元素的索引位置。 2. ArrayList底层原理ArrayList底层实现是基于数组的数据结构,通过数组实现动态扩容。当使用add方法向ArrayList中添加元素时,首先会判断当前数组容量是否已满,如果已满就会创建一个新数组,将原数组中的元素复制到新数组中,并将新增元素添加到新数组中。由于数组的随机访问效率较高,因此ArrayList在随机访问元素时效率较高,但在插入和删除元素时效率较低。 3. TreeSet底层原理: TreeSet底层实现是基于红黑树的数据结构,对于插入、删除和查找操作的时间复杂度均为O(log n)。当使用add方法向TreeSet中添加元素时,会自动按照元素的大小进行排序,并将元素插入到红黑树中的合适位置。由于红黑树的特性,TreeSet中的元素是有序的,因此在获取元素的子集、前驱和后继等操作时效率较高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值