摘要
主要是讲解这个集合的原理类相关的类。
参看:https://zhuanlan.zhihu.com/p/165393520
这个图由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));
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,你了解多少?
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));
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
Spliterator()是1.8新加的方法,字面意思可分割的迭代器,不同以往的iterator()需要顺序迭代,Spliterator()可以分割为若干个小的迭代器进行并行操作,既可以实现多线程操作提高效率,又可以避免普通迭代器的fail-fast(fail-fast机制是java集合中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事)机制所带来的异常。Spliterator()可以配合1.8新加的Stream()进行并行流的实现,大大提高处理效率。
Collectio类中常用的方法介绍
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;
}
//参数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;
}
用的最多的集合之一——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;
}
}
刚刚你说到了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
的源码在结构上分为了两种内部迭代器,两种内部类以及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);
}
}
说说远古时代的ArrayList——Vector
Vector
是一种实现了动态数组的集合,即长度可以自动增长的数组,它是**线程同步(安全)的,也就是说同一时刻只有一个线程可以写Vector
,可以避免多线程同时写引起的不一致性,但是比较消耗资源。
由于资源的耗费较为严重,它已经逐渐的消失在了历史的尘埃中,取而代之的是同样基于动态数组实现的ArrayList
。
简单说一下Stack
栈(Stack
)是Vector
的一个子类,它实现了一个标准的后进先出的栈。Stack
继承自Vector
,说明它也是通过数组实现的而非链表。而且Vector
类具有的特性它都具有。
public class Stack<E> extends Vector<E> {
/**
* Stack的无参构造函数
*/
public Stack() {
}
/**
* 把项压入堆栈顶部
*/
public E push(E item) {
addElement(item);
return item;
}
/**
* 移除堆栈顶部的对象,并作为此函数的值返回该对象。
* @return 被移除的对象
*/
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
/**
* 查看堆栈顶部的对象
* @return
*/
public synchronized E peek() {
int len = size();
if (len == 0) {
throw new EmptyStackException();
}
return elementAt(len - 1);
}
/**
* 测试堆栈是否为空
* @return
*/
public boolean empty() {
return size() == 0;
}
/**
* 返回对象在堆栈中的位置,以 1 为基数
* @param o 需要查找位置的对象
* @return
*/
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
/**
* 版本id
*/
private static final long serialVersionUID = 1224463164541339165L;
}
说一下你对ArrayList源码的理解
ArrayList
与Vector
非常相似,他们都是基于数组实现的集合,都可以动态扩容,只不过Vector
是同步的,所需的资源较多,而且比较老,有一些缺点,所以我们现在更多的是去使用ArrayList
,而不是Vector
。下面,我们在阅读源码的过程中遇到的一些问题对ArrayList
进行分析。
/**
* 共享空数组对象
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 和上面的区别在于,
* 第一次添加元素时知道该 elementData 从空的构造函数还是有参构造函数被初始化的。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 用于盛放集合元素的数组对象
*/
transient Object[] elementData;
/**
* 有参构造,参数为初始长度,如果参数为0,调用EMPTY_ELEMENTDATA来初始化
*
* @param initialCapacity 该集合的初始化长度
* @throws IllegalArgumentException 如果参数小于0,抛出该错误
*/
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);
}
}
/**
* 无参构造,调用了DEFAULTCAPACITY_EMPTY_ELEMENTDATA来初始化
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 提升该ArrayList对象容器的容量,确保可以提供一个该容器存放数据最低所需的容量
*
* @param minCapacity 最低所需容量
*/
public void ensureCapacity(int minCapacity) {
//这里可以看出,如果是默认的无参构造,最低容量是10,如果不是,最小是0。这里体现了代码设计的严谨性!
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
//如果最低所需的容量大于容器初始的最小容量,去调用扩容的方法,这里就体现出了两个不同常量的作用
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
/**
* 计算最小所需容量
* @param elementData 需要计算的数组
* @param minCapacity 期望的最小所需容量
* @return 最小所需容量
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断该容器是否是默认无参构造,如果不是,直接返回传入的minCapacity
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果是,返回默认容量和期望的最小所需容量中的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
* 扩容到最小所需容量方法
* @param minCapacity 最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 扩容到最小所需容量的方法,这里的参数是经过计算后的最小容量
* @param minCapacity 经过计算后的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
//这里关于modCount的疑问可以去看前面AbstractList中的实现
modCount++;
//这里进行一个关于最小容量和数组的长度比较,如果最小容量大于数组的长度,才会进行扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
/**
* 数组的最大容量
* 因为数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8 byte
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 动态扩容,确保数组的容量可以装下所有的元素
*
* @param 最低容量
*/
private void grow(int minCapacity) {
//首先获取当前的数组的容量
int oldCapacity = elementData.length;
//将数组扩容50%,比如原容量是4,扩容后为 4 + 4 / 2 = 6
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后的容量小于最小所需的容量
if (newCapacity - minCapacity < 0) {
//直接将最小容量作为该容器的容量
newCapacity = minCapacity;
}
//如果扩容后的容量大于数组最大的容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//将经过处理后的最小所需容量作为新的容量,最大不超过Integer的最大值
newCapacity = hugeCapacity(minCapacity);
}
//使用Arrays.copyOf将原数组复制到一个新容量的数组,并将拷贝的结果返回给原数组,完成动态扩容。
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 防止数组的容量过大导致内存溢出的方法,程序基本上不会走到这里,只是以防万一
* @param minCapacity
* @return
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
//如果最小所需容量大于数组最大容量,返回Integer的最大值,否则返回数组的最大容量值
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从上面的动态扩容部分的源码分析中,我们可以看到这两个空常量数组的作用所在,这里又会遇到一个问题,在ArrayList
的扩容过程中,是按照50%的比例进行扩容的,这里就有一个问题,扩容后的数组的长度一定会大于数组的长度,就会造成空间和资源的浪费,这时候可以使用下列的方法。
/**
* 清除集合中的空元素所占的空间,一般在动态扩容后会产生空余的空间
*/
public void trimToSize() {
modCount++;
//如果数组中数据的个数小于数组所占的空间,说明产生了多余的空间
if (size < elementData.length) {
//如果没有数据,返回一个EMPTY_ELEMENTDATA对象,否则将数据拷贝一个新的size长度的数组,并赋给原数组
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
看着看着,我们会发现一个问题,ArrayList
中包括了两个remove
方法
/**
* 删除位于某个索引位置的元素
*
* @param index 即将被删除的元素的索引
* @return 返回的是被删除的元素
* @throws IndexOutOfBoundsException 当索引超过集合的长度时,抛出该异常
*/
@Override
public E remove(int index) {
//首先进行索引越界的检查
rangeCheck(index);
//由于这个操作会引起结构的变化,所以要将modCount+1
modCount++;
//获取原本位于该位置的元素,用于返回
E oldValue = elementData(index);
//获取位于被删除索引的前一位的索引
int numMoved = size - index - 1;
if (numMoved > 0) {
//这里的原理是 elementData = {1 ,2 ,3 ,4} ===>
// 删除索引为1 的元素,然后 0(numMoved)的元素{1}作为头,将{3, 4}(index+1)后的部分拼接到原本的index的位置 ==>
// {1, 3, 4},
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
}
//将原本最后一位,置为null ==> {1,3,4,null},再将size-1,完成删除
elementData[--size] = null;
return oldValue;
}
/**
* 删除集合中的指定元素,如果集合中包含该元素,返回true
*
* @param o 被删除的元素
* @return 如果集合中包含该指定元素,返回true
*/
@Override
public boolean remove(Object o) {
//分为两种情况 为null和不为null
if (o == null) {
for (int index = 0; index < size; index++) {
//为null的时候使用 == 判断
if (elementData[index] == null) {
//这里调用的这个方法其实和上面的方法类似,只不过没有返回值,而且没有进行索引的判断
fastRemove(index);
return true;
}
}
} else {
for (int index = 0; index < size; index++) {
//不为null的时候使用 equals 判断
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
}
return false;
}
/**
* 这里没有进行索引的越界判断,也没有返回被删除的值,其他的原理和remove(int index)类似
* @param 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;
}
可以看出两个删除方法的区别在于,一个是根据元素找到索引进行删除,返回的是否删除成功,而一个是根据直接索引进行删除,返回的是被删除的元素,说起删除,下面我们还会看到一个被private
修饰的batchRemove(Collection<?> c, boolean complement)
方法,而调用这个私有方法的分别是removeAll(Collection<?> c)
和retainAll(Collection<?> c)
,而这两个方法的区别在于一个是取交集,一个是取交集之外的元素,是两个刚好对立的方法。
/**
* 删除指定集合与collection的交集
*
* @param c 需要与集合进行判断的collection
* @return 如果这次操作改变了集合的结构,返回true
* @throws ClassCastException 如果集合的元素类型和该集合的元素类型不一致,抛出该异常
* @throws NullPointerException 如果参数collection为空,抛出空指针异常
*/
@Override
public boolean removeAll(Collection<?> c) {
//首先进行非空校验
Objects.requireNonNull(c);
//调用封装好的批量删除的方法,这里传入的参数为false的时候,删除的是交集
return batchRemove(c, false);
}
/**
* 删除collection元素和该集合交集之外的元素
*
* @param c 需要将元素保留在该集合中的collection对象
* @return 如果这次操作改变了集合,返回true
* @throws ClassCastException 如果该集合的元素类型和collection中的元素不一致,抛出该异常
* @throws NullPointerException 如果collection中的元素为空,抛出该异常
*/
@Override
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
//调用封装好的批量删除的方法,这里传入的参数为true的时候,保留的是交集
return batchRemove(c, true);
}
/**
* 批量删除的方法
* @param c 需要和原集合进行对比的collection对象
* @param complement 为false的时候,删除交集,为true的时候,取交集,删除其他
* @return
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
//下面我写了一个小例子帮助大家理解
//假设原集合数组为{1,2,3,4},c为{2,3}
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
//size = 4
for (; r < size; r++) {
//a.当complement为false,r = 0 和 3 的时候会进入循环
//b.当complement为true,r = 1 和 2 的时候会进入循环
if (c.contains(elementData[r]) == complement) {
//r = 0 w = 0 elementData[0] = elementData[0] {1,2,3,4}
//r = 3 w = 1 elementData[1] = elementData[3] {1,4,3,4}
// r = 1 w = 0 elementData[0] = elementData[1] {2,2,3,4}
//r = 2 w = 1 elementData[1] = elementData[2] {2,3,3,4}
elementData[w++] = elementData[r];
}
}
} finally {
//如果contains方法使用过程报异常,将剩余的元素赋给该集合,如果不出现异常的话,是不会进入这个代码块的
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// w = 2
if (w != size) {
for (int i = w; i < size; i++) {
//a. elementData[2] = null, elementData[3] = null {1,4,null,null},null元素会被垃圾回收器回收么?
//b. elmentData[2] = null, elementData[3] = null {2,3,null,null}
elementData[i] = null;
}
//修改次数+2
modCount += size - w;
//当前的数组数量就是符合条件的元素数量
size = w;
//返回操作成功的标志
modified = true;
}
}
return modified;
}
简单介绍一下Map吧
答:Map
是一个接口,代表的是将键映射到值的对象。一个映射不能包含重复的键,每个键最多只能映射到一个值。Map
接口提供了三种collection
视图,允许以键集、值集或键-值映射关系集的形式查看某个映射的内容。映射顺序 定义为迭代器在映射的 collection
视图上返回其元素的顺序。某些映射实现可明确保证其顺序,如 TreeMap
类;另一些映射实现则不保证顺序,如 HashMap
类。
排序
/**
* 根据映射的键进行排序
*/
public static <K extends Comparable<? super K>, V> Comparator<Entry<K,V>> comparingByKey() {
return (Comparator<Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
/**
* 通过指定的比较器根据映射的键进行排序
*/
public static <K, V> Comparator<Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
替换:
/**
* 对映射中的所有键值对执行计算,并将返回结果作为value覆盖
* map.replaceAll((k,v)->((String)k).length());
* @param function 执行的操作,函数式接口
*/
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
...
}
/**
* 当且仅当 key 存在,并且对应值与 oldValue 不相等,才用 newValue 作为 key 的新相关联值,返回值为是否进行了替换。
* @param key 与指定值相关联的键
* @param oldValue 预期与指定键相关联的值
* @param newValue 与指定键相关联的值
* @return 如果该值被替换,返回true
*/
default boolean replace(K key, V oldValue, V newValue) {
...
}
/**
* 只有当目标映射到某个值时,才能替换指定键的条目。
* @param key 与指定值相关联的键
* @param value 与指定键相关联的值
* @return 与指定键相关联的上一个值,如果没有键的映射,返回null
*/
default V replace(K key, V value) {
...
}
Set 的源码你了解多少
Set
继承了Collection
接口,它本身也是一个接口,代表一种不能拥有重复元素的容器类型,更确切的说,集合不包含一对元素e1
和e2
,使得e1.equals(e2)
。通过Set
的一些实现,我们可以发现,Set
是基于Map
进行实现的,所以Set
取值时不保证数据和存入的时候顺序一致,并且不允许空值,不允许重复值。下面我们来看一下Set
都给我们提供了哪些方法。
那么AbstractSet的源码呢,有没有什么了解
/**
* 从此 set 中移除包含在指定 collection 中的所有元素
* 如果指定 collection 也是一个 set,则此操作有效地修改此 set,从而其值成为两个 set 的 不对称差集。
*
* @param c 包含将从此 set 中移除的元素的 collection
* @return 如果此 set 由于调用而发生更改,则返回 true
*/
@Override
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
//通过在此 set 和指定 collection 上调用 size 方法,此实现可以确定哪一个更小。
if (size() > c.size()) {
// 如果此 set 中的元素更少,则该实现将在此 set 上进行迭代,依次检查迭代器返回的每个元素,查看它是否包含在指定的 collection 中。
for (Iterator<?> i = c.iterator(); i.hasNext(); ) {
//如果包含它,则使用迭代器的 remove 方法从此 set 中将其移除。
modified |= remove(i.next());
}
} else {
//如果指定 collection 中的元素更少,则该实现将在指定的 collection 上进行迭代,并使用此 set 的 remove 方法,从此 set 中移除迭代器返回的每个元素。
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}
标签:index,elementData,return,Iterator,JDK,int,元素,源码,集合
From: https://blog.51cto.com/u_13643065/6169236