前言: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;
- 构造函数
注意:该构造函数的设计是JDK1.8新添的懒初始化特性/** *共享的空数组对象,当含参构造器的初始化容量为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; }
- 添加元素
过程梳理: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,复制待删除元素之后的元素到原数组(左移覆盖),最后释放最后一个元素,已达到删除的效果;- 查找元素
查找元素过程简单,首先判断给定索引位置是否合法,合法直接返回索引处的元素,否则,抛出异常; - 遍历元素
元素的遍历可以通过调用iterator()方法返回的Iterator对象进行遍历,注意返回的Iterator对象是ArrayList内部实现的Itr内部类的对象;遍历的过程不难,需要注意的是,ArrayList对象的遍历是快速失败的,即在迭代的过程中,如果发现modCout与开始遍历之前的modCount值不同,将会停止遍历,抛出ConcurrentModificationException异常;private class Itr implements Iterator<E>{ //省略具体实现 } public Iterator<E> iterator() { return listIterator(); }
小结:
ArrayList底层基于数组实现,采用1.5倍扩容机制,懒初始化以及非线程安全;
- LinkedList
- 底层结构
注意:LinkedList是基于链表结构实现的,且链表是双向链表;元素存放到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() { }
- 添加元素
小结:链表实现的线性表的插入只需要对前后节点进行操作即可,不需要移动元素,故效率相比数组实现的线性表要高//底层结构调整次数 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家族成员进行分析,如有描述不合适的地方还请指出,相互交流,共同成长!