分布式Java应用之集合框架篇(上)

前言:Java程序员开发程序时,必定会使用JDK中提供的集合类来完成功能模块的开发,而JDK是Java规范的实现,不同厂商提供的JDK也多少会存在一些差异,那么,如何选用合适的集合类实现应用中的具体需求,是每个Java程序员在实际开发中必须解决的一个问题;解决这一问题就需要我们对JDK中集合类的相关实现有一个清晰的认识!

本文首先从全局角度对JDK中的集合包进行一个分析,接着对JDK中常用的集合类的关键实现进行源码分析和比较,最后对本文的内容进行归纳总结,希望能够能够帮助读者从整体与局部两方面认识和理解JDK中的集合以及对应的选择使用!


首先,给出集合框架总体UML图:

注意:上述UML图仅仅给出了本文主要讨论的集合的接口以及实现类的关系图,对于一些其他的类图关系并没有给出,目的在于将接口与实现类之间的关系清晰化;
在这里插入图片描述


分析:

Collection接口存放一个个的单个对象,Collection接口下有两个子接口:List接口以及Set接口;

  • List接口:元素按序存放、可重复、允许元素为null
  • 常用实现类:
    • ArrayList
    • LinkedList
    • Vector
    • Stack
  • Set接口:元素无序存放、不可重复、允许元素为null
  • 常用实现类:
    • HashSet
    • TreeSet

Map接口存放Key-Value形式的键值对,Key不可重复,Value可重复;

  • 常用实现类:
    • HashMap
    • Hashtable
    • TreeMap

接着,对每一个具体的实现类中常用方法的实现进行源码分析(基于JDK1.8);

首先,看一下List接口的实现类:ArrayList、LinkedList以及Vector;主要从底层存储结构、实例构造、添加元素、删除元素、查找元素以及遍历元素这些方面进行分析;


  • ArrayList
    • 底层结构及相关属性
     /**
     * 默认初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * ArrayList用于存放元素的数组
     * ArrayList的容量为数组的长度
     */
    transient Object[] elementData; // 非私有属性,便于内部类访问
     /**
     * ArrayList实例中包含的元素个数
     */
    private int size;
    
    • 构造函数
    /**
     *共享的空数组对象,当含参构造器的初始化容量为0时,使用的底层数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
    /**
     * 共享的空数组对象,用于无参构造函器使用,区别于EMPTY_ELEMENTDATA数组的是,
     * 当第一次添加元素时,会初始化底层数组为长度为10的数组
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /**
     * 根据指定参数实例化对象
     * @param  initialCapacity ArrayList实例的初始化容量
     * @throws IllegalArgumentException 如果指定参数为负数,抛出异常
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;//
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
    }
    /**
     * 构造一个空数组,当第一次添加元素时,会将底层数组替换为长度为10的数组
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    注意:该构造函数的设计是JDK1.8新添的懒初始化特性
    • 添加元素
    protected transient int modCount = 0;
    /**
     * 向数组尾部添加指定元素
     * @param e 待添加的元素
     * @return <tt>true</tt> 添加成功返回true
     */
    public boolean add(E e) {
        //判断当前数组的长度是否足够容纳新元素,如果长度不够,需要进行扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //插入元素
        elementData[size++] = e;
        return true;
    }
    /**
     * 判断当前数组长度是否足够
     * @param minCapacity
     */
    private void ensureCapacityInternal(int minCapacity) {
        //判断是否第一次添加,第一次添加元素需要初始化数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //再次判断数组长度是否足够
        ensureExplicitCapacity(minCapacity);
    }
    //纯判断数组长度是否足够
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//添加元素属于实例结构变更,需要记录
        // 判断是否需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    /**
     * 数组可以分配的最大长度
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    /**
     * 扩容以保证能够存放个数为minCapacity的元素
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // 扩容代码
        int oldCapacity = elementData.length;
        // 1.5倍扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 取两者的较大值
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 不能超过最大数组的长度
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 拷贝原有数组的元素到新数组中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // 溢出
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }
    
    过程梳理:
    首先,判断数组是否第一次添加元素,如果是,初始化数组为长度为10的数组,并添加元素;如果不是,判断当前数组中元素个数+1的值是否超出当前数组长度,如果未超出,则modCount加一,同时,将新元素插入底层数组;如果超出,则modCount加一,同时,进行初步扩容(1.5倍),并对初步扩容之后的容量进行合法判断,保证不能溢出,以及超出允许的数组最大长度,复制原数组中的元素到扩容后的新数组中,用新数组更新底层数组,并添加新元素;
    • 删除元素
    /**
      * 删除第一个出现的与指定参数相等的元素(注意指定参数可以为null)
      * @param o 待删除的元素(如果存在的话)
      * @return <tt>true</tt> 如果存在指定元素,并删除成功返回true
      */
     public boolean remove(Object o) {
         //如果待删除元素为null,使用==判断是否相等
         if (o == null) {
             for (int index = 0; index < size; index++)
                 if (elementData[index] == null) {
                     //找到元素位置,调用具体的删除方法
                     fastRemove(index);
                     return true;
                 }
         } else {
             //如果待删除元素不为null,使用equals判断是否相等
             for (int index = 0; index < size; index++)
                 if (o.equals(elementData[index])) {
                     //找到元素位置,调用具体的删除方法
                     fastRemove(index);
                     return true;
                 }
         }
         return false;
     }
    
     /*
      * 不需要判断是否越界,快速删除
      */
     private void fastRemove(int index) {
         modCount++;
         int numMoved = size - index - 1;
         if (numMoved > 0)
             System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
         elementData[--size] = null; // 帮助GC回收待删除的元素
     }
    
    过程梳理:
    分情况进行查找待删除元素,待删除元素为null,则使用==进行查找;待删除元素不为null,则使用equals进行查找;找到之后,调用System.arraycopy,复制待删除元素之后的元素到原数组(左移覆盖),最后释放最后一个元素,已达到删除的效果;
    • 查找元素
      查找元素过程简单,首先判断给定索引位置是否合法,合法直接返回索引处的元素,否则,抛出异常;
    • 遍历元素
    private class Itr implements Iterator<E>{
    //省略具体实现
    }
    public Iterator<E> iterator() {
            return listIterator();
        }
    
    元素的遍历可以通过调用iterator()方法返回的Iterator对象进行遍历,注意返回的Iterator对象是ArrayList内部实现的Itr内部类的对象;遍历的过程不难,需要注意的是,ArrayList对象的遍历是快速失败的,即在迭代的过程中,如果发现modCout与开始遍历之前的modCount值不同,将会停止遍历,抛出ConcurrentModificationException异常;

