综述:
在数据结构课程中,我了解到顺序表,链接表,栈和队列,树这些存储数据的结构。但事实上Java在这些底层之上的构建有很多门道,Java通过其精妙的继承与实现能力构造了一系列类,用于满足程序员们存储信息的各种需要,那么这个集合框架是怎样的呢?
编辑
所有集合类可分为两大组,一方面是图片左侧,祖宗类是Collection的一组,叫做单列集合(一次存储一个元素),另一方面是图片右侧,祖宗类是Map的一组,叫做双列集合(一次存储两个元素)。最左侧的Iterator接口,我认为它的存在只是确保了所有集合的一个基本功能——迭代,而没有明确其他内容或功能。
迭代器
迭代器的基本语法是这样的:
List<String> list=new ArrayList<>();
list.add("11");
list.add("22");
list.add("33");
list.add("44");
Iterator <String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
复制代码
总体而言,集合类都会有iterator方法(或者重写过)用来产生一个迭代器,迭代器对象的三个基础方法有:next(),hasNext()和remove()。作用如下:
hasNext()方法用来判断指针当前位置有无元素。
next()方法用来返回当前元素,并且将指针移动到下一元素。
remove()用于在迭代器中去除当前元素(直接在集合中remove掉)。
注意的点:
1:为什么hasNext不是有下一个元素?为什么next不是返回下一个元素?这和我们在链表中常用的方法有些区别,b站黑马阿玮的视频从源码角度说明了这个问题:
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.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;
// prevent creating a synthetic constructor
Itr() {}
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 = i + 1;
return (E) elementData[lastRet = i];
}
复制代码
源码如上,关键点在于属性cursor默认为0,表示当下指针位置,在每次next()时返回的时elementData中的索引为cursor的值(代码最后一行用的时i,而非cursor)。而hasNext()在cursor越出数组长度后才会return false(因为cursor是从0起步),于是得出了此刻指针已经超出了范围,hasNext才会返回False。
我们在链表中更熟悉:this.head起步到达第一个节点,next就表示返回下一个元素。
2:迭代器的底层还是对底层数组按照索引遍历。
3:remove的源码分析:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
复制代码
首先我们要明白,remove掉的并非是cursor当下指向的元素,而是上一次next返回的元素。
如果我们有abc,此时光标在a,一旦调用了next方法返回了cursor当下的元素a,光标可就移走了,如果我们想要在知道这个位置是a之后操作元素,就要想办法操作cursor的上一个数据。Java中用lastRet记录了下来,确实是有原因的。
(不过如果按照字面意义理解next,hasNext的话,next把光标从a移动到下一位b同时返回b,那么此时remove掉的也确实是b,Java的实现看起来对它的属性名字透明了)。
一个更强大的迭代器——ListItr
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;
}
@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;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
复制代码
其特殊之处在于拥有add方法和previous方法,让迭代器更加灵活。
发现:在迭代器中的增删操作底层关键还是对集合的增删操作,只不过是对迭代器的某些属性进行了更新,(关键是对modCount和expectModCount的操作,避开了专门设计的报错)。
Collection族
Collection作为祖宗接口声明了一些普适方法:
boolean |
add(E e) 确保此集合包含指定的元素(可选操作)。 |
boolean |
addAll(Collection<? extends E> c) 将指定集合中的所有元素添加到此集合(可选操作)。 |
void |
clear() 从此集合中删除所有元素(可选操作)。 |
boolean |
contains(Object o) 如果此集合包含指定的元素,则返回 true 。 |
boolean |
containsAll(Collection<?> c) 如果此集合包含指定 集合中的所有元素,则返回true。 |
boolean |
equals(Object o) 将指定的对象与此集合进行比较以获得相等性。 |
int |
hashCode() 返回此集合的哈希码值。 |
boolean |
isEmpty() 如果此集合不包含元素,则返回 true 。 |
Iterator<E> |
iterator() 返回此集合中的元素的迭代器。 |
default Stream<E> |
parallelStream() 返回可能并行的 Stream 与此集合作为其来源。 |
boolean |
remove(Object o) 从该集合中删除指定元素的单个实例(如果存在)(可选操作)。 |
boolean |
removeAll(Collection<?> c) 删除指定集合中包含的所有此集合的元素(可选操作)。 |
default boolean |
removeIf(Predicate<? super E> filter) 删除满足给定谓词的此集合的所有元素。 |
boolean |
retainAll(Collection<?> c) 仅保留此集合中包含在指定集合中的元素(可选操作)。 |
int |
size() 返回此集合中的元素数。 |
default Spliterator<E> |
spliterator() 创建一个Spliterator在这个集合中的元素。 |
default Stream<E> |
stream() 返回以此集合作为源的顺序 Stream 。 |
Object[] |
toArray() 返回一个包含此集合中所有元素的数组。 |
<T> T[] |
toArray(T[] a) 返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。 |
本身是泛型接口,其子接口和子类们基本也都是泛型接口和泛型类。
其下分为三类:List,Set,Queue。
其中List是有序,有重,关键在于拥有索引。Set无序无重。Queue是队列,后进后出,拥有的操作有限。
List往下有了索引属性后为其增添了许多有用方法,按索引查找,按索引遍历,按索引删除......
我们所学的LinkedList和ArrayList都是其实现类,虽然底层的构造和实现方式不同,但这对上层对象的调用都是透明的,这也就是接口实现的本质要求——只关心功能,不关心实现。
如果我们想创建List对象(当然是用多态的方式创建其实现类的对象),无论具体子类是ArrayList还是LinkedList还是Vector,都只能用list内声明的方法,这说明用多态创建对象的一个弊端:虽然很规范,但会丢失子类方法。
很奇怪为什么栈stack会在这里,文档中查到它的方法很有限,那有何必继承Vector呢?
编辑
编辑
与之相对的队列Queue是一个接口,只声明了几个必要方法:而且似乎没有实现类。
编辑
源码分析:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
复制代码
主要属性:
Object数组elementData用于存储数据,int类型size用于记录数组长度并且表示下一次添加时的索引。一些可能会用到的常量,Java选择用静态常量的方式定义在属性中,比如Object数组类型的EMPTY_ELEMENTDATA={}会用于比较当前数组是否为空,int DEFAULT_CAPACITY = 10用于确定默认数组的初始长度。
构造方法:
无参构造方法只执行了一步,为elementData创建了一个空数组。
参数为int的构造方法意在创建一个确定长度的ArrayList,Java在此的操作也就是简单的两次if分支debug。
参数为集合的构造方法目的是把集合中的元素作为新ArrayList的元素而创建,Java的操作是:先将集合变为Object数组,然后将此数组赋给elementData,其中两个deBug分别是:如果我们传入参数不是ArrayLst,则用Arrays工具包中的copyOf直接转成Object数组类型,如果是ArrayList类型则也会自动向上转型成为Object[]后赋值(伪泛型确实伪);另外如果传入的集合为空,便不必进行太多操作(感觉并非是因为方法不能兼容size=0,而是为了代码的效率),直接为elementData赋值常量,这和无参构造的效果一致了。
add方法:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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;
size = s + 1;
}
复制代码
以上是三个重载的add方法。
第一个和第二个方法很局限,或者说是默认的add。第一个add用了private修饰实则只能为第二个服务(或者理解为是第二个add为了代码模块化而分开写了一个方法)。
我们重点分析一下第三个add方法。
首先判断了输入的index合不合法。然后判断一下elementData满了没有,如果满了调用grow方法(后续分析)。
之后是关键代码:
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
复制代码
arraycopy将elementData的index及以后都赋给了index+1及以后,我们画个图理解一下:
编辑
arraycopy方法我在IDEA中只能看到这些,因为native是调用的本地代码(用c写的),我查了一下还要下载调试器和c的编译器才能看,然后我就没看。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
复制代码
(2)图是最常见最常用的用法,arraycopy从srcPos+length的位置开始取出原数组的值,放到destPos+length的位置,然后沿着length自减的方向一直copy(很明显很多地方需要deBug)。
那么在(1)中我们就可以理解了,一个数组内从后往前覆盖,直到最后会出现index和index+1的值相同,最后Java为index位置赋值,这样完成指定位置add。
接下来我们分析一下grow方法:
grow方法:
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 {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
复制代码
总体而言grow方法用于数组扩容。
我们先来看第一个grow,第一行记录下原本数组的长度oldCapacity,接下来调用ArraysSupport的newLength方法得到新的数组长度newCapacity,得到后返回具有新长度的Object数组elementData。在此中Java分支考虑了一个情况,当原本数组长度为0,那么grow就直接到DEFAULT_CAPACITY的长度,或者当此时添加的长度minCapacity比DEFAULT_CAPACITY大时,就直接grow到minCapacity的长度了。
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);
}
}
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;
}
}
}
复制代码
那么这个确实newCapacity的newLength方法是怎样的呢?
首先看第一个方法:
需要三个参数:原本数组的长度,扩容的最小长度和扩容的首选长度(意思是一般扩容会首选首选长度perfered growth,但当这个首选长度太小,小于最小长度minimum growth时,会扩容最小长度的。(这在一个方法中看起来是毫无必要的,毕竟谁会在调这个函数的时候设置首选长度比最小长度还小呢?但在底层调用中这三个参数位置上可都是变量,变化中这样设置是有必要的)。
那么我们回到代码中,首选Java判断了一下:当扩容后的长度preLength大于0(?能小于0?)并且不是太大(public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;),那么就根据参数确定了长度。但当这个prefLength真的大(导致overflow),Java调用了hugeLength来防止overflow,具体是这样的:
向hugeLength()传入原本长度和最小扩容长度,当他俩的和太大就直接报错。
throw new OutOfMemoryError(
"Required array length " + oldLength + " + " + minGrowth + " is too large");
}
复制代码
这就有一个问题:难道数值超过最大值,计算机中就会记录为负值吗?
咱计网没学好,不过从代码上看确实是这样。
问了问chatGpt。溢出——环绕(有负回负,无负回0).
编辑
然后如果并不大于最大值的话,就返回最大值,如果超过了最大值,就返回这个超过的值。
(为什么会到达最后一个else呢?难道不是超过以后会环绕到负数吗?)
所以咱们再回到grow方法,(我再复制到下面来便于说明:)
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 {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
复制代码
新的数组长度newCapacity是怎么来的呢?是在oldCapacity的基础上,默认增加到1.5倍,而最小增量是我们输入的长度减去原本长度,也就是至少达到minCapacity。
另外我还想记录一下一个方法:
copyOf方法:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
复制代码
copyOf是Arrays工具类中的一个静态方法,没有对象调用就意味着在参数中要传入更多东西(比如基础的数组)。
在这个方法中传入了original基础数组,newLength新数组的长度,newType返回的数组的(实际)新类型。(那么传入T的子类就会自动转型到T,但可以向下转到newType)。
代码块中用三元进行了一个判断:如果我们传入的newType是Object[]类型,那么直接创建Obj[]类型的数组,除此之外,创建一个新的newType类型的数组,然后根据是原数组短还是要复制的长度短来进行复制。
这句判断有点怪:
(Object)newType == (Object)Object[].class
复制代码
编辑
我理解的泛型的擦除是因为集合内部的底层数组是用Obj[]存储的,泛型只是在编译时期限制元素进入。所以无论用泛型存入何种数据类型,底层都无法调用这个类的特有方法。
可是这个源代码里面没有明显向上转型的操作啊(不会有比源还源的代码吧)...
编辑
这样一个来自getClass方法一个来自直接.class却可以...
getComponentType用于对数组进行操作,返回数组的类//////////反射知识暂不深入。
这个创建新数组的底层会深入到native里面导致看不到,但我猜测因为这个参数中的newType不太好识别(Class类?),所以想创建这个类的数组对象或许并不简单。
remove方法:
先看源码:
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;
}
复制代码
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;
}
复制代码
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);
es[size = newSize] = null;
}
复制代码
前两个remove都是基于fastRemove的。这个fastRemove我们根据参数可以明白它是根据索引删除指定Obj数组的值。代码很容易理解,我们上面已经介绍了arraycopy方法。
那么我们看第一个remove方法:
首先第一个问题在于:声明了final的es对象,为什么还可以通过fastRemove修改内容呢?
看看AI怎么说:
编辑
对于一般的基本数据类型,也许final声明后就真改不了了,但对于数组,对象等等引用数据类型,final只是绑定了声明的名称和引用对象,不能更改这个赋值关系,但是却可以微微小改这个引用对象里面的值。
第二个问题在于:明明remove方法中修改的是es,为什么实际作用能作用到属性elementData呢?Java中有许多传递,源代码里更是如此,final Object[] es = elementData;这句传递了elementData的地址,而非只是传递数据(如果是es=elementData.copy()之类的才是通过创建一个新的对象来完成真的copy),所以代码中操作es,实则是操作es所指向地址的某个数组,这就是elementData。
为啥要这样写啊卧槽!?直接改elementData不行吗?不懂
问问AI
编辑
编辑 编辑
通体而言有以下几点好处吧:(硬凑的亮点)
1:局部变量es的值存储在当前线程的栈中,而elementData作为类属性应该存的深一点,访问es开销更小。
2:如果一个方法中需要多次操作elementData,那么反反复复地写elementData会显得很繁琐且可能面临修改多次elementData的情况。
我们再看这第二个for循环:
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;
}
复制代码
首先这里面最关键的就是found:{}。这是一个循环代码块,起了一个名字方便指定位置break或者continue。
我们用一个简单的例子说明:
public static void main(String[] args) {
outer: for (int i = 0; i < 10; i++) {
inner: for (int j = 0; j < 5; j++) {
if (i * j > 15) {
System.out.print(i + " " + j+" ");
System.out.println("Breaking out of outer loop");
break outer;
}
System.out.println(i + " " + j);
}
}
System.out.println("Done");
}
复制代码
测试结果:
编辑这表面确实我们想要跳出哪层就跳出哪层,只要每层有名字。但是found:{}为什么算循环?
嗨呀就有这用法,咱也不清楚有啥用,或许可以代替switch,或许会用于指定位置暴力停止。
addAll方法
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
modCount++;
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);
System.arraycopy(a, 0, elementData, s, numNew);
size = s + numNew;
return true;
}
复制代码
首先看来这些源码之后,发现Java的源代码有许多莫名其妙的局部变量,明明在一个方法中就用一次,而且没产生额外的有效信息,却有。
关键的地方在于第二个if语句,简化的理解是:如果添加的元素数量大于原数组剩下的空余量,elementData用grow扩容。但为什么if写的这么怪呢?--不懂。
其中grow内的参数任然是最小容量的意思,意思是如果这个最小容量小于原数组一半就扩容一半,大于一半就增加这个最小容量。
clone方法
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);
}
}
复制代码
代码第三行的泛型值得一看,这个?是无限制通配符,它表示不知道是什么类就可以是所有类,它就是我们熟悉的<? extends T>,<? super T>的最普遍的类型。很容易和一种情况搞混:
public <E>void Test(E e) {
ArrayList<E> arr = new ArrayList<E>();
}
复制代码
public class TestForFound<E> {
public void Test(E e) {
ArrayList<E> arr = new ArrayList<E>();
}
复制代码
如果一般我们为集合添加泛型时,都会考虑是泛型类,还是泛型方法,还是泛型接口,因为这是站在编码人的角度,还不考虑具体实现时的内容。只有在主函数或者方法中我们考虑运用数组存储信息时才明确泛型的类型。而clone方法中我们为了接收下来ArrayList,要用明确的ArrayList类型接收而不能用泛型E,(用E就像是clone方法强行把调用对象转化到E类型,这明显不是我们想要的),但又因为不知道什么类型,那么用?通配符吧,传进来什么是什么。
另外,super.clone()来自Object类中的方法,这个方法会将调用者自动向上转型到Object,于是我们要强转回来。(还TM在native本地)。
重置modCount有维护Iterator的含义,具体我们下面再讲。
SubList内部类
private static class SubList<E> extends AbstractList<E> implements RandomAccess {
private final ArrayList<E> root;
private final SubList<E> parent;
private final int offset;
private int size;
/**
* Constructs a sublist of an arbitrary ArrayList.
*/
public SubList(ArrayList<E> root, int fromIndex, int toIndex) {
this.root = root;
this.parent = null;
this.offset = fromIndex;
this.size = toIndex - fromIndex;
this.modCount = root.modCount;
}
/**
* Constructs a sublist of another SubList.
*/
private SubList(SubList<E> parent, int fromIndex, int toIndex) {
this.root = parent.root;
this.parent = parent;
this.offset = parent.offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = parent.modCount;
}
复制代码
感觉这个内部类设施的很骚
subList的几个属性有root,表示它基于哪个集合创建,offset表示起步位置,size表示子列表长度。
它的属性没有一个实际数组,这就表示它的任何调用都是和root绑定的。
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList<>(this, fromIndex, toIndex);
}
复制代码
这是一个ArrayList类下的subList方法,它直接返回一个subList对象。
原本我在想subList应该返回一个数组类型,或者是很明显具有数据的集合。但实际操作是:这个集合对象里设置了一个属性:ArrayList类型的root,然后任何常规的方法都重写了,目的是根据这个root和起步位置模拟完成一般意义的操作。
确实有浓浓的面向对象的味道。
我们简单看两个重写的方法。
public E get(int index) {
Objects.checkIndex(index, size);
checkForComodification();
return root.elementData(offset + index);
}
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification();
root.add(offset + index, element);
updateSizeAndModCount(1);
}
public E remove(int index) {
Objects.checkIndex(index, size);
checkForComodification();
E result = root.remove(offset + index);
updateSizeAndModCount(-1);
return result;
}
复制代码
我们看到:get,set,add,remove方法都是基于root,然后用offset修正索引,并且对size和modCount进行修正。
toArray方法
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;
}
复制代码
首先参数是一个数组,如果数组的长度不能够容纳调用对象,则返回一个新的长度为size,类型为a的类型(再强转到T[]),数据来源于elementData的数组。
可是a本来不就是T[]的类型吗?newType设置成a.getClass(),又为什么要强转呢?