问题一:看到这个图,你会想到什么?
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200730214624898.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDUxNjkx,size_16,color_FFFFFF,t_70)
答:
这个图由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()进行并行流的实现,大大提高处理效率。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200730214924576.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDUxNjkx,size_16,color_FFFFFF,t_70)
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的抽象类实现,它的方法都是基于迭代器来完成的,这里只贴出了源码中几个需要特殊的注意的点,
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200730215116974.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDUxNjkx,size_16,color_FFFFFF,t_70)
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 operator)和sort(Comparator<? super E> c)。
replaceAll(UnaryOperator 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.