小结:

ArrayList底层基于数组实现,采用1.5倍扩容机制,懒初始化以及非线程安全;


  • LinkedList
    • 底层结构
    /**
      * 指向第一个节点的指针,如果为null,则指向最后一个节点的指针也为null
      */
     transient LinkedList.Node<E> first;
     /**
      * 指向最后一个节点的指针,如果为null,则指向第一个节点的指针也为null
      */
     transient LinkedList.Node<E> last;
     /**
      * 静态内部类,存放元素的节点类
      * @param <E>
      */
     private static class Node<E> {
         E item;//链表中存放的元素
         Node<E> next;//指向后继节点的指针
         Node<E> prev;//指向上一节点的指针
         //构造函数
         Node(Node<E> prev, E element, Node<E> next) {
             this.item = element;
             this.next = next;
             this.prev = prev;
         }
     }
     /**
      * 构造一个空链表
      */
     public LinkedList() {
     }
    
    注意:LinkedList是基于链表结构实现的,且链表是双向链表;元素存放到LinkedList,需要先包装成一个节点,然后在插入链表;
    • 添加元素
    //底层结构调整次数
    protected transient int modCount = 0;
    /**
     * 添加指定元素到线性表尾部
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //在尾部插入元素
        linkLast(e);
        return true;
    }
    /**
     * 将待插入元素插入线性表尾部
     */
    void linkLast(E e) {
        final Node<E> l = last;
        //以指定元素构造节点对象
        final Node<E> newNode = new Node<>(l, e, null);
        //将新节点作尾节点
        last = newNode;
        if (l == null)
            first = newNode;//头结点
        else
            l.next = newNode;//连接到尾部结点
        size++;
        modCount++;
    }
    
    小结:链表实现的线性表的插入只需要对前后节点进行操作即可,不需要移动元素,故效率相比数组实现的线性表要高
    • 删除元素
     /**
     * 删除链表中第一个出现的与待删除元素相同的元素(如果存在的话)
     * 同样需要对待删除元素是否为null进行分情况处理
     * @param o 待删除元素
     * @return {@code true} 如果包含待删除元素则返回true
     */
    public boolean remove(Object o) {
        //待删除元素为null
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    //实际的删除方法
                    unlink(x);
                    return true;
                }
            }
        } else {
            //待删除元素不为null
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    //实际的删除方法
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * 删除指定元素所在的节点
     */
    E unlink(Node<E> x) {
        //先保存前后向指针以及节点中的元素
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        //处理前驱指针
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        //处理后继指针
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        //释放元素的引用,帮助GC
        x.item = null;
        size--;
        modCount++;
        return element;
    }
    
    小结:双向链表中元素的删除,需要先遍历链表找到待删除的节点,接着需要先处理前向指针,再处理后继指针;查找节点的过程是与数组链表相同的,但是删除操作却是更简单、高效的,因为不需要移动元素!
    • 查找元素
      查找元素则相对简单,只需要遍历链表,比较元素是否相等即可,但是,在遍历之前,需要对索引位置的合法性进行判断!
      注意:
      JDK对基于链表实现的List的查找进行了优化,利用待查找元素的位置与元素的个数进行比较,并且结合双向链表的双向遍历,缩小查找的范围,位于左边则从左向右遍历,反之类似;
    • 遍历元素
      遍历过程与基于数组实现的List相似,但是双向链表允许从两个方向进行遍历
      注意:
      LinkedList的遍历也是支持快速失败的,即在遍历的过程中,如果发生添加或删除,都会抛出并发修改的异常!

小结:

LinkedList是基于链表实现的,插入元素时,需要先将元素包装成一个节点对象,LinkedList是非线性安全的!


Vector
 Vector与ArrayList类似,都是基于数组实现的,也是支持扩容的;下面给出两者之间不同的地方:
扩容机制:

两者的初始容量都是10,但是Vector引进变量capacityIncrement,对扩容的方式进行控制;当capacityIncrement > 0时,扩容后的数组大小为原数组的大小加上capacityIncrement;当capacityIncrement <= 0时,扩容后的数组大小为原数组大小的两倍;可见,相比ArrayList,Vector的扩容机制更为可控!

线程安全性:

Vector是线程安全(采用synchronized进行同步)的,ArrayList是非线程安全的


Stack

Stack是继承Vector类的,在Vector的基础上实现了Stack所要求的后进先出的操作,提供了push、pop以及peek等方法
注意:
Stack是基于Vector实现的,支持先进后出特性!


总结:

篇幅所限,本文先给出了集合包的整体框架,之后,又对其中的List家族进行了源码分析;在后续的文章中,将会对Set家族成员以及Map家族成员进行分析,如有描述不合适的地方还请指出,相互交流,共同成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值