问题一:看到这个图,你会想到什么?
答:
这个图由Map指向Collection的Produces并不是说Map是Collection的一个子类(子接口),这里的意思是指Map的KeySet获取到的一个视图是Collection的子接口。
我们可以看到集合有两个基本接口:Map和Collection。但是我个人认为Map并不能说是一个集合,称之为映射或许更为合适,因为它的KeySet视图是一个Set类型的键集,所以我们姑且把它也当做集合。
Collection继承了Iterator接口,而Iterator的作用是给我们提供一个只能向后遍历集合元素的迭代器,也就是说所有实现Collection的类都可以使用Iterator遍历器去遍历。
每种接口都有一个Abstract开头的抽象子类,这个子类中包括了一些默认的实现,我们在自定义类的时候都需要去继承这个抽象类,然后根据我们不同的需求,对于其中的方法进行重写。
从容器角度上来说,只有四种容器:Map,Queue,Set,List。
问题二:列出常见的集合,并进行简单的介绍
答:
- ArrayList: 一种可以动态增长和缩减的的索引序列
- LinkedList:一种可以在任何位置进行高效地插入和删除操作的有序序列
- ArrayDeque:一种用循环数组实现的双端队列
- HashSet:一种没有重复元素的无序集合
- TreeSet:一种有序集
- EnumSet:一种包含枚举类型值的集
- LinkedHashSet:一种可以记住元素插入次序的集
- PriorityQueue:一种允许高效删除最小元素的集合
- HashMap:一种存储键/值关联的数据结构
- TreeMap:一种键值有序排列的映射表
- EnumMap:一种键值属于枚举类型的映射表
- LinkedHashMap:一种可以记住键/值项添加次序的映射表
- WeakHashMap:一种其值无用武之地后可以被垃圾回收期回收的映射表
- IdentityHashMap:一种用==而不是用equals比较键值的映射表
- Vector:目前使用较少,因为设计理念的陈旧和性能的问题被ArrayList所取代
- Hashtable:线程非同步可以使用HashMap来替代,同步的话可以使用ConcurrentHashMap来替代
问题三:关于Iterator,聊聊你的看法
从鸟瞰图中我们可以看到,所有实现Collection的子类都继承了Iterable接口。这个接口提供了一个iterator()方法可以构造一个Iterator接口对象。然后我们可以使用这个迭代器对象依次访问集合中的元素。
迭代器一般使用方法是这样的:
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
或者是这样的:
//适用于JDK1.8以后的版本
iter.forEachRemaining(element -> System.out.println(element));
迭代器的next()工作原理是这样的:
迭代器是位于两个集合元素之间的位置,当我们调用next()方法的时候迭代器指针就会越过一个元素,并且返回刚刚越过的元素,所以,当我们迭代器的指针在最后一个元素的时候,就会抛出会抛出一个NoSuchElementException的异常。所以,在调用next()之前需要调用hasNext()去判断这个集合的迭代器是否走到了最后一个元素。
通过调用next()方法可以逐个的去访问集合中的每个元素,而访问元素的顺序跟该容器的数据结构有关,比如ArrayList就是按照索引值开始,每次迭代都会使索引值加1,而对于HashSet这种数据结构是散列表的集合,就会按照某种随机的次序出现。
Iterator的接口中还有一个remove()方法,这个方法实际上删除的是上次调用next()方法返回的元素,下面我来展示一下remove()方法的使用方法
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
iter.next();
iter.remove();
这样就可以删除该集合中的第一个元素,但是需要注意一点,如果我们需要删除两个元素,必须这样做:
iter.remove();
iter.next();
iter.remove();
而不能这么做:
iter.remove();
iter.remove();
因为next()方法和remove()方法之间是有依赖性的,如果调用remove之前没有调用next就会抛出一个IllegalStateException的异常。
问题四:对于Collection,你了解多少?
可以看出,作为顶级的框架,Collection仅仅是继承了Iterable接口,接下来,我们来看一下Iterable的源码,看看有什么收获。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
可以看到这个接口中有三个方法,其中iterator()方法可以给我们提供一个迭代器,这个在之前的教程就已经说过了,而forEach()方法提供了一个函数式接口的参数,我们可以使用lambda表达式结合来使用:
Collection<String> collection = ...;
collection.forEach(String s -> System.out.println(s));
这样就可以获取到每个值,它的底层实现是加强for循环,实际上也是迭代器去遍历,因为编译器会把加强for循环编译为迭代遍历。 Spliterator()是1.8新加的方法,字面意思可分割的迭代器,不同以往的iterator()需要顺序迭代,Spliterator()可以分割为若干个小的迭代器进行并行操作,既可以实现多线程操作提高效率,又可以避免普通迭代器的fail-fast(fail-fast机制是java集合中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件)机制所带来的异常。Spliterator()可以配合1.8新加的Stream()进行并行流的实现,大大提高处理效率。
Collection()中提供了17个接口方法(除去了继承自Object的方法)。接下来,我们来了解一下这些方法的作用:
- size(),返回当前存储在集合中的元素个数。
- isEmpty(),如果集合中没有元素,返回true。
- contains(Object obj),如果集合中包含了一个与obj相等的对象,返回true。
- iterator(),返回这个集合的迭代器。
- toArray(),返回这个集合的对象数组
- toArray(T[] arrayToFill),返回这个集合的对象数组,如果arrayToFill足够大,就将集合中的元素填入这个数组中。剩余空间填补null;否则,分配一个新数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素。
- add(Object element),将一个元素添加到集合中,如果由于这个调用改变了集合,返回true。
- remove(Object obj),从集合中删除等于obj的对象,如果有匹配的对象被删除,返回true。
- containsAll(Collection<?> other),如果这个集合包含other集合中的所有元素,返回true。
- addAll(Collection<? extends E> other),将other集合中的所有元素添加到这个集合,如果由于这个调用改变了集合,返回true。
- removeAll(Collection<?> other),从这个集合中删除other集合中存在的所有元素。如果由于这个调用改变了集合,返回true。
- removeIf(Predicate<? super E> filter),从这个集合删除filter返回true的所有元素,如果由于这个调用改变了集合,则返回true。
- retainAll(Collection<?> other),从这个集合中删除所有与other集合中的元素不同的元素。如果由于这个调用改变了集合,返回true。
- clear(),从这个集合中删除所有的元素。
- spliterator(),返回分割后的若干个小的迭代器。
- stream(),返回这个集合对于的流对象。
- parallelStream(),返回这个集合的并行流对象。
作为第一级的集合接口,Collection提供了一些基础操作的借口,并且可以通过实现Iterable接口获取一个迭代器去遍历获取集合中的元素。
问题五:那么AbstractCollection呢?
作为Collection的抽象类实现,它的方法都是基于迭代器来完成的,这里只贴出了源码中几个需要特殊的注意的点,
TAG 1 :
数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8 byte。
TAG 2 :
finishToArray(T[] r, Iterator<?> it)方法用于数组扩容,当数组索引指向最后一个元素+1时,对数组进行扩容:即创建一个大小为(cap + cap/2 +1)的数组,然后将原数组的内容复制到新数组中。扩容前需要先判断是否数组长度是否溢出。这里的迭代器是从上层的方法(toArray(T[] t))传过来的,并且这个迭代器已执行了一部分,而不是从头开始迭代的
TAG 3 :
hugeCapacity(int minCapacity)方法用来判断该容器是否已经超过了该集合类默认的最大值即(Integer.MAX_VALUE -8),一般我们用到这个方法的时候比较少,后面我们会在ArrayList类的学习中,看到ArrayList动态扩容用到了这个方法。
TAG 4 :
这里的add(E)方法默认抛出了一个异常,这是因为如果我们想修改一个不可变的集合时,抛出 UnsupportedOperationException 是正常的行为,比如当你用 Collections.unmodifiableXXX() 方法对某个集合进行处理后,再调用这个集合的修改方法(add,remove,set…),都会报这个错。因此 AbstractCollection.add(E) 抛出这个错误是准从标准。
问题六: 能否详细说一下toArray方法的实现?
高能预警:废话不多说,直接上源码
/**
* 分配了一个等大空间的数组,然后依次对数组元素进行赋值
*/
public Object[] toArray() {
//新建等大的数组
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
//判断是否遍历结束,以防多线程操作的时候集合变得更小
if (! it.hasNext())
return Arrays.copyOf(r, i);
r[i] = it.next();
}
//判断是否遍历未结束,以防多线程操作的时候集合变得更大,进行扩容
return it.hasNext() ? finishToArray(r, it) : r;
}
/**
* 泛型方法的`toArray(T[] a)`方法在处理里,会先判断参数数组的大小,
* 如果空间足够就使用参数作为元素存储,如果不够则新分配一个。
* 在循环中的判断也是一样,如果参数a能够存储则返回a,如果不能再新分配。
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
//当数组a的长度大于等于a,直接将a赋予给r,否则使用反射API获取一个长度为size的数组
T[] r = a.length >= size ? a :
(T[])java.lang.reflect.Array
.newInstance(a.getClass().getComponentType(), size);
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
//判断是否遍历结束
if (! it.hasNext()) {
//如果 a == r,将r的每项值赋空,并将a返回
if (a == r) {
r[i] = null;
} else if (a.length < i) {
//如果a的长度小于r,直接调用Arrays.copyOf进行复制获取一个新的数组
return Arrays.copyOf(r, i);
} else {
System.arraycopy(r, 0, a, 0, i);
if (a.length > i) {
a[i] = null;
}
}
return a;
}
//如果遍历结束,将迭代器获取的值赋给r
r[i] = (T)it.next();
}
//判断是否遍历未结束,以防多线程操作的时候集合变得更大,进行扩容
return it.hasNext() ? finishToArray(r, it) : r;
}
/**
* 设定该容器的最大值
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 用于动态扩容
*/
@SuppressWarnings("unchecked")
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
int i = r.length;
while (it.hasNext()) {
int cap = r.length;
if (i == cap) {
int newCap = cap + (cap >> 1) + 1;
if (newCap - MAX_ARRAY_SIZE > 0)
newCap = hugeCapacity(cap + 1);
r = Arrays.copyOf(r, newCap);
}
r[i++] = (T)it.next();
}
return (i == r.length) ? r : Arrays.copyOf(r, i);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError
("Required array size too large");
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
为了帮助了解,我把Arrays.copyOf(r.i)的源码也贴出来:
//参数original代表你传入的需要复制的泛型数组,newLength复制得到数组的大小
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
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;
}
我们可以观察到其中调用了System.arraycopy方法,为了保持刨根问底的态度,我们又去翻看了这个方法的源码:
//src数组里从索引为srcPos的元素开始, 复制到数组dest里的索引为destPos的位置, 复制的元素个数为length个.
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,int length);
可以看到这个方式是由关键字native修饰的方法,那么native修饰的方法有什么含义呢? native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。
然后我们来分析toArray()中需要注意的点,通过原源码中的英文注解,toArray得到的数组跟原collection没有任何关系,我们可以对数组的每个引用值做修改,而不会影响到原collection.这个看起来好像是多余说明的,但是考虑到ArrayList其实就是基于数组实现的,那这个限制保证了即使是将ArrayList转化为数组,那也应该是分配一个新数组,而不是返回原来的数组。
如果我们在单线程操作的情况下,collection集合大小不变,正常应该是执行到 return it.hasNext() ? finishToArray(r, it) : r;这条语句结束,但考虑到在复制的过程中,collection的集合可能会有变化,可能是变大也可能是变小,所以方法增加了对这种情况的处理,这就是为什么每次循环都要判断是collection是否遍历完,以及最后再判断collection是否变得更长,如果是的话,还需要重新再为array分配空间。
通常情况下,我们不会执行到hugeCapacity,但作为一个框架来说,这体现了设计时的严谨。
问题七:用的最多的集合之一——List,说说你对它的理解
List是继承自Collection的一个子接口,它提供了一个有序的集合,在这个集合中我们可以使用索引去获取集合中的值,同时,我们也可以通过迭代器去访问集合中的元素,第一种方法被称为随机访问,因为我们可以按照任意的顺序去访问元素,而使用迭代器就必须顺序的去访问元素。
相比于它的父接口Collection,并没有发生很大的改动,但是由于List是一个有序的集合,所以提供了一些基于索引进行的操作:
get(int index):获取该集合中索引等于index的元素
set(int index, E element):将该集合中索引等于index的元素赋值为element
add(int index, E element):在集合中索引等于index的位置将element插入,并将当前处于该位置的元素及其后续元素的索引加1。
remove(int index):删除指定索引(index)位置的元素,并将处于该位置后面的元素索引减1
indexOf(Object o):获取对象o在集合中的索引
lastIndexOf(Object o):获取对象o在集合中最后一次出现的索引值,如果集合中不存在这个对象,返回-1。
同时,提供了一个Iterator的子接口ListIterator,基于这个迭代器,我们实现了两个默认方法replaceAll(UnaryOperator<E> operator)和sort(Comparator<? super E> c)。
replaceAll(UnaryOperator<E> operator)这里和String类中replaceAll()方法并不相同,这里的接收参数是一个函数式接口,我们来看一下这个函数式接口的源码:
package java.util.function;
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
用法如下:
List<String> strList = new ArrayList<>();
strList.add("Hungary");
strList.add("Foolish");
strList.replaceAll(t -> "Stay " + t);
strList.forEach(s -> System.out.println(s));
打印结果为
Stay Hungary Stay Foolish
而sort(Comparator<? super E> c)传入的同样是一个函数式接口,我们可以自定义排序规则后,调用这个方法进行排序:
List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
humans.sort((Human h1, Human h2) -> h1.getName().compareTo(h2.getName()))
这里是Arrays.sort的源码,可以看到使用了归并算法和TimSort算法来进行排序。
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
问题八:刚刚你说到了ListIterator,可以详细说一下嘛
前面我们已经提过,ListIterator作为Iterator的子接口,给有序的集合List提供了一个链表结构下的迭代器,接下来,我们来看一下ListIterator的源码:
和Iterator不同的是,ListIterator新增了一些基于链表数据结构的操作以及可以用来反向遍历链表的方法:
hasPrevious():当反向迭代列表时,还有可供访问的元素,返回true。
previous():返回前一个对象,如果已经到达了列表的头部,抛出一个NoSuchElementException异常
nextIndex():返回下一次调用next方法将返回的元素索引
previousIndex():返回下一次调用previous方法将返回的元素索引
add(E newElement):在当前位置前添加一个元素。
set(E newElement):用新元素取代next或previous上次访问的元素。如果在next或previous上次调用之后列表结构被修改了,将抛出一个IllegalStateException异常。
问题九:说说AbstractList
AbstractList是实现List接口的一个抽象类,它的地位之与List类似于AbstractCollection之与Collection,同时,AbstractList继承了AbstractCollection,并针对List接口给出了一些默认的实现。而且它是针对随机访问储存数据的方式的,如果需要使用顺序访问储存数据方式,还有一个AbstractSequentialList是AbstractList的子类,顺序访问时应该优先使用它。
接下来,我们来看一下AbstractList的源码,看看他针对于List接口相较于AbstractCollection给出了哪些不同的实现方法。
AbstractList的源码在结构上分为了两种内部迭代器,两种内部类以及AbstractList本身的代码,它的一些实现都是基于内部类和内部的两种迭代器:Itr和ListItr来完成的,下面是部分源码的解析(由于篇幅原因,不能放上全部,只能抛砖引玉,写一部分)
//由于该集合是不可变的,所以一切可能会改变集合元素的操作都会抛出一个UnsupportedOperationException()
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
//获取某个元素在集合中的索引
public int indexOf(Object o) {
//这里是由AbstractList内部已经提供了Iterator, ListIterator迭代器的实现类,分别为Itr,ListItr。这里是调用了一个实例化ListItr的方法
ListIterator<E> it = listIterator();
if (o == null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
//如果集合中不存在该元素,返回-1
return -1;
}
/**
* 内部实现了Iterator接口的实现类Itr
*/
private class Itr implements Iterator<E> {
//光标位置
int cursor = 0;
//上一次迭代到的元素的光标位置,如果是末尾会置为-1
int lastRet = -1;
//并发标志,如果两个值不一致,说明发生了并发操作,就会报错
int expectedModCount = modCount;
//删除上一次迭代器越过的元素
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//调用需要子类去实现的remove方法
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
//每次删除后,将lastRet置为-1,防止连续的删除
lastRet = -1;
//将修改次数赋给迭代器对对象的结构修改次数这个会在下面进行详解
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
//如果出现索引越界,说明发生了并发的操作导致,所以抛出一个并发操作异常。
throw new ConcurrentModificationException();
}
}
//判断是否发生了并发操作
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
//继承自Itr的ListIterator的实现类ListItr
private class ListItr extends Itr implements ListIterator<E> {
//获取上一位的元素,这里在后面会有画图帮助理解
public E previous() {
checkForComodification();
try {
//这里和父类的写法略有不同,先将光标的位置进行减一
int i = cursor - 1;
E previous = get(i);
//因为需要返回的是前一位的元素,所以这里的光标值和上一次迭代到的光标的位置实际上是一样的
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
//设置元素
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//默认设置的位置是上一次迭代器越过的元素
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//添加元素
public void add(E e) {
checkForComodification();
try {
//设置添加的位置为当前光标所在的位置
int i = cursor;
AbstractList.this.add(i, e);
//这里讲lastRet设置为-1,即添加的元素不允许立即删除
lastRet = -1;
//添加后,将光标移到
cursor = i + 1;
//迭代器并发标志和集合并发标志统一
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
//如果出现了索引越界,说明发生了并发操作
throw new ConcurrentModificationException();
}
}
}
//切取子List
public List<E> subList(int fromIndex, int toIndex) {
//是否支持随机访问
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
//使用迭代器成段删除集合中的元素
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}
}
//继承自AbstractList的内部类SubList,代表了它父类的一部分
class SubList<E> extends AbstractList<E> {
private final AbstractList<E> l;
private final int offset;
private int size;
//根据父类来构造一个SubList
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
//修改次数(并发标志)和父类保持一致
this.modCount = l.modCount;
}
//实际上还是调用的父类的set方法和get方法
public E set(int index, E element) {
rangeCheck(index);
checkForComodification();
return l.set(index+offset, element);
}
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification();
//实际上还是在父类上进行添加
l.add(index+offset, element);
this.modCount = l.modCount;
//然后把size + 1
size++;
}
}
//相较于SubList内部类,多了一个是否可以随机访问的标志
class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
super(list, fromIndex, toIndex);
}
public List<E> subList(int fromIndex, int toIndex) {
return new RandomAccessSubList<>(this, fromIndex, toIndex);
}
}
问题十:索引和游标的关系
这里我画了一个图,然后对照着这个图,我们再来看一下ListItr中的一些代码:
//下一位的索引值等于光标值
public int nextIndex() {
return cursor;
}
//上一位的索引值等于光标值减一
public int previousIndex() {
//其实这里并不理解,为啥不去检查索引越界。。
return cursor-1;
}
假定迭代器现在运行到1所在的位置,可以很容易的看出当迭代器处于这个位置的时候,去调用nextIndex()方法得到的是1,而调用previousIndex得到的就是0。这是完全符合我们的逻辑的,接下来,我们再来看previous()方法的源码:
//获取上一位的元素,这里在后面会有画图帮助理解
public E previous() {
checkForComodification();
try {
//这里和父类的写法略有不同,先将光标的位置进行减一
int i = cursor - 1;
E previous = get(i);
//因为需要返回的是前一位的元素,所以这里的光标值和上一次迭代到的光标的位置实际上是一样的
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
其实这里我在分析的时候是存在疑问的(为什么这里的lastRet等于cursor,而Itr中的next()方法的实现中cursor实际上等于lastRet - 1),在画完图分析索引和游标的关系之后又来看一遍才恍然大悟,
这里的lastRet代表的是上一次迭代到的元素的光标位置,所以,我们来举个例子,当迭代器在4的位置的时候,使用了previous()方法,这时的迭代器的位置是在3,而上次迭代到的元素的游标位置也是3,而如果使用了next()方法,使用之后,迭代器的位置在5,而上一次迭代到的元素却是4。这也印证了nextIndex()和previousIndex()的逻辑。
最后,关于面试我还通过一些渠道整理了大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、等初级,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。