集合各实现类的底层实现原理

1、 ArrayList实现原理要点概括

1.1 ArrayList数据结构

       ArrayList的数据结构如下:
在这里插入图片描述
       说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都是基于数组的。下面我们来分析通过数组是如何保证库函数的正确实现的。

1.2 ArrayList源码分析

1.2.1 类的继承关系

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

       说明:ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。

1.2.2 类的属性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 版本号
    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;
    // 实际元素大小,默认为0
    private int size;
    // 最大数组容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

       说明:类的属性中核心的属性为elementData,类型为Object[],用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候,此字段是不会被序列化的。

1.2.3 类的构造函数

1、ArrayList(int)型构造函数

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) { // 初始容量大于0
            this.elementData = new Object[initialCapacity]; // 初始化元素数组
        } else if (initialCapacity == 0) { // 初始容量为0
            this.elementData = EMPTY_ELEMENTDATA; // 为空对象数组
        } else { // 初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

说明:指定elementData数组的大小,不允许初始化大小小于0,否则抛出异常。

2、ArrayList()型构造函数

    public ArrayList() { 
        // 无参构造函数,设置元素数组为空 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

说明:当未指定初始化大小时,会给elementData赋值为空集合。

3、ArrayList(Collection<? extends E>)型构造函数

    public ArrayList(Collection<? extends E> c) { // 集合参数构造函数
        elementData = c.toArray(); // 转化为数组
        if ((size = elementData.length) != 0) { // 参数为非空集合
            if (elementData.getClass() != Object[].class) // 是否成功转化为Object类型数组
                elementData = Arrays.copyOf(elementData, size, Object[].class); // 不为Object数组的话就进行复制
        } else { // 集合大小为空,则设置元素数组为空
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

说明:当传递的参数为集合类型时,会把集合类型转化为数组类型,并赋值给elementData。

1.2.4 核心函数分析

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

说明:正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。

当我们调用add方法时,实际上的函数调用如下
在这里插入图片描述
说明:程序调用add,实际上还会进行一系列调用,可能会调用到grow,grow可能会调用hugeCapacity。

下面通过两种方式给出调用add的例子,并分析最后的elementData数组的大小。

  • 示例一(只给出了会影响到最终结果的核心代码) 
    List<Integer> lists = new ArrayList<Integer>();
    lists.add(8);
     说明:初始化lists大小为0,调用的ArrayList()型构造函数,那么在调用lists.add(8)方法时,会经过怎样的步骤呢?下图给出了该程序执行过程和最初与最后的elementData的大小
    在这里插入图片描述
     说明:我们可以看到,在add方法之前开始elementData = {};调用add方法时会继续调用,直至grow,最后elementData的大小变为10,之后再返回到add函数,把8放在elementData[0]中。
  • 示例二核心代码如下
    List<Integer> lists = new ArrayList<Integer>(6);
    lists.add(8);
    说明:调用的ArrayList(int)型构造函数,那么elementData被初始化为大小为6的Object数组,在调用add(8)方法时,具体的步骤如下:
    在这里插入图片描述
    说明:我们可以知道,在调用add方法之前,elementData的大小已经为6,之后再进行传递,不会进行扩容处理。

2. set函数 

    public E set(int index, E element) {
        // 检验索引是否合法
        rangeCheck(index);
        // 旧值
        E oldValue = elementData(index);
        // 赋新值
        elementData[index] = element;
        // 返回旧值
        return oldValue;
    }

说明:设定指定下标索引的元素值。

3. indexOf函数

    // 从首开始查找数组里面是否存在指定元素
    public int indexOf(Object o) {
        if (o == null) { // 查找的元素为空
            for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标
                if (elementData[i]==null)
                    return i;
        } else { // 查找的元素不为空
            for (int i = 0; i < size; i++) // 遍历数组,找到第一个和指定元素相等的元素,返回下标
                if (o.equals(elementData[i]))
                    return i;
        } 
        // 没有找到,返回空
        return -1;
    }

说明:从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找。

4. get函数

    public E get(int index) {
        // 检验索引是否合法
        rangeCheck(index);

        return elementData(index);
    }

 说明:get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的是,在get函数中存在element函数,element函数用于返回具体的元素,具体函数如下

    E elementData(int index) {
        return (E) elementData[index];
    }

说明:返回的值都经过了向下转型(Object -> E),这些是对我们应用程序屏蔽的小细节。

5. remove函数

    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);
        // 赋值为空,有利于进行GC
        elementData[--size] = null; 
        // 返回旧值
        return oldValue;
    }

 说明:remove函数用户移除指定下标的元素,此时会把指定下标到数组末尾的元素向前移动一个单位,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可以作为小的技巧使用。

1.3 总结

 ArrayList有其特殊的应用场景,与LinkedList相对应。其优点是随机读取,缺点是插入元素时需要移动大量元素,效率不太高。至此,ArrayList的源码分析就到这里,总体来说,ArrayList的底层还是很简单的,

2、LinkedList实现原理要点概括

参考文献:
1.http://www.cnblogs.com/ITtangtang/p/3948610.htmll
2.https://www.cnblogs.com/leesf456/p/5308843.html

  • LinkedList是List接口的双向链表非同步实现,并允许包括null在内的所有元素。

  • 底层的数据结构是基于双向链表的,该数据结构我们称为节点。

  • 双向链表节点对应的类Node的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。

  • 它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到。

    3、HashMap实现原理要点概括

    参考文献:http://zhangshixi.iteye.com/blog/672697
    参考文献:http://blog.csdn.net/lizhongkaide/article/details/50595719

  • HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序。

  • 底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。

  • HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。

  • HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能。

  • 采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常。

    4、Hashtable实现原理要点概括

    参考文献:http://blog.csdn.net/zheng0518/article/details/42199477

  • Hashtable是基于哈希表的Map接口的同步实现,不允许使用null值和null键。

  • 底层使用数组实现,数组中每一项是个单链表,即数组和链表的结合体 Hashtable在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。

  • Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

  • synchronized是针对整张Hash表的,即每次锁住整张表让线程独占。

    5、ConcurrentHashMap实现原理要点概括

    参考文献:http://blog.csdn.net/zheng0518/article/details/42199477

  • ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。

  • 它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个并发发生在不同的段上,它们就可以并发进行。

  • ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

  • 与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)。

  • ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

    6、HashSet实现原理要点概括

    参考文献:http://zhangshixi.iteye.com/blog/673143l

  • HashSet由哈希表(实际上是一个HashMap实例)支持,不保证set的迭代顺序,并允许使用null元素。

  • 基于HashMap实现,API也是对HashMap的行为进行了封装,可参考HashMap。

    7、LinkedHashMap实现原理要点概括

    参考文献:http://zhangshixi.iteye.com/blog/673789l

  • LinkedHashMap继承于HashMap,底层使用哈希表和双向链表来保存所有元素,并且它是非同步,允许使用null值和null键。

  • 基本操作与父类HashMap相似,通过重写HashMap相关方法,重新定义了数组中保存的元素Entry,来实现自己的链接列表特性。该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而构成了双向链接列表。

    8、LinkedHashSet实现原理要点概括

    参考文献:http://zhangshixi.iteye.com/blog/673319l

  • 对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。

全部评论

相关推荐

07-11 22:27
中南大学 Java
程序员牛肉:学历的话没问题。但是没问题的也就只有学历了。 其实你的整体架构是正确的,博客接着干。但是项目有点过于简单了。从后端的角度上讲,你这也就是刚入门的水平,所以肯定约面试够呛。 如果你要应聘后端岗位,那你第一个项目竟然是仿写操作系统。这个你要面试官咋问你。你一定要记住一点,你简历上写的所有的东西,都是为了证明你有能力胜任当前的岗位,而不是为了证明你自己会什么。 如果你只是浅浅的做几个项目,描述也都是烂大街。技术点也都是各种混水类的配置类需求,那你就不要幻想自己能走多远。一定要保持思考,保持学习。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务