1. 前言
集合是Java基础中非常重要的一部分, 也是日常中用到非常多的类, 这篇博客主要记录集合List相关的知识.
特别声明: 大部分内容并非原创, 引用自参考链接.2. Java集合
相比于数组(Array)来说, 集合类的长度可变, 更加适合于现代开发需求;
在程序运行时, Java集合可以动态的进行扩展, 随着元素的增加而扩大.
在Java中, 集合类通常存在于java.util包中.
Java集合主要由2大体系构成, 分别是Collection体系和Map体系, 其中Collection和Map分别是2大体系中的顶层接口.
Collection主要有三个子接口, 分别为List(列表), Set(集), Queue(队列). 其中, List, Queue中的元素有序可重复, 而Set中的元素无序不可重复;
List中主要有ArrayList, LinkedList两个实现类; Set中则是有HashSet实现类; 而Queue是在JDK1.5后才出现的新集合, 主要以数组和链表两种形式存在.
Map同属于java.util包中, 是集合的一部分,但与Collection是相互独立的, 没有任何关系. Map中都是以key-value的形式存在, 其中key必须唯一, 主要有HashMap, Hashtable, TreeMap三个实现类.
3. List
在Collection中, List集合是有序的, Developer可对其中每个元素的插入位置进行精确地控制, 可以通过索引来访问元素, 遍历元素.
在List集合中, 我们常用到ArrayList和LinkedList这两个类.
3.1 初识ArrayList
其中, ArrayList底层通过数组实现, 随着元素的增加而动态扩容. 而LinkedList底层通过链表来实现, 随着元素的增加不断向链表的后端增加节点.
ArrayList是Java集合框架中使用最多的一个类, 是一个数组队列, 线程不安全集合.
它继承于AbstractList, 实现了List, RandomAccess, Cloneable, Serializable接口.
(1)ArrayList实现List, 得到了List集合框架基础功能;
(2)ArrayList实现RandomAccess, 获得了快速随机访问存储元素的功能, RandomAccess是一个标记接口, 没有任何方法;
(3)ArrayList实现Cloneable, 得到了clone()方法, 可以实现克隆功能;
(4)ArrayList实现Serializable, 表示可以被序列化, 通过序列化去传输, 典型的应用就是hessian协议.
它具有如下特点:
(1)容量不固定, 随着容量的增加而动态扩容(阈值基本不会达到)
(2)有序集合(插入的顺序=输出的顺序)
(3)插入的元素可以为null
(4)增删改查效率更高(相对于LinkedList来说)
(5)线程不安全.
数据结构:
3.2 初识LinkedList
LinkedList是一个双向链表, 每一个节点都拥有指向前后节点的引用. 相比于ArrayList来说, LinkedList的随机访问效率更低.
它继承AbstractSequentialList, 实现了List, Deque, Cloneable, Serializable接口.
(1)LinkedList实现List, 得到了List集合框架基础功能;
(2)LinkedList实现Deque, Deque 是一个双向队列, 也就是既可以先入先出, 又可以先入后出,说 简单些就是既可以在头部添加元素, 也可以在尾部添加元素;
(3)LinkedList实现Cloneable, 得到了clone()方法, 可以实现克隆功能;
(4)LinkedList实现Serializable, 表示可以被序列化, 通过序列化去传输, 典型的应用就是hessian协议.
数据结构:
3.3 List常用方法
1 | A:添加功能 |
3.4 ArrayList和LinkedList性能比较
3.4.1 元素新增
从直观上看, 在新增操作时, ArrayList效率不如LinkedList, 因为ArrayList底层是数组实现, 在动态扩容时, 性能有所损耗, 而LinkedList不存在数组扩容机制, 所以LinkedList效率更高.
1 | public class ListTest { |
结果:
1 | 第一组: |
结果与预想的有些不太一样, ArrayList的新增性能并不低.
究其原因, 可能是经过JDK近几年的更新发展, 对于数组复制的实现进行了优化, 以至于ArrayList的性能也得到了提高.
3.4.2 元素获取
由于LinkedList是链表结构, 没有角标的概念, 没有实现RandomAccess接口, 不具备随机元素访问功能, 所以在get方面表现的差强人意, ArrayList再一次完胜.
1 | public class ListTest { |
结果:
1 | 第一组: |
从结果中可以看到, ArrayList在随机访问方面表现的十分优秀, 比LinkedList强了很多, 基本上保持在500-1000倍.
LinkedList为什么这么慢呢?这主要是LinkedList的代码实现所致, 每一次获取都是从头开始遍历, 一个个节点去查找, 每查找一次就遍历一次, 所以性能自然得不到提升.
3.5 ArrayList源码分析(基于JDK1.7.0_45)
接下来, 我们几对ArrayList的源码进行一个解析, 主要从以下几个问题出发.
(1)ArrayList构造
(2)增删改查实现
(3)迭代器-modCount
(4)为什么数组对象要使用transient修饰符
(5)System.arraycopy()参数含义 和 Arrays.copyOf()参数含义
我们通过这这几个问题, 来一步步的学习ArrayList.
3.5.1 ArrayList构造器
在JDK1.7版本中, ArrayList的无参构造方法并没有生成容量为10的数组;
elementData对象是ArrayList集合底层保存元素的实现;
size属性记录了ArrayList集合中实际元素的个数;
1 | public class ArrayList<E> extends AbstractList<E> |
3.5.2 add()
ArrayList增加元素的方法事关重要, 我们都知道ArrayList底层是由数组, 可以随着元素的增加而扩容, 那么具体是如何实现的呢?
在JDK1.7当中, 当第一个元素添加时, ensureCapacityInternal()方法会计算ArrayList的扩容大小, 默认为10;
其中grow()方法最为重要, 如果需要扩容, 那么扩容后的大小是原来的1.5倍, 实际上最终调用了Arrays.copyOf()方法得以实现;
1 | //添加元素e |
3.5.3 remove()
remove(int index)是针对于角标来进行删除, 不需要去遍历整个集合, 效率更高;
而remove(Object o)是针对于对象来进行删除, 需要遍历整个集合进行equals()方法比对, 所以效率较低;
不过, 无论是哪种形式的删除, 最终都会调用System.arraycopy()方法进行数组复制操作, 所以效率都会受到影响;
1 | //在ArrayList的移除index位置的元素 |
3.5.4 set()
由于ArrayList实现了RandomAccess, 所以具备了随机访问特性, 调用elementData()可以获取到对应元素的值;
1 | //设置index位置的元素值了element,返回该位置的之前的值 |
3.5.5 get()
通过elementData()方法获取对应角标元素, 在返回时候进行类型转换;
1 | //获取index位置的元素 |
3.5.6 modCount含义
在Itr迭代器初始化时,将ArrayList的modCount属性的值赋值给了expectedModCount.
通过上面的例子中, 我们可以知道当进行增删改时, modCount会随着每一次的操作而+1, modCount记录了ArrayList内发生改变的次数.
当迭代器在迭代时, 会判断expectedModCount的值是否还与modCount的值保持一致, 如果不一致则抛出异常.
AbstractList类当中定义的变量:
1 | protected transient int modCount = 0; |
ArrayList获取迭代器对象:
1 | //返回一个Iterator对象,Itr为ArrayList的一个内部类,其实现了Iterator<E>接口 |
迭代器实现:
1 | //Itr实现了Iterator接口,是ArrayList集合的迭代器对象 |
3.5.7 transient
transient修饰符是什么含义?
当我们序列化对象时, 如果对象中某个属性不进行序列化操作, 那么在该属性前添加transient修饰符即可实现; 例如:
1 | private transient Object[] elementData; |
那么, 为什么ArrayList不想对elementData属性进行序列化呢? elementData可是集合中保存元素的数组啊, 如果不序列化elementData属性, 那么在反序列化时候, 岂不是丢失了原先的元素?
ArrayList在添加元素时, 可能会对elementData数组进行扩容操作, 而扩容后的数组可能并没有全部保存元素.
例如: 我们创建了new Object[10]数组对象, 但是我们只向其中添加了1个元素, 而剩余的9个位置并没有添加元素. 当我们进行序列化时, 并不会只序列化其中一个元素, 而是将整个数组进行序列化操作, 那些没有被元素填充的位置也进行了序列化操作, 间接的浪费了磁盘的空间, 以及程序的性能.
所以, ArrayList才会在elementData属性前加上transient修饰符.
接下来, 我们来看下ArrayList的writeObject(), readObject():
1 | //序列化写入: |
ArrayList在序列化时会调用writeObject(), 直接将elementData写入ObjectOutputStream;
而反序列化时则调用readObject(), 从ObjectInputStream获取elementData;
3.5.8 Arrays.copyOf()
该方法在内部创建了一个新数组, 底层实现是调用System.arraycopy();
1 | public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { |
original - 要复制的数组
newLength - 要返回的副本的长度
newType - 要返回的副本的类型
3.5.9 System.arraycopy()
该方法是用了native关键字, 调用的为C++编写的底层函数.
1 | public static native void arraycopy(Object src, int srcPos, |
src - 源数组
srcPos - 源数组中的起始位置
dest - 目标数组
destPos - 目标数据中的起始位置
length - 要复制的数组元素的数量
3.6 LinkedList源码分析(基于JDK1.7.0_45)
发现很多文章在介绍的时候, 都说LinkedList是一个环形链表结构, 头尾相连. 但, 当我开始看源码的时候, 发现并不是环形链表, 是一个直线型链表结构. 这是因为JDK1.7之前的版本是环形链表, 而到了JDK1.7以后进行了优化, 变成了直线型链表结构;
3.6.1 LinkedList基础结构
在LinkedList中, 内部类Node对象最为重要, 它组成了LinkedList集合的整个链表, 分别指向上一个点, 下一个结点, 存储着集合中的元素;
成员变量中, first表明是头结点, last表明是尾结点;
1 | public class LinkedList<E> |
3.6.2 add()
LinkedList的添加方法, 主要分为2种, 一是直接添加一个元素, 二是在指定角标下添加一个元素;
add(E e)底层调用linkLast(E e)方法, 就是在链表的最后面插入一个元素;
add(int index, E element), 插入的角标如果==size, 则插入到链表最后; 否则, 按照角标大小插入到对应位置;
1 | //添加元素:添加到最后一个结点; |
对于LinkedList集合增加元素来说, 可以简单的概括为以下几点:
将添加的元素转换为LinkedList的Node对象节点;
增加该Node节点的前后引用, 即该Node节点的prev, next属性, 让其分别指向哪一个节点);
修改该Node节点的前后Node节点中pre/next属性, 使其指向该节点.
3.6.3 remove()
LinkedList的删除也提供了2种形式, 其一是通过角标删除元素, 其二就是通过对象删除元素; 不过, 无论哪种删除, 最终调用的都是unlink来实现的;
1 | //删除对应角标的元素: |
3.6.4 set()
LinkedList的set(int index, E element)方法与add(int index,E element)的设计思路基本一致, 都是创建新Node节点, 插入到对应的角标下, 修改前后节点的prev, next属性;
其中, node(int index)方法至关重要, 通过对应角标获取到对应的集合元素.
可以看到, node()中是根据角标的大小是选择从前遍历还是从后遍历整个集合. 也可以间接的说明, LinkedList在随机获取元素时性能很低, 每次的获取都得从头或者从尾遍历半个集合.
1 | //设置对应角标的元素: |
3.6.5 get()
1 | get(int index) |
终于到了最后一个方法, 也是开发中最常用的方法. 其中, 核心方法node(int index)在上面已经介绍过.
在通过node(int index)获取到对应节点后, 返回节点中的item属性, 该属性就是我们所保存的元素.
1 | //获取相应角标的元素: |