集合底层实现原理

本文详细介绍了Java集合框架中的ArrayList、LinkedList、HashMap和线程安全容器如HashTable与ConcurrentHashMap的工作原理及区别。ArrayList基于数组实现,扩容时会拷贝元素;LinkedList是双向链表,适用于频繁插入删除;HashMap使用数组+链表/红黑树,快速存取但非线程安全;HashTable线程安全但效率较低;而ConcurrentHashMap则通过细粒度锁提升并发性能。
摘要由CSDN通过智能技术生成

ArrayList

  ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。底层使用数组实现。类的属性中核心的属性为elementData,类型为Object[],用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候,此字段是不会被序列化的。
该集合是可变长度数组,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过方法ensureExplicitCapacity(int minCapacity)来实现。数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。若是能预估到顶峰容量,可以设置一个足够大的量以避免数组容量以后的扩展。add函数源码:

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

说明:在add函数中有ensureCapacityInternal,此函数可以理解为确保elementData数组有合适的大小。ensureCapacityInternal的具体函数如下:

private void ensureCapacityInternal(int minCapacity) {
   
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    // 判断元素数组是否为空数组
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取较大值
        }
        ensureExplicitCapacity(minCapacity);
    }

在ensureCapacityInternal函数中有ensureExplicitCapacity函数,这个函数也是为了确保elemenData数组有合适的大小。ensureExplicitCapacity的具体函数如下:

private void ensureExplicitCapacity(int minCapacity) {
   
        // 结构性修改加1
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

在ensureExplicitCapacity函数又发现了grow函数,grow函数才会对数组进行扩容,ensureCapacityInternal、ensureExplicitCapacity都只是过程,最后完成实际扩容操作还是得看grow函数,grow函数的具体函数如下:

 private void grow(int minCapacity) {
   
        int oldCapacity = elementData.length; // 旧容量
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍
        if (newCapacity - minCapacity < 0) // 新容量小于参数指定容量,修改新容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大于最大容量
            newCapacity = hugeCapacity(minCapacity); // 指定新容量
        // 拷贝扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

如果在添加的时候原数组是空的,就直接给一个10的长度,否则的话就加一。正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。当调用add方法时,实际上的函数调用如下:
在这里插入图片描述

Java容器的快速报错机制ConcurrentModificationException

  List的fail-fast机制,采用modCount实现,能够防止多个进程同时修改同一个容器的内容。如果在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入,删除或修改此容器的某个对象,就会立刻抛出ConcurrentModificationException。
  在迭代遍历的过程中都调用了方法checkForComodification来判断当前ArrayList是否是同步的。假设往一个Integer类型的ArrayList插入了10条数据,那么每操作一次modCount(继承自父类AbstractList)就加一所以就变成10,而当对这个集合进行遍历的时候就把modCount传到expectedModCount这个变量里,然后ArrayList在checkForComodification中通过判断两个变量是否相等来确认当前集合是否是同步的,如果不同步就抛出ConcurrentModificationException。所谓的不同步指的就是,如果在遍历的过程中对ArrayList集合本身进行add,remove等操作时候就会发生。当然如果用的是Iterator那么使用它的remove是允许的,因为此时直接操作的不是ArrayList集合而是它的Iterator对象。

LinkedList

  LinkedList是List接口的双向链表非同步实现,并允许包括null在内的所有元素。底层的数据结构是基于双向链表的,该数据结构称为节点。双向链表节点对应的类Node的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。
node函数,此函数是根据索引下标找到该结点并返回,具体代码如下:

Node<E> node(int index) {
   
        // 判断插入的位置在链表前半段或者是后半段
        if (index < (size >> 1)) {
    // 插入位置在前半段
            Node<E> x = first; 
            for (int i = 0; i < index; i++) // 从头结点开始正向遍历
                x = x.next;
            return x; // 返回该结点
        } else {
    // 插入位置在后半段
            Node<E> x = last; 
            for (int i = size - 1; i > index; i--) // 从尾结点开始反向遍历
                x = x.prev;
            return x; // 返回该结点
        }
    }

它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找。根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。

对addAll函数的思考
  addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,这里对addAll(int, Collection<? extends E>)型进行分析。

// 添加一个集合
    public boolean addAll(int index, Collection<? extends E> c) {
   
        // 检查插入的的位置是否合法
        checkPositionIndex(index);
        // 将集合转化为数组
        Object[] a = c.toArray();
        // 保存集合大小
        int numNew = a.length;
        if (numNew == 0) // 集合为空,直接返回
            return false;
        Node<E> pred, succ; // 前驱,后继
        if (index == size) {
    // 如果插入位置为链表末尾,则后继为null,前驱为尾结点
            succ = null;
            pred = last;
        } else {
    // 插入位置为其他某个位置
            succ = node(index); // 寻找到该结点
            pred = succ
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值