ArrayList集合、HashSet集合、HashMap、Hashtable、ConcurrentHashMap的原理

本文是我个人对ArrayList集合、HashSet集合、HashMap、Hashtable、ConcurrentHashMap这几个集合的理解,也希望能够帮助到看了此文章的你,如有不当之处,还望不吝赐教。


ArrayList:

成员变量:

private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;//初始容量
private static final Object[] EMPTY_ELEMENTDATA = {};//空对象实例
//一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; //用于存储元素的数组,不参与序列化
private int size;//数组长度
  1. 首先创建这个集合的时候,如果指定初始长度会先判断长度是否大于0,如果大于0就创建一个object[长度] 赋值给elementData 它就是存储整个集合 否则就报一个一个异常 并且将类中的一个静态常量 值是空的object数组赋值给elementData

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else {
            if (initialCapacity != 0) {
                throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
            }
    
            this.elementData = EMPTY_ELEMENTDATA;
        }
    
    }
    
  2. 上面是有参构造,如果创建集合的时候是无参的,也会将另一个静态常量 长度为0的object数组 赋值给elementData

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;   //Object[0];
    }
    
  3. 第三种就是构造的时候传的参数是一个Collection集合,会直接会这个形参调用toArray()方法赋值给elementData;这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy 做深拷贝 为size赋值 判断形参传递过来的collection集合是否是一个空的集合,如果是则还是把静态成员变量一个空的object数组在赋值给elementData,如果不为空 则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。

    public ArrayList(Collection<? extends E> c) {
        this.elementData = c.toArray();
        if ((this.size = this.elementData.length) != 0) {
            if (this.elementData.getClass() != Object[].class) {
                this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
            }
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    
  4. 上面就是list集合是三种构造方法,接下来的添加元素以及扩容的问题,第一次添加元素的时候,比较的是size+1 和当前的最大容量比较,所以第一次添加元素的时候会将当前elementData的长度赋值为10,也是静态常量。

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    

    size+1 和 当前数组长度 进行比较,数组长度大,就允许添加,将新元素添加到位于size的位置上。否则当前容量会进行扩容,当前容量加上当前容量右移一位。如果扩充的长度小于与所需的最小长度,则长度变为数组所需的最小长度;如果扩充的长度大于MAX_ARRAY_SIZE,则调用hugeCapacity()方法来获得 最小长度minCapacity和MAX_ARRAY_SIZE 的小值 在进行copyOf方法

    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);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
    }
    

HashSet

HashSet底层由HashMap实现,HashSet的值存放于HashMap的key位置上,HashMap的value统一为present

HashMap

HashMap的默认长度是16,默认负载因子是0.75 ,每一次添加 首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,尾插法 。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。

当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍+1,把原链表数组的搬移到新的数组中

在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。当一个链的长度变为了6又会将红黑树变为链表。6与8直接空了一个7,这样的好处是避免长度一直在7和8之间来回切换从而频繁进行链表与红黑树的转换。那为什么是8的时候进行链表转红黑树呢?这是在jdk底层源码中可以发现,jdk的开发人员使用泊松分布列举出了1到8具体值,其实在为8的时候概率就已经很小了,再往后调整并没有很大意义。

加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低


新的理解:
初始值:16
负载因子:0.75

  1. 调用put()方法
  2. hash(key) & (当前容量-1) 进行位运行 得到该键值对存放在数组中的位置
  3. 是否hash冲突,如果在数组的这个位置没有产生hash冲突就直接存储
  4. 检查key是否相同,如果相同则执行替换并返回旧值的操作
  5. 如果key不相同则进行判断 红黑树/链表
  6. 如果是红黑树则进行红黑树的添加逻辑
  7. 如果是链表,会使用一个for循环,找到链表的最后一个null,然后存值
  8. 在进行循环遍历链表时如果发现了key相同,也会进行替换并返回旧值的操作
  9. 在进行遍历时也会进行判断当前链表是否会扩容或转换红黑树,转换红黑树的条件是链表长度>8并且数组长度>64,如果只满足其中一个条件只会进行扩容

扩容的步骤:
10. 当前容量不能超过2^30次方
11.当前容量<1 左移一位
源码中有一个size变量来记录已经存储了多少个数据,当进行put()方法后 ++size > 当前总容量*负载因子 就会进行扩容。

Hashtable

hashtable和hashmap其实没什么区别,只是它的底层方法都加了synchronized,是线程安全的,它的初始容量是11,负载因子也是0.75,put() 添加元素的时候首先判断value是否为空 如果的就报空指针异常,然后在通过key的hashCode() 得到哈希值 确保当前key在集合中没有重复的,这个时候如果添加元素的key是空,null.hashCode() 也会报空指针异常。

如果当前集合的存储个数>= 了集合的总容量*负载因子,这时候就会进行扩容。扩容机制也是当前容量左移一位 + 1;

ConcurrentHashMap

在jdk1.5的时候出现的,正是因为Hashtable的关系到存取的每个方法都加了synchronized关键字,所以在高并发的环境下效率很慢,然后就出现了ConcurrentHashMap,它的底层实现和HashMap息息相关,它内部有两个静态内部类:HashEnter和 Sngment 。HashEnter存储的是集合元素的键值对映射关系,Sngment是负责锁的。ConcurrentHashMap默认会分为16段锁,这也正是因为HashMap的默认初始化容量是16,锁其实就是锁的HashMap的每一条链表。

ConcurrentHashMap默认分成了16个segment,每个Segment都对应一个 Hash表,且都有独立的锁。所以这样就可以每个线程访问一个Segment,就可以并行访问了, 从而提高了效率。这就是锁分段。但是,java 8 又更新了,不再采用锁分段机制,也采用CAS算 法了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值