许多语言,例如 Perl ,Python 和 Ruby ,都有集合的本地支持。有些语言(例如Python)甚至将基本集合组件(列表,映射和集合)作为内置函数包含在其中。
通常,程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。java.util 库提供了一套相当完整的集合类(collection classes)来解决这个问题,其中基本的类型有 List 、 Set 、 Queue 和 Map。这些类型也被称作容器类。
下面我们就重点聊一聊在日常开发中经常被使用到的两个集合类ArrayList和LinkedList的本质区别吧。
ArrayList是基于数组实现的,数组是典型的紧密结构,所以它使用索引在数组中搜索和读取数据快,可以直接返回数组中index位置的元素,因此在随机访问集合元素上有较好的性能。
LinkedList是基于双链表实现的,链表是典型的跳转结构,插入和删除只是指针指向的修改,所以它插入、删除中间元素快,因此在操作数据方面性能较好。
如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
如果应用程序有更多的插入或者删除操作,较少的随机访问,LinkedList对象要优于ArrayList对象;
下面我们通过JDK1.8源码源码分析一下核心功能。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //默认大小 private static final int DEFAULT_CAPACITY = 10; //省略。。。 //动态数组 transient Object[] elementData; //数组大小 private int size; //空构造器 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 查询元素 public E get(int index) { // 检查索引是否在范围内 rangeCheck(index); return elementData(index); } //顺序添加元素 public boolean add(E e) { //扩容 ensureCapacityInternal(size + 1); elementData[size++] = e; return true; } //从数组中间添加元素 public void add(int index, E element) { // 检查索引是否在范围内 rangeCheckForAdd(index); // 扩容 ensureCapacityInternal(size + 1); // 复制数组 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 替换元素 elementData[index] = element; size++; } //是否要扩容 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } //确定容量 private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); } //计算数组容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } //动态数组扩容放法 private void grow(int minCapacity) { // int oldCapacity = elementData.length; 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); } //省略。。。 }
总结:
//JDK1.8 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { // 元素数量 transient int size = 0; // 链表首节点 transient Node<E> first; // 链表尾节点 transient Node<E> last; // 空构造器 public LinkedList() { } // Node内部类 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 boolean add(E e) { linkLast(e); return true; } // 指定位置添加元素 public void add(int index, E element) { // 检查是否越界 checkPositionIndex(index); // 在链表末尾添加,否则在之前插入元素 if (index == size) linkLast(element); else linkBefore(element, node(index)); } // 添加元素e void linkLast(E e) { //把链表中last元素赋给l ,如果是第一个元素则为null final Node<E> l = last; //把元素封装为一个Node对象 final Node<E> newNode = new Node<>(l, e, null); //把链表的last元素指向新的元素 last = newNode; //如果添加的是第一个元素 if (l == null){ //把链表的first元素指向新的元素 first = newNode; } //如果添加的不是第一个元素 else{ //把l的下一个元素指向新的元素 l.next = newNode; } //集合中元素数量加1 size++; modCount++; } void linkBefore(E e, Node<E> succ) { final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } //获取元素 public E get(int index) { //检测元素索引 checkElementIndex(index); return node(index).item; } //返回指定元素索引处的(非空)节点 Node<E> node(int index) { //把链表分为两段,如果index在链表的前段,则从前往后找,否则反之 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } //省略。。。 }
总结:
ArrayList和LinkedList本质上每次插入和删除元素都会进行复制数组的操作,如果ArrayList插入操作没有触发数组扩容操作,并且在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。
JDK1.7中在调用构造器的时候数组长度就初始化为了10,JDK1.8则是在调用add方法后才创建数组长度为10的新数组替换默认空数组。
ArrayList和LinkedList都不是线程安全的。然而Vector类被认为是过时的废弃的了。
方案一: Collections.synchronizedList(); 得到一个线程安全的 ArrayList。
方案二: concurrent 并发包下的 CopyOnWriteArrayList 类。
CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。
4.4 为什么Java的Vector类被认为是过时的或者废弃的
Vector中对每一个独立操作都实现了同步,这通常不是我们想要的做法。对单一操作实现同步通常不是线程安全的(举个例子,比如你想遍历一个Vector实例。你仍然需要申明一个锁来防止其他线程在同一时刻修改这个Vector实例。如果不添加锁的话
通常会在遍历实例的这个线程中导致一个ConcurrentModificationException)同时这个操作也是十分慢的(在创建了一个锁就已经足够的前提下,为什么还需要重复的创建锁)
当然,即使你不需要同步,Vector也是有锁的资源开销的。