源码面经Java源码系列-ArrayList与LinkedList
Posted 「已注销」
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码面经Java源码系列-ArrayList与LinkedList相关的知识,希望对你有一定的参考价值。
- ArrayList的大小是如何自动增加的
- 什么情况下你会使用ArrayList?什么时候你会选择LinkedList?
- 如何复制某个ArrayList到另一个ArrayList中去
- 在索引中ArrayList的增加或者删除某个对象的运行过程?效率很低吗?解释一下为什么?
- ArrayList插入删除一定慢么?
- ArrayList的遍历和LinkedList遍历性能比较如何?
- ArrayList是线程安全的么?
- ArrayList如何remove
不想看源码解析的同学,可以直接去最下方查看答案
源码解析
List是最简单的线性数据结构,Java在最上层提供了List接口,然后通过AbstractList实现了List接口。
ArrayList和LinkedList是Java中最常用的List实现类。
ArrayList底层是由数组实现的,相当于动态数组。LinkedList底层相当于链表的实现方式。
下面我们开始分析源码
ArrayList
底层数据结构
/** * 默认初始化的数组大小 */ private static final int DEFAULT_CAPACITY = 10; /** * 所有ArrayList实例共享的空list实例 */ private static final Object[] EMPTY_ELEMENTDATA = ; /** * 使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA与EMPTY_ELEMENTDATA区分,标识 * elementData数组是通过默认构造方法创建的空数组,并且还没有向其中添加 * 元素 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ; /** * 底层用来真实存储数据的数组,在调用ArrayList默认构造方法时,该数组会被 * 赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。直到第一次向数组中添加元素时, * 会被扩容为DEFAULT_CAPACITY */ transient Object[] elementData; /** * 数组中元素的数量 */ private int size; /** * 可以分配的最大数组大小。某些VM在数组中保留一些header words。 * 尝试分配更大的数组可能会导致OutOfMemoryError */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 复制代码
构造方法
/** * 通过给定的初始容量创建一个空list */ public ArrayList(int initialCapacity) if (initialCapacity > 0) // 如果指定的容量大于0,就新建一个数组 this.elementData = new Object[initialCapacity]; else if (initialCapacity == 0) // 如果指定的容量等于0,那么就是一个空数组 this.elementData = EMPTY_ELEMENTDATA; else throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); /** * 不指定初始容量,创建一个容量为10的空数组 */ public ArrayList() // 使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA标识elementData数组 // 是通过默认构造方法创建的,在第一向ArrayList添加元素时,会进行 // 扩容,而扩充的容量为DEFAULT_CAPACITY(10) this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; /** * 创建一个ArrayList,包含给定集合c中的所有元素,顺序即为c迭代器遍历的顺序。 * * @throws NullPointerException 如果c为null,抛出NPE */ public ArrayList(Collection<? extends E> c) elementData = c.toArray(); if ((size = elementData.length) != 0) // 如果给定集合c的size不为0 // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) // c.toArray()并不一定返回Object[]类型,如果返回的不是 // Object[]类型,就调用Arrays的复制方法变为Object类型 elementData = Arrays.copyOf(elementData, size, Object[].class); else // 如果给定集合c的size为0,那么就构造一个空数组 this.elementData = EMPTY_ELEMENTDATA; 复制代码
动态扩容
/** * 修剪elementData多余的槽,使ArrayList的capacity修剪为当前的size */ public void trimToSize() modCount++; if (size < elementData.length) elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); /** * 增加ArrayList的capacity,确保elementData的容量最少能支持 * minCapacity * * @param minCapacity 需要的最小容量 */ public void ensureCapacity(int minCapacity) // 1. 如果ArrayList不是通过默认构造方式构造的,或者 // ArrayList中已经添加过元素了,则minExpand为0, // 2. 如果ArrayList是通过默认构造方式构造的,且从未 // 添加过任何元素,那么minExpand就为默认的初始化 // 容量DEFAULT_CAPACITY(10) int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY; if (minCapacity > minExpand) // 如果需要扩容 ensureExplicitCapacity(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++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); /** * @param minCapacity 需要的最小容量 */ private void grow(int minCapacity) // overflow-conscious code int oldCapacity = elementData.length; // newCapacity = 1.5 * oldCapacity int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 对elementData进行扩容,然后将elementData原有的内容, // 复制到扩容后的数组中 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; 复制代码
插入/替换
/** * 添加元素e到list尾部 */ public boolean add(E e) // 检查是否需要扩容,并将modCount + 1 ensureCapacityInternal(size + 1); elementData[size++] = e; return true; /** * 在指定index插入元素element,并将原先在index位置及右方元素向右移动一位 */ public void add(int index, E element) // 检查index是否有效 rangeCheckForAdd(index); // 检查是否需要扩容,并将modCount + 1 ensureCapacityInternal(size + 1); // 把index及index右面的元素全部向右移动一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; /** * 将给定的集合c中的所有元素按照c迭代器的顺序添加到list的末尾。 * <p> * 在遍历集合c的过程中,如果对c进行了修改,那么会产生未定义的现象。 */ public boolean addAll(Collection<? extends E> c) Object[] a = c.toArray(); int numNew = a.length; // 扩容,并修改modCount ensureCapacityInternal(size + numNew); // 将数组a复制到数组elementData尾部 System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; /** * 将给定的集合c中的所有元素按照c迭代器的顺序添加到list的index位置。 * 并把index及index之后的元素的位置向后移动n个位置,n为集合c的大小。 */ public boolean addAll(int index, Collection<? extends E> c) rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; // 扩容并修改modCount ensureCapacityInternal(size + numNew); int numMoved = size - index; if (numMoved > 0) // 如果index小于size,即为在之前的元素中间插入,所以要把index及之后的 // 元素向右移动numNew位 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); // 将数组a的元素,复制到elementData数组中 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; /** * 用element替换下标为index的元素,并返回之前的元素 */ public E set(int index, E element) // 检查index是否超过限制 rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; 复制代码
删除
/** * 删除指定下标的元素,并将该下标右边的元素全部向左移动一位 */ public E remove(int index) rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 将index+1及之后的元素全部向左移动一位 System.arraycopy(elementData, index + 1, elementData, index, numMoved); elementData[--size] = null; return oldValue; /** * 如果list中包含一个或多个元素o,那么删除第一个o(下标最小的),并将第一个o对应 * 下标右边的元素全部向左移动一位。 * <p> * 如果list中不包含元素o,那么不做任何改变 */ public boolean remove(Object o) if (o == null) for (int index = 0; index < size; index++) if (elementData[index] == null) fastRemove(index); return true; else for (int index = 0; index < size; index++) if (o.equals(elementData[index])) fastRemove(index); return true; return false; /** * 快速删除index对应的元素,不需要进行范围检查,因为调用该方法时 * 已经可以保证index绝对有效 */ 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; // clear to let GC do its work /** * 清空elementData中的所有元素的引用 */ public void clear() modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) // 帮助GC elementData[i] = null; size = 0; /** * 移除下标在 [fromIndex, toIndex) 之间的元素,并将toIndex和之后 * 的元素全部向左移动至fromIndex的位置 */ protected void removeRange(int fromIndex, int toIndex) modCount++; int numMoved = size - toIndex; // 将toIndex和之后的元素向左移动至fromIndex的位置 System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // 计算新的数组的大小 int newSize = size - (toIndex - fromIndex); // 将多余的元素引用置为null,帮助GC for (int i = newSize; i < size; i++) elementData[i] = null; size = newSize; /** * 从list中删除指定集合c中包含的所有元素。 */ public boolean removeAll(Collection<?> c) Objects.requireNonNull(c); return batchRemove(c, false); /** * 保留在list中且在指定集合c中包含的所有元素 */ public boolean retainAll(Collection<?> c) Objects.requireNonNull(c); return batchRemove(c, true); /** * @param complement false-remove,true-retain */ private boolean batchRemove(Collection<?> c, boolean complement) final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try // 双指针遍历list for (; r < size; r++) // c.contains(elementData[r])判断给定集合c中是否存 // 在r指针对应的元素。 // 如果complement为false,代表remove集合c中的元素。 // 如果complement为true,代表retain集合c中的元素。 if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; finally // 如果c.contains抛出异常,仍能保证与AbstractCollection相同的兼容性 if (r != size) // 如果r指针没有遍历完数组,就把r指针未遍历的元素,复制到r指针 // 之后,因为r指针-w指针之间的元素应该被移除 System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; if (w != size) // 清空不用的元素引用,帮助GC for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; return modified; 复制代码
遍历
/** * 返回list中第一次出现给定元素o的下标,如果不存在元素o * 则返回-1 */ public int indexOf(Object o) // 将o为null和非null的情况分开做处理 if (o == null) for (int i = 0; i < size; i++) if (elementData[i] == null) return i; else for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; return -1; /** * 返回list中最后一次出现给定元素o的下标,如果不存在元素o * 则返回-1 */ public int lastIndexOf(Object o) // 与indexOf实现方式相同,只是把遍历的方向 // 改为从后向前遍历 if (o == null) for (int i = size - 1; i >= 0; i--) if (elementData[i] == null) return i; else for (int i = size - 1; i >= 0; i--) if (o.equals(elementData[i])) return i; return -1; /** * 返回下标为index的元素 */ @SuppressWarnings("unchecked") E elementData(int index) return (E) elementData[index]; /** * 返回下标为index的元素 */ public E get(int index) // 检查index是否超过限制 rangeCheck(index); return elementData(index); @Override public void forEach(Consumer<? super E> action) Objects.requireNonNull(action); final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; // forEach的内部实现其实就是遍历内部elementData数组, // 然后对每个元素进行action.accept操作。 // 遍历过程中要比较modCount是否发生变化,如果发生了变化, // 会抛出ConcurrentModificationException,快速失败 for (int i = 0; modCount == expectedModCount && i < size; i++) action.accept(elementData[i]); if (modCount != expectedModCount) throw new ConcurrentModificationException(); 复制代码
迭代器
private class Itr implements Iterator<E> int cursor; // 下一个要访问的元素的下标 int lastRet = -1; // 上一次访问的元素的下标,如果没有则为-1 int expectedModCount = modCount; // 是否包含下一个元素 public boolean hasNext() return cursor != size; @SuppressWarnings("unchecked") public E next() checkForComodification(); int i = cursor; // 如果要访问的下标大于最大长度,则抛出异常 if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); // cursor后移 cursor = i + 1; // 返回下标为i的元素,并将lastRet置为i return (E) elementData[lastRet = i]; public void remove() // 如果上一个访问的元素不存在,则抛出异常 if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try // 删除上一个访问的元素 ArrayList.this.remove(lastRet); // 重置下标 cursor = lastRet; // 因为上一个访问的元素已经删除了,所以不存在了 // 要把lastRet置为-1 lastRet = -1; expectedModCount = modCount; catch (IndexOutOfBoundsException ex) throw new ConcurrentModificationException(); @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) return; final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); while (i != size && modCount == expectedModCount) consumer.accept((E) elementData[i++]); // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); // 防止并发冲突 final void checkForComodification() if (modCount != expectedModCount) throw new ConcurrentModificationException(); /** * 在Iterator的基础上,支持了双向遍历 */ private class ListItr extends Itr implements ListIterator<E> ListItr(int index) super(); cursor = index; public boolean hasPrevious() return cursor != 0; public int nextIndex() return cursor; public int previousIndex() return cursor - 1; /** * 与Iterator的next方法一样,只不过改成了向前遍历 */ @SuppressWarnings("unchecked") public E previous() checkForComodification(); int i = cursor - 1; if (i < 0) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i; return (E) elementData[lastRet = i]; public void set(E e) if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try ArrayList.this.set(lastRet, e); catch (IndexOutOfBoundsException ex) throw new ConcurrentModificationException(); public void add(E e) checkForComodification(); try int i = cursor; ArrayList.this.add(i, e); cursor = i + 1; // 向list添加元素后,上一次访问的元素就发生了变化, // 所以要将lastRet置为-1 lastRet = -1; expectedModCount = modCount; catch (IndexOutOfBoundsException ex) throw new ConcurrentModificationException(); 复制代码
其他
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable private static final long serialVersionUID = 8683452581122892189L; /** * 返回ArrayList的浅拷贝 */ public Object clone() try ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; catch (CloneNotSupportedException e) // this shouldn't happen, since we are Cloneable throw new InternalError(e); /** * 以数组的形式返回list中的所有元素 */ public Object[] toArray() return Arrays.copyOf(elementData, size); /** * 以数组的形式返回list中的所有元素, 数组的类型为T */ @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; /** * 检查index是否超出限制,需要检查index的原因是当list动态扩容时,会分配出 * 未使用的空间,访问时并不会报错。而size记录了当前真实使用的空间,所以 * 需要将index与size比较。 * <p> * 不检查index为负的情况,因为当index为负时,访问数组会抛出ArrayIndexOutOfBoundsException */ private void rangeCheck(int index) if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); /** * add and addAll 时检查是否index是否有效 */ private void rangeCheckForAdd(int index) // 与rangeCheck版本不同,这里index是可以等于size的,因为size的值 // 就是下一个要插入的下标值,所以index==size就相当于在list尾插入 // // 不知道为什么这里还判断了index < 0 的情况 if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); private String outOfBoundsMsg(int index) return "Index: " + index + ", Size: " + size; 复制代码
LinkedList
// 待定 复制代码
面试题解答
-
ArrayList的大小是如何自动增加的
当调用ArrayList的add, addAll等方法时,会调用ensureCapacityInternal方法检查底层数组容量是否满足所需容量,如果容量不够大,就调用grow方法将容量扩展至原来的1.5倍(一般情况下)
-
什么情况下你会使用ArrayList?什么时候你会选择LinkedList?
待定
-
如何复制某个ArrayList到另一个ArrayList中去
public class Node implements Cloneable public Node() public Object clone() Node node = null; try node = (Node) super.clone(); catch (CloneNotSupportedException e) e.printStackTrace(); return node; public static void main(String[] args) ArrayList<Node> srcList = new ArrayList<>(); srcList.add(new Node()); srcList.add(new Node()); for (Node node : srcList) System.out.println("srcList: " + node); /* --------------------- 浅复制 --------------------- */ System.out.println("浅复制"); // 循环遍历 List<Node> destList1 = new ArrayList<>(); srcList.forEach(src -> destList1.add(src)); System.out.println("循环遍历"); for (Node node : destList1) System.out.println("descList: " + node); // 构造方法 List<Node> destList2 = new ArrayList<>(srcList); System.out.println("构造方法"); for (Node node : destList2) System.out.println("descList: " + node); // addAll方法 List<Node> destList3 = new ArrayList<>(); destList3.addAll(srcList); System.out.println("addAll方法"); for (Node node : destList3) System.out.println("descList: " + node); // clone方法 List<Node> destList4 = (List<Node>) srcList.clone(); System.out.println("clone方法"); for (Node node : destList4) System.out.println("descList: " + node); // System.arraycopy Node[] destArray = new Node[srcList.size()]; System.arraycopy(srcList.toArray(), 0, destArray, 0, srcList.size()); System.out.println("System.arraycopy方法"); for (Node node : destArray) System.out.println("descList: " + node); /* --------------------- 深复制 --------------------- */ System.out.println("深复制"); // 改造后的clone方法 List<Node> destList5 = (List<Node>) srcList.clone(); System.out.println("改造后的clone方法"); for (Node node : destList5) System.out.println("descList: " + node.clone()); 复制代码
返回结果
srcList: Node@71bc1ae4 srcList: Node@6ed3ef1 ---------- 浅复制 ---------- 循环遍历 descList: Node@71bc1ae4 descList: Node@6ed3ef1 构造方法 descList: Node@71bc1ae4 descList: Node@6ed3ef1 addAll方法 descList: Node@71bc1ae4 descList: Node@6ed3ef1 clone方法 descList: Node@71bc1ae4 descList: Node@6ed3ef1 System.arraycopy方法 descList: Node@71bc1ae4 descList: Node@6ed3ef1 ---------- 深复制 ---------- 改造后的clone方法 descList: Node@17d99928 descList: Node@3834d63f 复制代码
-
ArrayList插入/删除一定慢吗
取决于插入与删除的位置
插入:在插入过程中会将index及之后的元素向后移动,如果插入的位置是数组靠后的位置。那么要移动的元素并不多,通过index直接访问的,操作并不会很慢。
删除:与插入相同,插入过程中会将index及之后的元素向前移动,如果位置靠后,移动的元素也不多。
-
ArrayList的遍历和LinkedList遍历性能比较如何?
待定
-
ArrayList是线程安全的么?
不是。对ArrayList的操作并没有做任何同步或者加锁的行为。可以看到对ArrayList的结构性操作中,都会对modCount值进行修改。这样在操作时,通过比较modCount可以实现fail-fast机制,在并发冲突时,抛出ConcurrentModificationException
-
ArrayList如何remove
public static void main(String[] args) List<Integer> list = new ArrayList<>(); list.add(1); list.add(1); list.add(2); list.add(2); list.add(3); list.add(3); List<Integer> list2 = new ArrayList<>(list); for (int i = 0; i < list2.size(); i++) if ((list2.get(i) % 2) == 0) list2.remove(i); System.out.println(list2); // [1, 1, 2, 3, 3] list2 = new ArrayList<>(list); Iterator<Integer> iterator = list2.iterator(); while (iterator.hasNext()) if ((iterator.next() % 2) == 0) iterator.remove(); System.out.println(list2); // [1, 1, 3, 3] list2 = new ArrayList<>(list); for (Integer data : list2) // ConcurrentModificationException if ((data % 2) == 0) list2.remove(data); System.out.println(list2); 复制代码
-
通过遍历下标的方式删除(×):
这种方式是错误的,有可能会遗漏元素。比如数组中的元素为[1, 1, 2, 2, 3, 3],当下标index为2时,对应的元素为第一个[2],满足条件删除后,数组中的元素变为[1, 1, 2, 3, 3],因为[2]被删除后,之后的[2, 3, 3]向左移动。在遍历中,index++ 变为3,对应的元素为[3],所以数组中第二个[2]被遗漏了,没有遍历到。
-
通过迭代器进行迭代(√):
这种方式是删除的正确方式
-
在forEach中进行删除(×):
这种方式是错误的,会抛出ConcurrentModificationException。原因在于ArrayList中的forEach方法:
-
final int expectedModCount = modCount; 在整个遍历之前会先记录modCount。 复制代码
-
在调用ArrayList的remove方法时,会通过modCount++对modCount值进行修改, 这时modCount与遍历前记录的modCount已经不一致了。 复制代码
-
for (int i = 0; modCount == expectedModCount && i < size; i++) action.accept(elementData[i]); 遍历每个元素时,会检查modCount是否与之前一致。而在remove方法中, 已经进行了修改,所以在删除元素后的下次遍历时会退出循环。 复制代码
-
if (modCount != expectedModCount) throw new ConcurrentModificationException(); 如果modCount与之前不一致了,就抛出ConcurrentModificationException 复制代码
-
-
源码系列Java中的数据结构——数组与ArrayList
文章目录
- 前言
- 关于本系列
- 一、指令与二进制数据
- 二、最常用的数据结构——数组
- 三、Java中的封装类ArrayList
- 四、总结
前言
自从上次字节面试凉了之后,我就一直有这个想法,想写个源码系列的博客。无奈最近事情太多,无法真正静下心来写。原本是想暑假来好好写这个系列,但因为下周要由我来负责协会授课,所以只能在这周写完。也好,毕竟只有ddl才有效率嘛(笑哭)。
关于本系列
作为本系列的第一篇文章,我想讲讲我的对于此系列的想法。
首先作为源码系列的文章,我肯定会讲讲源码,当然我也只能是作为一个菜鸟的身份,带着大家一起阅读源码,顺便说说我自己的理解。
当然了,我也不想仅限于源码,我更希望达到一种效果——让概念与实践融合。
所以在讲源码之前,我会把这个数据结构的概念通讲一遍,在这个过程中我也会尽量抛出一些问题,来帮助大家思考理解。
当然,本人技术水平有限,大佬勿喷。
注:本系列的jdk版本为1.8
一、指令与二进制数据
在正式介绍今天的数据结构之前,我想让大家切换一下视角,来到硬件底层来看看我们的程序。
相信大家也都听过,数据结构和算法课上老师一定会说的一句话——“程序就是算法和数据结构”,当然我也相信听了这话的你一定是云里雾里的,感觉很高深,很对,但又说不来怎么个对法。
其实从硬件层面上来看就不难理解了。
在硬件层面,算法对应的就是cpu的一条条指令,数据结构便是对应内存中二进制数字(的存储方式),而计算机最核心的操作不就是拿着二进制数据去执行一条条指令吗?
那么问题来了,我们该如何去拿到内存中的数据呢?
这么多的数据我们又该如何确定数据存储的位置呢?
我们自然而然想到需要有个类似地址的东西来描述内存中数据的位置,这样数据才能被准确访问。
而这个位置便叫做物理地址。
由此引出指针这个概念,指针存储的就是数据的地址,当然这个地址和上面说的物理地址还是有些不同的,指针存储的一般是虚拟地址。
不过呢,现在你只需要知道两件事——
1.数据是通过地址来进行访问的
2.指针存储的就是数据的地址,计算机可以通过指针的地址访问该地址的数据
为什么要讲上面这些内容呢,这是为了让你更好的理解下面的内容。
二、最常用的数据结构——数组
1.理解数组原理
首先,我们来看看数组的声明与创建:
int[] nums=new int[5];
这是一条最简单的用Java写的数组声明与创建。
它主要干了两件事:
1. 声明了一个int类型的一维数组
2. 给这个数组分配了五个int长度的存储空间
那如果我想访问数组中第3个元素怎么办呢?
你只需要写上
nums[2]
即可访问。
那我们来仔细思考一个问题——为什么它就能访问到该数组的第三个元素呢?
换句话说,我们为什么只写nums[2]就能确定第二个元素的确切地址呢?
其实啊,nums数组本身便是一个指针(PS:Java中没有指针这个概念,但实际上除了基本类型外,处处都是指针,具体可以看这篇Java中有没有指针。当然官方叫做句柄,但是我觉得指针更为贴切一点),它指向数组0号元素的地址,而我们之前又声明了数组的类型,再加上数组下标,我们不难算出第三个元素地址为
nums地址+int字节数*2。
专业描述就是:
a[i]_address = base_address + i * data_type_size
(图片来源于极客时间数据结构与算法之美)
而我们常说的——数组支持随机访问,根据下标随机访问的时间复杂度为 O(1),相信大家也就不难理解了吧。
有了数组,我们可以方便的存储同种类型元素,而不用为每个元素进行烦人的命名。
但数组有没有什么不方便的地方呢?
2.数组的优缺点
优点
1.支持随机访问
由于数组顺序存储,所以知道下标即可算出地址,这样根据下标访问数据的时间复杂度就为O(1)
2.同类型存储,避免烦人的命名
当然这个也是所有集合通用的优点
缺点(局限)
1.数组大小无法改变
由于数组元素在内存中的存储方式是连续存储,而大小在一开始便已经确定(这个也很好理解,不确定你怎么知道它之后的空间能不能分配给其他人),所以数组无法扩容。
而很多时候我们无法一开始就确定准确的数组大小,所以这对于某些情况就不太友好。
2.增加删除数组元素操作比较繁琐
除了在数组最后增加删除元素,其他时候增删数组元素都会牵扯到数组元素的移动,因为数组是顺序存储的,当中间的元素增加或删除时,后面的元素需要后移或前移。
总的来说,顺序存储的方式让数组支持随机访问,但也因此造成一些其他的局限。
三、Java中的封装类ArrayList
在理解完数组的原理和特点后,我们再来看看今天的主角——ArrayList。
它是Java对数组这个数据结构的封装,它封装了一些数组常用的操作,让操作更加简便,同时让数组看起来可以自动扩容。
好了,接下来开始我们的源码之旅。
源码阅读
1.源码阅读的方式
当然,在开始源码之前,我想啰嗦几句我关于源码研读的看法。
源码,在我看来就像一个巨大的迷宫,这对于体型庞大的jdk库更是如此。任何一个类都会牵扯到很多父类和接口,类中、类间方法的调用随处可见。如果你从头到尾阅读,那么你很容易迷失在这巨大的迷宫中。
读懂源码,走出迷宫的最好方式便是有一张地图(这个后面会讲)。然后根据地图,我们选择从我们熟悉的方法入手,慢慢研读,遇到不会的可以看看注释。如果还是不懂可以先跳过。很多时候我们首先要做的不是抓住每一个细节,而是看懂这个方法,看懂这个类大概是做什么的,做到心中有数,而很多时候方法的意思也能从方法名称中看出一二。
等你大致阅读了一遍明白了大概的框架后,再着眼细处也不迟。
所以接下来的源码之旅我也会据此展开。
2.ArrayList源码之旅
2.1 “迷宫地图”
ArrayList位于jdk的util包中,所以我们先打开ArrayList的源码。
打开idea,随便输个ArrayList,然后ctrl+点击,这样就进入了ArrayList的源码处,鼠标放置util处,右键选择图,选择第一个
这样我们就生成了第一张地图——继承关系图
是不是有点头晕,别急,这里你只需要大概记住继承关系即可。
然后返回ArrayList源码处,点击左侧的结构
这样我们就有了第二张地图,它也是我们本次的向导,将带领我们此次的源码之旅。
2.2 属性字段
首先我们要看的是属性字段
//版本号,这个可以忽略
private static final long serialVersionUID = 8683452581122892189L;
//默认容量,这里为10,后面会提到
private static final int DEFAULT_CAPACITY = 10;
//一个空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = ;
//另一个空的对象数组,为什么有两个呢?主要是为了区分后面的用法
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ;
//真正存储数据的数据,整个ArrayList就是围绕它展开的,transient表示该属性无法被序列化
transient Object[] elementData;
//当前的元素数量
private int size;
有人可能会觉得这会占很多空间,其实不是的,前面几个属性都是static final,即静态常量,它们并不会占用对象的内存空间,当你创建ArrayList时,真正占空间的是elementData数组和size属性。
2.3 常用方法
这里我们主要探究List接口的方法
2.3.1 ArrayList构造函数
/**
* 构造一个初始容量为10的空列表。
*/
public ArrayList()
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
/**
* 构造一个具有指定初始容量的空列表。
*/
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);
/**
* 构造一个列表,该列表包含指定集合的元素,其顺序由集合的迭代器返回。
*/
public ArrayList(Collection<? extends E> c)
Object[] a = c.toArray();
if ((size = a.length) != 0)
if (c.getClass() == java.util.ArrayList.class)
elementData = a;
else
elementData = Arrays.copyOf(a, size, Object[].class);
else
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
这里我们看到ArrayList有三种构造方法:
-
ArrayList():创建一个默认ArrayList对象。
这里有个很有意思的地方——它一开始并没有创建一个对象数组,而是把默认的空对象数组的指针赋给了它,至于注释里为什么说它是一个初始容量为10的地址呢,这个奥妙在于之后要讲的add方法。现在你只需要知道这里运用懒加载的技巧,所谓初始化并非给这个elementData 数组创建一个默认大小的对象数组,而是把一个空的对象数组指针赋给了它。尽管它这里是个空数组,但实际上你可以把它认为一个默认容量为10的对象数组。 -
ArrayList(int initialCapacity):这里创建一个大小为initialCapacity的ArrayList对象,这里有个细节的地方,当initialCapacity为0时,它是把之前那个空对象数组的指针赋给了它。
-
public ArrayList(Collection<? extends E> c):传入其他集合对象,调用toArray方法返回对象数组初始化一个ArrayList,其顺序由集合的迭代器决定。
2.3.2 add方法
这里有两个add方法,我们逐一来介绍。
①boolean add(E e)
第一个add是我最常用的往队尾添加一个元素,返回值表示添加是否成功、
public boolean add(E e)
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
添加一个元素到数组队尾,那么ensureCapacityInternal(size + 1)这句话干了什么呢?
点开方法可以看到
private void ensureCapacityInternal(int minCapacity)
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
calculateCapacity方法作用是计算当前需要的容量大小
private static int calculateCapacity(Object[] elementData, int minCapacity)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
return minCapacity;
从上面可以看到,它会判断当前的elementData 数组是不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,还记得之前说过ArrayList在执行空构造函数时的操作吗?就是让elementData 对象数组指向了DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这时候执行add方法,他就会进入这个if语句中,返回DEFAULT_CAPACITY(默认容量10)和minCapacity(当前数组所需的最小容量)的最大值。
这就是当时为什么说执行无参构造函数时,虽然只是让elementData 指向一个空对象数组,但实际上可以认为它就是默认容量为10的数组的原因。
回到ensureCapacityInternal(int minCapacity)这个方法上来,在计算完所需容量后它会把这个返回值当做参数传入ensureExplicitCapacity(int minCapacity)方法中。
private void ensureExplicitCapacity(int minCapacity)
//这个暂时不用管它,它是为了防止并发修改时出现错误
modCount++;
// 判断当前所需的容量是否大于elementData数组的长度,即判断是否需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
当minCapacity 大于数组的长度时,说明此时的数组已经装不下现在的元素了,这时候就需要扩容,调用grow(int minCapacity)方法。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity)
int oldCapacity = elementData.length;
//正常情况是原来的3/2倍。注:这里用>>会比用/快
int newCapacity = oldCapacity + (oldCapacity >> 1);
//当计算好的新容量小于当前所需容量时,直接变成最小所需容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//当新容量大于数组最大长度(Integer.MAX_VALUE - 8)时,执行hugeCapacity方法
if (newCapacity - MAX_ARRAY_SIZE > 0)
//这里会对边界情况作出处理,返回数组的边界大小
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf(最后会调用一个System.arraycopy方法)来进行数组复制,新数组大小为newCapacity,这样便实现了数组扩容
elementData = Arrays.copyOf(elementData, newCapacity);
这个方法就是扩容当前elementData ,它会把之前的elementData 复制并传入一个更大的数组中,并将elementData 的指针更新。而一般情况下扩容后的大小都是原来大小的3/2,除非到了边界或者当前所需的最小大小大于这个3/2,它会做出一些改变。
一句话来讲就是——grow方法会将当前的elementData 对象数组扩容到适合的大小,一般是原来的3/2倍。
回到最开始的问题——ensureCapacityInternal(size + 1)干了什么?
现在我们明白了,它是确保当前的数组容量能装下接下来的元素,如果不够它会选择扩容。
最后再执行
elementData[size++] = e;
即size自增1,然后将最后一个元素赋值为e。
这样add方法便完成了!
②add(int index, E element)
这个方法和上面类似,所以重复的部分我就不赘述了。
public void add(int index, E element)
//检查该index下标是否合法
rangeCheckForAdd(index);
//确保数组容量足够,如果不够则扩容
ensureCapacityInternal(size + 1);
//调用arraycopy方法,让index后面的元素全部后移一位,好让新的元素加进来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//赋值
elementData[index] = element;
//size自增1
size++;
和之前类似,做了以下几步:
- 检查index下标是否合法
- 确保数组容量足够,如果不够则扩容
- 调用arraycopy方法,让index后面的元素全部后移一位,好让新的元素加进来
- 给index下标出的元素赋值
- size++
这里我们看到每次插入需要做的便是将index下标后面的元素全部后移,所以效率不是特别高,比较麻烦。
2.3.3 remove方法
remove方法也有两种,一种是根据下标删除,一种是根据对象删除。
①E remove(int index)
根据数组下标删除元素
public E remove(int index)
//检查下标是否合法
rangeCheck(index);
//用于检查并发错误的标记变量++
modCount++;
//获取index下标的元素
E oldValue = elementData(index);
//index离最后一个元素的距离
int numMoved = size - index - 1;
//如果index后面存在元素则将index后面的元素前移一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将之前最后一位元素指向null
elementData[--size] = null;
//返回删除的对象
return oldValue;
这个方法说明看注释吧,注释说的很清楚了。
②boolean remove(Object o)
根据对象删除元素
public boolean remove(Object o)
//因为对象的判断需要调用equals方法,所以需要判断一下o是否为null
if (o == null)
//遍历所有元素
for (int index = 0; index < size; index++)
if (elementData[index] == null)
//快速删除index下标的元素
fastRemove(index);
return true;
else
for (int index = 0; index < size; index++)
if (o.equals(elementData[index]))
fastRemove(index);
return true;
return false;
这里就是一次遍历所有元素,如果找到该对象就删除。
不过可以看看这个fastRemove方法,
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;
可以看到这里和前面非常类似,只不过它这里没有进行边界检查。
2.3.4 get方法
我们知道ArrayList实际上就是数组的封装,所以对于具有随机访问的数组而言,根据下标访问元素实在太简单不过了。
public E get(int index)
//下标安全检查
rangeCheck(index);
return elementData(index);
2.3.5 set方法
修改index下标的元素为element
public E set(int index, E element)
//下标检查
rangeCheck(index);
//旧的值
E oldValue = elementData(index);
//更新值
elementData[index] = element;
//返回旧的值
return oldValue;
2.3.6 size方法
public int size()
return size;
其实就是把size返回,但这里要注意的是size的意义,它不是指当前ArrayList的容量有多少,而是指当前ArrayList中有多少元素。
2.3.7 indexOf方法
public int indexOf(Object o)
if (o == null)
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
else
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
return -1;
该方法就是遍历查找是否有对象o,如果找到则返回下标。
2.3.8 toArray方法
public Object[] toArray()
return Arrays.copyOf(elementData, size);
需要注意的是该方法是将其elementData对象数组从内存中复制了一份,而且长度就是元素的数量。所以不必担心修改toArray方法的得到的数组会影响原ArrayList对象。
2.3.9 removeAll和retainAll方法
①removeAll(Collection<?> c)
删除ArrayList中所有和集合中相同的元素
public boolean removeAll(Collection<?> c)
//检查该集合是否为空
Objects.requireNonNull(c);
return batchRemove(c, false);
该方法的核心在于boolean batchRemove(Collection<?> c, boolean complement)方法,我们点进去看一下,
private boolean batchRemove(Collection<?> c, boolean complement)
final Object[] elementData = this.elementData;
//r为快指针,w为慢指针
int r = 0, w = 0;
boolean modified = false;
try
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
//只有符合条件时,w才自增
elementData[w++] = elementData[r];
finally
// 这里正常情况都是r=size,只有在程序遇到异常时,才可能会遇到r!=size的情况。
if (r != size)
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
//当w!=size时,即有元素被抛弃了,这时需要把后面无用的元素值置为null
if (w != size)
// 将后面的元素值置为null
for (int i = w; i < size; i++)
elementData[i] = null;
//modcount加上修改过的元素
modCount += size - w;
//更新size
size = w;
//此时已经抛弃了一些元素,所以置为true
modified = true;
return modified;
可以看到这里用了快慢指针的技巧,当遇到要保留的元素时w再自增,最终保留元素的下标便是0到w-1。
在遍历的过程中调用contains方法看看这个元素是否在集合c中。
这里巧妙的地方在于它把contains的返回值complement参数比较。
这样,Boolean类型的complement参数的作用就类似于一个开关,当为true时,这个函数就是保留在集合c中出现过的元素,否则保留未出现过的元素。
②retainAll(Collection<?> c)
而retainAll也类似,只不过它把开关置为false,这样就会保留在集合c中出现过的元素。
public boolean retainAll(Collection<?> c)
Objects.requireNonNull(c);
return batchRemove(c, true);
2.4 迭代器
2.4.1 普通迭代器Itr
private class Itr implements Iterator<E>
//下一个元素下标
int cursor; // index of next element to return
//上一个元素下标
int lastRet = -1; // index of last element returned; -1 if no such
//期望修改次数(后面会讲,现在不用管)
int expectedModCount = modCount;
Itr()
//判断是否有下一个
public boolean hasNext源码系列Java中的数据结构——数组与ArrayList
Java集合系列:-----------03ArrayList源码分析