【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
ArrayList:
构造函数:
ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。
ArrayList底层是使用一个Object类型的数组来存放数据的。
transient Object[] elementData; // non-private to simplify nested class access
size变量代表List实际存放元素的数量
private int size;
不指定ArrayList大小时,默认数组大小为10
private static final int DEFAULT_CAPACITY = 10;
接下来看几个常用的方法:
add:
1.
public boolean add(E e) { ensureCapacityInternal(size + 1); // 扩容检测 elementData[size++] = e; //新增元素加到末尾 return true; }
2.
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); //扩容检测 System.arraycopy(elementData, index, elementData, index + 1, size - index); //使用System.arraycopy的方法,将index后面元素往后移动1位 elementData[index] = element; // 存放元素到index位置 size++; }
set,get
get和set方法,都是通过数组下标,直接操作数据的,时间复杂度为O(1)
public E get(int index) { rangeCheck(index); return elementData(index); }
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
我们重点关注ArrayList的扩容策略,这也是我们在实际工作中决定是否选择ArrayList需要考虑的
像之前add方法里,每次增加元素时都会进行扩容检测,如果数组大小不足,则会自动扩容;如果扩容后的大小超出数组最大的大小,则会抛出异常。
ensureCapacityInternal(size + 1); // 扩容检测
那么ArrayList最大长度可以看一下定义,是Integer的最大长度-8,Integer是4个字节,那么ArrayList的最大长度就是2^32-8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
扩容:
接下来看一下具体的扩容过程:
ArrayList扩容方案,主要有两个步骤:1.大小检测,2.扩容
大小检测:
检测数组大小是否为0,如果是,则使用默认的扩容大小10
检测是否需要扩容,只有当数组最小需要容量大小大于当前数组大小时,才会进行扩容
扩容:grow和hugeCapacity
进行数组越界判断
拷贝原始数据到新的数组中
private void ensureCapacityInternal(int minCapacity) { // 通过ArrayList<Integer> a = new ArrayList<Integer>()或者通过序列化读取,元素大小为0时,底层数组才会为null数组 // 如果底层数组大小为0,则使用默认的容量大小10 if (elementData == EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { // 数据结构发生改变,和fail-fast机制有关,在使用迭代器过程中,只能通过迭代器的方法(比如迭代器中add,remove等),修改List的数据结构, // 如果使用List的方法(比如List中的add,remove等),修改List的数据结构,会抛出ConcurrentModificationException modCount++; // 当前数组容量大小不足时,才会调用grow方法,自动扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); }
可以看到扩容长度是通过位运算完成的
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 新的容量大小 = 原容量大小的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) //溢出判断,比如minCapacity = Integer.MAX_VALUE / 2, oldCapacity = minCapacity - 1 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
fail-fast机制的实现
可以看到在ArrayList中,有个modCount的变量,每次进行add,set,remove等操作,都会执行modCount++。
在获取ArrayList的迭代器时,会将ArrayList中的modCount保存在迭代中,
每次执行add,set,remove等操作,都会执行一次检查,调用checkForComodification方法,对modCount进行比较。如果迭代器中的modCount和List中的modCount不同,则抛出ConcurrentModificationException
final void checkForComodification() { if (expectedModCount != ArrayList.this.modCount) throw new ConcurrentModificationException(); }
在对集合进行迭代过程中,除了迭代器可以对集合进行数据结构上进行修改,其他的对集合的数据结构进行修改,都会抛出ConcurrentModificationException错误。
序列化
transient Object[] elementData;
transient修饰符让elementData无法自动序列化,这样的原因是,数组内存储的的元素其实只是一个引用,单单序列化一个引用没有任何意义,反序列化后这些引用都无法在指向原来的对象。ArrayList使用writeObject()实现手工序列化数组内的元素。
通过以上源码分析,其实就可以总结出ArrayList的优缺点以及使用场景
优点:
1.get,set,时间复杂度为O(1),查找元素快速
2.数据是顺序存储的
缺点:
1.add(一般都是在末尾插入),时间复杂度为O(1),最差情况下(往头部插入数据),时间复杂度O(n)
2.remove,时间复杂度为O(n),最优情况下(移除末尾元素),时间复杂度为O(1)
3.ArrayList底层使用数组存储数据,数组是不能自动扩容的,因此在发生扩容的情况下,需要移动大量的元素。
4.数组大小是由限制的,受jvm和机器的影响,当扩容超出上限时,ArrayList会抛出异常
5.ArrayList所有的操作,都不是同步的,因此ArrayList不是线程安全的。
使用场景:
插入操作多,数据量不大,顺序存储时,可以考虑使用ArrayList
LinkedList
构造函数:
LinkedList有两个构造参数,一个为无參构造,只是新建一个空对象,第二个为有参构造,新建一个空对象,然后把所有元素添加进去。
public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
LinkedList的存储单元为一个名为Node的内部类,包含pre指针,next指针,和item元素,实现为双向链表
//链表的节点个数
transient int size = 0;
//指向头节点的指针
transient Node<E> first;
//指向尾节点的指针
transient Node<E> last;
节点结构
Node 是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域item,一个后置指针next,一个前置指针prev。
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; } }
总结:
ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方:
1.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。
2.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
使用场景:
当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
来源:oschina
链接:https://my.oschina.net/u/1393804/blog/607699