List集合源码分析
再次接触List集合,已经有一定的能力可以看看List的源码,接下来便分析一下。
ArrayList源码分析
-
构造方法
无参数构造方法:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // non-private to simplify nested class access public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
可以发现,ArrayList集合底层采用数组。当我们采用无参构造方法初始化的时候,默认会初始化一个空数组。
初始化容量构造方法:
private static final Object[] EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // non-private to simplify nested class access public ArrayList(int initialCapacity) { // 如果容量大于0,则创建一个容量为初始化值的数组,赋值给底层的数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 如果初始化数值为0,则赋值一个空对象 this.elementData = EMPTY_ELEMENTDATA; } else { // 小于零就直接抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
当初始化时ArrayList数组时,如果在参数部分填上数值,则代表初始化容量:
ArrayList<String> list = new ArrayList<>(18);
通过添加另一个集合的方式来初始化
// 官方注释: 数组列表的大小(它包含的元素数) // The size of the ArrayList (the number of elements it contains). private int size; private static final Object[] EMPTY_ELEMENTDATA = {}; public ArrayList(Collection<? extends E> c) { // 将传入的集合转化成数组 方便后面赋值给底层的数组 Object[] a = c.toArray(); // 判断传入数组的容量是否不等于0 , // 如果不等于0就把传入集合的元素数量传给ArrayList的计数变量 size if ((size = a.length) != 0) { // 判断传入的集合是否为ArrayList对象 如果时ArrayList直接将传入集合转化的数组赋值给底层数组 // 这里再JDK8有提示: // c.toArray might (incorrectly) not return Object[] (see 6260652) /* 因为看的是JDK17的源码 所以 ‘‘感觉’’下面判断好像貌似似乎可以不加 用JDK17好像复现不了jdk8的错误了 有兴趣的取百度搜一下 */ if (c.getClass() == ArrayList.class) { elementData = a; } else { // 不是ArrayList对象则拷贝传输的集合转化的数组给底层的数组 elementData = Arrays.copyOf(a, size, Object[].class); } } else { // replace with empty array. // 如果传入的集合元素数量为0,则初始化底层的数组为空数组 elementData = EMPTY_ELEMENTDATA; } }
Collection继承图:
-
添加元素
一个参数的添加元素
/* 官方解释:此列表在<i>结构上被修改的次数<i>。结构修改是指更改列表大小或以其他方式干扰列表,使正在进行的迭代可能会产生不正确结果的修改。<p>此字段由 {@code 迭代器} 和 {@code listIterator} 方法返回的迭代器和列表迭代器实现使用。如果此字段的值意外更改,迭代器(或列表迭代器)将抛出 {@code ConcurrentModificationException} 以响应 {@code next}、{@code remove}、{@code previous}、{@code set} 或 {@code add} 操作。这提供了<i>快速故障行为<i>,而不是在迭代期间面对并发修改时的非确定性行为。<p><b>子类使用此字段是可选的。<b>如果子类希望提供快速失败迭代器(并列出迭代器),则只需在其 {@code add(int, E)} 和 {@code remove(int)} 方法(以及它覆盖的任何其他导致列表结构修改的方法)中递增此字段。对 {@code add(int, E)} 或 {@code remove(int)} 的单个调用必须向此字段添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假的 {@code ConcurrentModificationExceptions}。如果实现不希望提供故障快速迭代器,则可以忽略此字段。 个人理解:避免在使用迭代器进行遍历的时候,有另外线程进行添加或者修改元素,从而出现不正确的结果 如果计算的结果modCount和预计的结果不相等就会出现并发修改异常。后面迭代遍历再说。 */ protected transient int modCount = 0; public boolean add(E e) { modCount++; // 调用添加元素的方法 add(e, elementData, size); // 默认返回值 true 插入成功与否都会提示true return true; } // 被调用添加的方法 private void add(E e, Object[] elementData, int s) { // 判断如果当前数组的实际容量如果和size大小一致 就调用grow()方法提升容量 // elementData虽然是存放数据的地方,但真正控制集合大小 还是由size来决定。 // 如果size 等于 底层数组的长度,就代表要进行扩容了 if (s == elementData.length) elementData = grow(); // 不需要扩容就进行赋值 elementData[s] = e; // 添加一个元素 size要加1 size = s + 1; } // ========== 添加容量的方法 =============== // 继续调用另一个方法 private Object[] grow() { return grow(size + 1); // 这里加1 ,是当前数组存下刚加入的元素需要的最小容量 } // 真正扩增容量的方法 /* 官方解释:增加容量以确保它至少可以容纳最小容量参数指定的元素数。 @param 最小容量 所需的最小容量 @throws内存不足错误 最小容量小于零时出错。 */ private static final int DEFAULT_CAPACITY = 10; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; private Object[] grow(int minCapacity) { // 现在的容量 = 现在底层数组的容量长度 int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { // 如果当前底层数组的长度为0 或者数组还是一个{} // 就给数组初始化长度10,或者当前底层数组的容量 return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } /** * 所以,当ArrayList用无参方法初始化时,默认容量为0。 当第一次添加时 如果添加的元素的数量小于10,则默认数量为10. 如果元素数量等于10时,再次添加容量会扩充到1.5倍。 通过构造方法添加元素时,容量变成构造方法的提供的容量/传入集合的数量, 下一次再添加时,容量扩充到1.5倍。 * **/ /* oldLength: 当前底层数组的容量 minGrowth: 所需要的最小容量 ==== 1 / 添加集合的长度 (当前集合需要添加的最小容量) prefGrowth: 首选增长量 (oldLength >> 1) 并不是每次添加都会出发这个机制,只有当底层数组容量不够支持再添加下一个元素时,会触发这个机制 */ public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // preconditions not checked because of inlining // assert oldLength >= 0 // assert minGrowth > 0 int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { return prefLength; } else { // put code cold in a separate method return hugeLength(oldLength, minGrowth); } } // 这里时避免集合数量达Math.max时 继扩容就是每次增长1 不进行1.5倍扩容 private static int hugeLength(int oldLength, int minGrowth) { int minLength = oldLength + minGrowth; if (minLength < 0) { // overflow throw new OutOfMemoryError( "Required array length " + oldLength + " + " + minGrowth + " is too large"); } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) { return SOFT_MAX_ARRAY_LENGTH; } else { return minLength; } } // 对ArrayList集合进行反射 ,查看真实的容量 private static int getArrayListLength(ArrayList<String> list) { Class<ArrayList> arrayListClass = ArrayList.class; try { //获取 elementData 字段 Field field = arrayListClass.getDeclaredField("elementData"); //开始访问权限 field.setAccessible(true); //把示例传入get,获取实例字段elementData的值 Object[] objects = (Object[]) field.get(list); //返回当前ArrayList实例的容量值 return objects.length; } catch (Exception e) { e.printStackTrace(); return -1; } } JDK17需要添加VM参数: --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
多个参数添加元素
搞懂上面的扩容机制,再往后看就简单很多了!
根据索引位置插入元素:
public void add(int index, E element) { // 检查所插入的索引是否合规 rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; // 这里是判断容量是否支持这次插入,如果不支持就进行扩容 if ((s = size) == (elementData = this.elementData).length) elementData = grow(); // 容量足够时,进行数组的拷贝 System.arraycopy(elementData, index, elementData, index + 1, s - index); // 数组赋值完之后(将索引以及后面的元素往后复制一个后) 再对索引位置进行数据覆盖。 // 这样就完成了数据按照索引的插入 elementData[index] = element; // 元素插入后 数量加1 size = s + 1; } private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } // 这是一个本地方法,我们不能查看方法体,所以可以看一下官方的解释 /* 将数组从指定的源阵列(从指定位置开始)复制到目标阵列的指定位置。 数组组件的子序列从 {@code src} 引用的源数组复制到 {@code dest} 引用的目标数组。 复制的组件数等于 {@code length} 参数。源数组中位置 {@code srcPos} 到 {@code srcPos+length-1} 的组件分别复制到目标数组的位置 {@code destPos} 到 {@code destPos+length-1} 中。 。。。。。。。。 */ // 个人理解就是将要插入索引后面的元素往后挪一位,将当前元素插入进来 /* 参数说明: src: 要被复制的元素 srcPos: 要开始复制的位置 src: 被赋值的元素 destPos: 复制到的位置 length: 要复制的长度 其实我们可以自己写一个数组的挪位置。回头有时间写写。 */ public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
把另一个集合的元素添加进来
经过前面的分析 这里就很简单了。
public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); modCount++; int numNew = a.length; // 如果添加的集合元素为空,就不添加 返回false if (numNew == 0) return false; Object[] elementData; final int s; // 判断容量够不够,如果(当前容量减去控制大小的size 就是剩余没有用到的数组容量)小于新添加集合元素的数量,就进行扩容。 if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); // 进行数组的拷贝 和上面一致 // 就是把传入的数组全部复制到原来数组后面 System.arraycopy(a, 0, elementData, s, numNew); // 复制完成 元素数量添加 size = s + numNew; return true; }
按照索引进行复制
public boolean addAll(int index, Collection<? extends E> c) { // 对索引的正确性进行检查 rangeCheckForAdd(index); // 将添加的集合的元素转化为数组 Object[] a = c.toArray(); modCount++; int numNew = a.length; // 如果长度为0就不用添加了 直接返回false if (numNew == 0) return false; Object[] elementData; final int s; // 判断当前可用的容量够不够支持插入新的集合里面的元素 if (numNew > (elementData = this.elementData).length - (s = size)) // 不够的话就进行扩容 elementData = grow(s + numNew); // 对要移动元素的位置进行判断 int numMoved = s - index; // 大于0,则代表在当前底层数组中进行插入 if (numMoved > 0) // 进行数组元素的拷贝 ,简单来说就是把原数组挪出来可以容下新元素的位置 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); // 不大于0,代表在当前底层数组后面插入 // 在这里进行新数组的数据覆盖 System.arraycopy(a, 0, elementData, index, numNew); // 集合的大小扩大 size = s + numNew; return true; }
-
删除元素
根据索引删除元素
public E remove(int index) { // 检查元素是否越界 这里很简单就不复制下面的代码了 Objects.checkIndex(index, size); // 拿到当前数组的元素 final Object[] es = elementData; // 获取当前索引对应的元素 @SuppressWarnings("unchecked") E oldValue = (E) es[index]; // 这里是删除的核心代码,详情看下面 fastRemove(es, index); // 返回被删除的元素 return oldValue; } /* 参数说明: param1: 源数组 param2: 要删除的索引位置 */ private void fastRemove(Object[] es, int i) { modCount++; // 删除一个元素后,剩余集合容量的大小 final int newSize; // 如果是大于代表当前元素在底层数组的中间或者前面,否则在末尾 if ((newSize = size - 1) > i) // 数组拷贝 和前面一样 不再分析 System.arraycopy(es, i + 1, es, i, newSize - i); // 这里代表正好要删除的元素在末尾 直接赋值为null就行 等待GC的处理 // 并且给size重新赋值大小 es[size = newSize] = null; }
根据元素删除
// 这里其实也是调用了索引删除 // 通过变量i拿到对应元素索引的位置,然后进行底层数组的覆盖(复制) /* 为什么这里写的那么复杂呢 因为ArrayList支持存储null, 当存储的元素为null的时候,寻找元素会空指针异常,所以分成了为null和不为null两个分流 这里也可以发现 这里的移除只会移除第一个符合的元素 后面的不会移除 */ public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; } /* 参数说明: param1: 源数组 param2: 要删除的索引位置 */ private void fastRemove(Object[] es, int i) { modCount++; // 删除一个元素后,剩余集合容量的大小 final int newSize; // 如果是大于代表当前元素在底层数组的中间或者前面,否则在末尾 if ((newSize = size - 1) > i) // 数组拷贝 和前面一样 不再分析 System.arraycopy(es, i + 1, es, i, newSize - i); // 这里代表正好要删除的元素在末尾 直接赋值为null就行 等待GC的处理 // 并且给size重新赋值大小 es[size = newSize] = null; }
按照条件删除
// 说实话 并看不懂这个。。特别牵扯到位运算。。。。想不清楚为什么这么做。 @Override public boolean removeIf(Predicate<? super E> filter) { return removeIf(filter, 0, size); } /** * Removes all elements satisfying the given predicate, from index * i (inclusive) to index end (exclusive). */ boolean removeIf(Predicate<? super E> filter, int i, final int end) { Objects.requireNonNull(filter); int expectedModCount = modCount; final Object[] es = elementData; // Optimize for initial run of survivors for (; i < end && !filter.test(elementAt(es, i)); i++) ; // Tolerate predicates that reentrantly access the collection for // read (but writers still get CME), so traverse once to find // elements to delete, a second pass to physically expunge. if (i < end) { final int beg = i; final long[] deathRow = nBits(end - beg); deathRow[0] = 1L; // set bit 0 for (i = beg + 1; i < end; i++) if (filter.test(elementAt(es, i))) setBit(deathRow, i - beg); if (modCount != expectedModCount) throw new ConcurrentModificationException(); modCount++; int w = beg; for (i = beg; i < end; i++) if (isClear(deathRow, i - beg)) es[w++] = es[i]; shiftTailOverGap(es, w, end); return true; } else { if (modCount != expectedModCount) throw new ConcurrentModificationException(); return false; } }
-
修改
设计lambda表达式额真的不太好想,能力上还是差了太多。
根据索引下标进行修改:
public E set(int index, E element) { // 检查数组是否越界 Objects.checkIndex(index, size); // 获取当前索引的元素 E oldValue = elementData(index); // 将当前索引的值进行覆盖 elementData[index] = element; return oldValue; } E elementData(int index) { return (E) elementData[index]; }
-
查询元素
// 这个很简单就不分析了 public E get(int index) { Objects.checkIndex(index, size); return elementData(index); }
-
遍历元素
// 使用增强for进行遍历的时候,会自动创建一个迭代器 public Iterator<E> iterator() { return new Itr(); }