面试专栏|一篇文章搞定ArrayList和LinkedList所有面试问题

172 阅读10分钟
原文链接: click.aliyun.com

在面试中经常碰到:ArrayList和LinkedList的特点和区别?

个人认为这个问题的回答应该分成这几部分:

  1. 介绍ArrayList底层实现

  2. 介绍LinkedList底层实现

  3. 两者个适用于哪些场合

本文也是按照上面这几部分组织的。

ArrayList的源码解析

成员属性源码解析

  1. public class ArrayList<E>

  2. extends AbstractList<E>

  3. implements List<E>, RandomAccess

  4. ,Cloneable , java.io.Serializable {

  5. private static final long

  6. serialVersionUID

  7. = 8683452581122892189L ;


  8. //默认容量是10

  9. private static final int

  10. DEFAULT_CAPACITY = 10;


  11. //当传入ArrayList构造器的容量为0时

  12. //用这个数组表示:容器的容量为0

  13. private static final Object []

  14. EMPTY_ELEMENTDATA = {};

接上面

  1. /*

  2. 主要作为一个标识位,在扩容时区分:

  3. 默认大小和容量为0,使用默认容量时采取的

  4. 是“懒加载”:即等到add元素的时候才进行实际

  5. 容量的分配,后面扩容函数讲解还会提到这点

  6. */

  7. private static final Object[]

  8. DEFAULTCAPACITY_EMPTY_ELEMENTDATA={};


  9. //ArrayList底层使用Object数组保存的元素

  10. transient Object[] elementData;


  11. //记录当前容器中有多少元素

  12. private int size;

构造器源码解析

  1. /*

  2. 最常用的构造器之一,实际上就是创建了一个

  3. 指定大小的Object数组来保存之后add的元素

  4. */

  5. public ArrayList(int initialCapacity){

  6. if (initialCapacity > 0) {

  7. //初始化保存数据的Object数组

  8. this .elementData

  9. =new Object[initialCapacity];

  10. } else if(initialCapacity==0) {

  11. //标识容量为0:EMPTY_ELEMENTDATA

  12. this .elementData

  13. = EMPTY_ELEMENTDATA;

  14. } else {

  15. throw new

  16. IllegalArgumentException (

  17. "Illegal Capacity: " +

  18. initialCapacity);

  19. }

  20. }


  21. /*

  22. 无参构造器,指向的是默认容量大小的Object

  23. 数组,注意使用无参构造函数的时候并没有

  24. 直接创建容量 为10(默认容量是10)的Object

  25. 数组,而是采取懒加载的策略:使用

  26. DEFAULTCAPACITY_EMPTY_ELEMENTDATA,

  27. 这个默认数组的容量是0,所以得区分是

  28. 默认容量,还是你传给构造器的容量参数大小

  29. 本身就是0。在真正执行add操作时才会创建

  30. Object数组,即在扩容函数中有处理默认容量

  31. 的逻辑,后面会有详细分析。

  32. */

  33. public ArrayList() {

  34. //这个赋值操作仅仅是标识作用

  35. this .elementData =

  36. DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

  37. }


  38. //省略一部分不常用代码函数

add方法源码解析

  1. /*

  2. add是ArrayList最常用的接口,逻辑很简单

  3. */

  4. public boolean add(E e) {

  5. /*

  6. 主要用于标识线程安全,即ArrayList只能

  7. 在单线程环境下使用,在多线程环境下会出现并发

  8. 安全问题,modCount主要用于记录对ArrayList的

  9. 修改次数,如果一个线程操作ArrayList期间

  10. modCount发生了变化即,有多个线程同时修改当前

  11. 这个ArrayList,此时会抛出

  12. “ConcurrentModificationException”异常,

  13. 这又被称为“failFast机制”,在很多非线程安全的

  14. 类中都有failFast机制:HashMap、 LinkedList

  15. 等。这个机制主要用于迭代器、加强for循环等相关

  16. 功能,也就是一个线程在迭代一个有failfast机制

  17. 容器的时候,如果其他线程改变了容器内的元素,

  18. 迭代的这个线程会抛

  19. 出“ConcurrentModificationException”异常

  20. */

  21. modCount++;


  22. /*

  23. add操作的核心函数,当使用无参构造器时并没有

  24. 直接分配大小为10的Object数组,这里面有对应 的处理逻辑。

  25. */

  26. //进入该函数

  27. add(e, elementData, size);

  28. return true;

  29. }


  30. private void add(E e,Object[] elementData

  31. , int s) {

  32. /*

  33. 如果使用无参构造器:开始时length为0,

  34. s也为0.grow()核心函数,扩容/初始化操作

  35. */

  36. if (s == elementData.length)

  37. elementData = grow();

  38. elementData[s] = e;

  39. size = s + 1 ;

  40. }

grow相关方法源码解析

  1. private Object[] grow() {

  2. //继续追踪

  3. return grow(size + 1);

  4. }


  5. private Object[] grow(int minCapacity){

  6. /*

  7. 使用数组复制的方式,扩容:将elementData

  8. 所有元素复制到一个新数组中,这个新数组的

  9. 长度是newCapacity()函数的返回值,之后再

  10. 把这个新数组赋值给elementData,完成扩容

  11. 操作

  12. */

  13. //进入newCapacity()函数

  14. return elementData =

  15. Arrays .copyOf(elementData,

  16. newCapacity(minCapacity));

  17. }


  18. //返回的是扩容后数组的长度

  19. private int newCapacity(int minCapacity){

  20. int oldCapacity=elementData.length;

  21. //扩容后的容量为原来容量的1.5倍

  22. int newCapacity = oldCapacity

  23. + (oldCapacity >> 1);

  24. if (newCapacity-minCapacity <=0){

  25. if (elementData ==

  26. DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

  27. //默认容量的处理

  28. return Math.max(

  29. DEFAULT_CAPACITY, minCapacity);


  30. /*

  31. minCapacity是int类型,有溢出的可能,也就

  32. 是ArrayList最大大小是Integer.MAX_VALUE

  33. */

  34. if (minCapacity<0) //overflow

  35. throw new OutOfMemoryError();


  36. //返回新容量

  37. return minCapacity;

  38. }


  39. /*

  40. MAX_ARRAY_SIZE=Integer.MAX_VALUE-8,

  41. 当扩容后大于MAX_ARRAY_SIZE ,返回

  42. hugeCapacity(minCapacity),

  43. 其实就是Integer.MAX_VALUE

  44. */

  45. return (newCapacity-MAX_ARRAY_SIZE

  46. <= 0 )? newCapacity

  47. : hugeCapacity(minCapacity);

  48. }


  49. private static int hugeCapacity

  50. (int minCapacity){

  51. if (minCapacity < 0) // overflow

  52. throw new OutOfMemoryError();

  53. return (minCapacity>MAX_ARRAY_SIZE)

  54. ? Integer .MAX_VALUE

  55. : MAX_ARRAY_SIZE;

  56. }

ArrayList的failfast机制

  1. //最后看下ArrayList的failFast机制

  2. private class Itr implements

  3. Iterator <E>{

  4. //index of next element to return

  5. int cursor;

  6. // index of last element returned;

  7. int lastRet = -1; -1 if no such

  8. /*

  9. 在迭代之前先保存modCount的值,

  10. modCount在改变容器元素、容器

  11. 大小时会自增加1

  12. */

  13. int expectedModCount=modCount;


  14. // prevent creating a synthetic

  15. // constructor

  16. Itr () {}


  17. public boolean hasNext() {

  18. return cursor != size;

  19. }


  20. @SuppressWarnings ("unchecked")

  21. public E next() {

  22. /*

  23. 使用迭代器遍历元素的时候先检查

  24. modCount的值是否等于预期的值,

  25. 进入该函数

  26. */

  27. checkForComodification();

  28. int i = cursor;

  29. if (i >= size)

  30. throw new

  31. NoSuchElementException ();

  32. Object [] elementData =

  33. ArrayList .this.elementData;

  34. if (i >= elementData.length)

  35. throw new

  36. ConcurrentModificationException ();

  37. cursor = i + 1;

  38. return (E)elementData[lastRet=i];

  39. }


  40. /*

  41. 可以发现:在迭代期间如果有线程改变了

  42. 容器,此时会抛出

  43. “ConcurrentModificationException”

  44. */

  45. final void checkForComodification(){

  46. if (modCount!=expectedModCount)

  47. throw new

  48. ConcurrentModificationException ();

  49. }

ArrayList的其他操作,比如:get、remove、indexOf其实就很简单了,都是对Object数组的操作:获取数组某个索引位置的元素,删除数组中某个元素,查找数组中某个元素的位置......所以说理解原理很重要。

上面注释的部分就是ArrayList的考点,主要有:**初始容量、最大容量、使用Object数组保存元素(数组与链表的异同)、扩容机制(1.5倍)、failFast机制等。

LinkedList源码分析

成员属性源码分析

  1. public class LinkedList<E>

  2. extends AbstractSequentialList<E>

  3. implements List<E>, Deque<E>

  4. ,Cloneable , java.io.Serializable {

  5. MAX_ARRAY_SIZE= Integer.MAX_VALUE-8,

  6. /*

  7. LinkedList的size是int类型,但是后面

  8. 会看到LinkedList大小实际只受内存大小

  9. 的限制也就是LinkedList的size大小可能

  10. 发生溢出,返回负数

  11. */

  12. transient int size = 0;


  13. //LinkedList底层使用双向链表实现,

  14. //并保留了头尾两个节点的引用

  15. transient Node<E> first;//头节点


  16. transient Node<E> last;//尾节点

  17. //省略一部分无关代码


  18. //下面分析LinkedList内部类Node

内部类Node源码分析

  1. private static class Node<E> {

  2. E item;//元素值

  3. Node <E> next;//后继节点


  4. //前驱节点,即Node是双向链表

  5. Node <E> prev;


  6. Node (Node<E> prev, E element

  7. , Node <E> next) {//Node的构造器

  8. this .item = element;

  9. this .next = next;

  10. this .prev = prev;

  11. }

  12. }

构造器源码分析

  1. //LinkedList无参构造器:什么都没做

  2. public LinkedList() {}

其他核心辅助接口方法源码分析

  1. /*

  2. LinkedList的大部分接口都是基于

  3. 这几个接口实现的:

  4. 1.往链表头部插入元素

  5. 2.往链表尾部插入元素

  6. 3.在指定节点的前面插入一个节点

  7. 4.删除链表的头结点

  8. 5.删除除链表的尾节点

  9. 6.删除除链表中的指定节点

  10. */

  11. //1.往链表头部插入元素

  12. private void linkFirst(E e) {

  13. final Node<E> f = first;

  14. final Node<E> newNode =

  15. new Node<>(null, e, f);

  16. first = newNode;

  17. if (f == null)

  18. last = newNode;

  19. else

  20. f.prev = newNode;

  21. size++;

  22. modCount++;//failFast机制

  23. }


  24. //2.往链表尾部插入元素

  25. void linkLast(E e) {

  26. final Node<E> l = last;

  27. final Node<E> newNode =

  28. new Node<>(l, e, null);

  29. last = newNode;

  30. if (l == null)

  31. first = newNode;

  32. else

  33. l.next = newNode;

  34. size++;

  35. modCount++;//failFast机制

  36. }


  37. //3.在指定节点(succ)的前面插入一个节点

  38. void linkBefore(E e, Node<E> succ) {

  39. // assert succ != null;

  40. final Node<E> pred = succ.prev;

  41. final Node<E> newNode

  42. = new Node<>(pred, e, succ);

  43. succ.prev = newNode;

  44. if (pred == null)

  45. first = newNode;

  46. else

  47. pred.next = newNode;

  48. size++;

  49. modCount++;//failFast机制

  50. }


  51. //4.删除链表的头结点

  52. private E unlinkFirst( Node<E> f){

  53. //assert f==first && f!=null;

  54. final E element = f.item;

  55. final Node<E> next = f.next;

  56. f.item = null ;

  57. f.next = null ; //help GC

  58. first = next;

  59. if (next == null)

  60. last = null ;

  61. else

  62. next.prev = null;

  63. size--;

  64. modCount++;//failFast机制

  65. return element;

  66. }


  67. //5.删除除链表的尾节点

  68. private E unlinkLast( Node<E> l) {

  69. //assert l==last && l!=null;

  70. final E element = l.item;

  71. final Node<E> prev = l.prev;

  72. l.item = null ;

  73. l.prev = null ; // help GC

  74. last = prev;

  75. if (prev == null)

  76. first = null ;

  77. else

  78. prev.next = null;

  79. size--;

  80. modCount++;//failFast机制

  81. return element;

  82. }


  83. //6.删除除链表中的指定节点

  84. E unlink(Node <E> x) {

  85. // assert x != null;

  86. final E element = x.item;

  87. final Node<E> next = x.next;

  88. final Node<E> prev = x.prev;


  89. if (prev == null) {

  90. first = next;

  91. } else {

  92. prev.next = next;

  93. x.prev = null ;

  94. }


  95. if (next == null) {

  96. last = prev;

  97. } else {

  98. next.prev = prev;

  99. x.next = null ;

  100. }


  101. x.item = null ;

  102. size--;

  103. modCount++;//failFast机制

  104. return element;

  105. }

常用API源码分析

  1. //LinkedList常用接口的实现 public E removeFirst() {

  2. final Node<E> f = first;

  3. if (f == null)

  4. throw

  5. new NoSuchElementException();

  6. //调用 4.删除链表的头结点 实现

  7. return unlinkFirst(f);

  8. }


  9. public E removeLast() {

  10. final Node<E> l = last;

  11. if (l == null)

  12. throw

  13. new NoSuchElementException();

  14. //调用 5.删除除链表的尾节点 实现

  15. return unlinkLast(l);

  16. }


  17. public void addFirst(E e) {

  18. //调用 1.往链表头部插入元素 实现

  19. linkFirst(e);

  20. }


  21. public void addLast(E e) {

  22. //调用 2.往链表尾部插入元素 实现

  23. linkLast(e);

  24. }


  25. public boolean add(E e) {

  26. //调用 2.往链表尾部插入元素 实现

  27. linkLast(e);

  28. return true;

  29. }


  30. public boolean remove(Object o) {

  31. if (o == null) {

  32. for (Node<E> x = first;

  33. x != null ; x = x.next) {

  34. if (x.item == null) {

  35. //调用 6.删除除链表中的

  36. //指定节点 实现

  37. unlink(x);

  38. return true;

  39. }

  40. }

  41. } else {

  42. for (Node<E> x = first

  43. ; x != null ; x = x.next) {

  44. if (o.equals(x.item)) {

  45. //调用 6.删除除链表中的

  46. //指定节点 实现

  47. unlink(x);

  48. return true;

  49. }

  50. }

  51. }

  52. return false;

  53. }


  54. //省略其他无关函数

failfast机制

  1. //迭代器中的failFast机制 private class ListItr

  2. implements ListIterator<E> {

  3. private Node<E> lastReturned;

  4. private Node<E> next;

  5. private int nextIndex;


  6. /*

  7. 在迭代之前先保存modCount的值,

  8. modCount在改变容器元素、容器大小时

  9. 会自增加1

  10. */

  11. private int expectedModCount

  12. = modCount;


  13. ListItr (int index) {

  14. next = (index == size)

  15. ? null : node(index);

  16. nextIndex = index;

  17. }


  18. public boolean hasNext() {

  19. return nextIndex < size;

  20. }


  21. public E next() {

  22. /*

  23. 使用迭代器遍历元素的时候先检查

  24. modCount的值是否等于预期的值,

  25. 进入该函数

  26. */

  27. checkForComodification();

  28. if (!hasNext())

  29. throw

  30. new NoSuchElementException();


  31. lastReturned = next;

  32. next = next.next;

  33. nextIndex++;

  34. return lastReturned.item;

  35. }


  36. /*

  37. 可以发现:在迭代期间如果有线程改变了容器,

  38. 此时会抛出

  39. “ConcurrentModificationException”

  40. */

  41. final void checkForComodification(){

  42. if (modCount!=expectedModCount)

  43. throw new

  44. ConcurrentModificationException ();

  45. }

LinkedList的实现较为简单: 底层使用双向链表实现、保留了头尾两个指针 、LinkedList的其他操作基本都是基于上面那六个函数实现的,另外LinkedList也有 failFast 机制,这个机制主要在迭代器中使用。

数组和链表各自的特性

数组和链表的特性差异,本质是:连续空间存储和非连续空间存储的差异。主要有下面两点:

ArrayList:底层是Object数组实现的:由于数组的地址是连续的,数组支持O(1)随机访问;数组在初始化时需要指定容量;数组不支持动态扩容,像ArrayList、Vector和Stack使用的时候看似不用考虑容量问题(因为可以一直往里面存放数据);但是它们的底层实际做了扩容;数组扩容代价比较大,需要开辟一个新数组将数据拷贝进去,数组扩容效率低;适合读数据较多的场合。

LinkedList:底层使用一个Node数据结构,有前后两个指针,双向链表实现的。相对数组,链表插入效率较高,只需要更改前后两个指针即可;另外链表不存在扩容问题,因为链表不要求存储空间连续,每次插入数据都只是改变last指针;另外,链表所需要的内存比数组要多,因为他要维护前后两个指针;它适合删除,插入较多的场景LinkedList还实现了Deque接口。


原文发布时间为: 2018-11-30
本文作者:大菜鸟
本文来自云栖社区合作伙伴“ Java技术驿站”,了解相关信息可以关注“ Java技术驿站”。