版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/MyEclipse_1214/article/details/52070565
今天带来的是java单列顶层接口的第一个轻量级实现:AbstractCollection
我们直接进入正题,先来看看它的声明:
package java.util;
//可以从名字上同样看到 AbstractCollection 是一个抽象类,所以并不能实例化,
//这个类只是作为轻量级实现存在,让实现Collection的子类不需要实现太多的方法
public abstract class AbstractCollection<E> implements Collection<E> //同样使用了泛型E
再来,我们先看看有哪些方法 AbstractCollection并没有负责实现:
有三个方法:
// Query Operations 查询操作
/**
* 返回一个迭代器
*/
public abstract Iterator<E> iterator();
/**
* 返回集合的大小
*/
public abstract int size();
<span style="white-space:pre"> </span>// Modification Operations 修改操作
/**
* 添加元素
*/
public boolean add(E e) {
throw new UnsupportedOperationException();
}
为什么这三个方法不能在AbstractCollection里实现呢?
大家都知道单列集合的种类有很多种,而有些集合各自底层实现的方法,数据结构是完全不同的,比如大家知道的ArrayList底层使用数组实现的,HashSet是散列结构,LinkedList则是双向链表结构,所以这边三个方法,在代码实现上不同,所以AbstractCollection 仍然以声明abstract的方式或者抛出 不支持本操作的异常方式,让子类必须得重写这三个方法。
那下面继续看,
/**
* 判断集合是否为空,这里则是调用获取集合大小的方法,
* 并判断大小是否等于0来判断集合是否为空的
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 判断集合中是否包含参数对象
*/
public boolean contains(Object o) {
Iterator<E> it = iterator();//这里用到了迭代器对象,我把迭代器对象贴在了下面
if (o==null) {
//首先判断参数是否为空
while (it.hasNext())
//迭代器的方法,意思是 迭代器是否 有下一个元素
if (it.next()==null)
//如果有 是否为空
return true;//返回true
} else {
// 传入参数不为空
while (it.hasNext())
if (o.equals(it.next()))
//这里使用了equals来比较 迭代器中的元素与传入参数
// 上一期说过equals来自Object类,Object默认equals是比较两对象引用是否一致
// 就是是否指向同一个地址值,所以这里如果参数并没有实现equals就会直接拿两个地址值进行比较
// 一致则返回true
return true;
}
//都没有则返回false
return false;
}
下面看看 迭代器Iterator的声明
package java.util;
/**
* 可以看到迭代器是和集合一起出现的都是1.2版本以后
* 而且这里的迭代器声明只能单向向后迭代
* @since 1.2
*/
public interface Iterator<E> {
/**
* 判断迭代器中是否含有下一个元素
*/
boolean hasNext();
/**
* 返回迭代器的下一个元素,并且把指针往下移动
*/
E next();
/**
* 删除迭代器当前的元素
*/
void remove();
}
看到这里,可以知道,如果要自己实现一个集合类, 那首先需要实现的方法就是获取一个迭代器,
到后面就可以看到,那些集合子类实现,都是通过内部类的形式实现获取一个迭代器这个方法的。
我们继续:
/**
* 将集合转化成数组
*/
public Object[] toArray() {
// 声明一个当前集合长度的Object数组
Object[] r = new Object[size()];
// 可以看到集合中很多重要的方法都要用到 获取迭代器 这个方法
// 集合内部都是使用这个迭代器去遍历,集合中的元素的
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
//这里的length ,在线程安全的情况下,其实和size() 是一致的
if (! it.hasNext()) //迭代其中没有下一个元素了
// 直接返回数组,这边调用的是Arrays.copyof()方法,我们下面看看这个方法的实现
return Arrays.copyOf(r, i);
//如果有下一个元素,则数组的当前索引位置则被赋值为集合的下一个元素
r[i] = it.next();
}
// 当遍历完整个数组的长度后,集合仍然有下一个元素,在线程不安全的情况下
// 很可能就是这边在转化数组,但是集合另一边还在添加元素导致的。
// 如果仍然有下一个元素,则调用finishToArray()方法,下面我们也会看看,
// 如果没有了,则把当前的Object数组给直接返回
return it.hasNext() ? finishToArray(r, it) : r;
}
可以看到jdk大神,考虑的是相当缜密的,这才是大神啊~膜拜
下面我们看看Arrays.copyOf()这个方法
/**
*
* Arrays类是操作数组的工具类,所以很多方法都是静态的
* 方便用户直接调用
*/
public static <T> T[] copyOf(T[] original, int newLength) {
//原始数组 , 新的数组长度
// jdk直接调用了另一个重载方法,后面可以看到这样的调用是非常多的
// 这么调用充分的利用了java的重载特性,将代码的复用最大化
return (T[]) copyOf(original, newLength, original.getClass());
}
我们继续:
/**
* 这里的copyof比上一个copyof多一个代表数组中元素类型的Class参数 这里还使用了通配符泛型
*/
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
// 如果传入的Class类型是Object的话,则直接创建一个Object数组
? (T[]) new Object[newLength]
// 如果不是Object类型的话,则用Array.newInstance方法去创建一个新的数组
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//这里调用的则是系统类中的数组复制,后面可以看到该方法是native的
// java中声明为native的方法,意思是底层实现并非java语言实现,应该是C语言
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
/**
* 这里jdk没有声明版本,可见该方法是从java诞生开始就有的
*/
// 源数组 源数组起始位置 目标数组 目标数组起始位置 单位长度
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
//这个方法大致意思是 把 源数组src 从 源数组起始位置srcPos 开始的元素
//复制到 目标数组dest 从 目标数组起始位置destPos 开始 单位长度length 个的元素
// 亲测以后,知道 如果 length > src的长度或者destPos + length > dest
// 或者srcPos,destPos两个索引位置都超过了对应数组的长度,
// 就会抛出 ArrayIndexOutOfBoundsException
这个方法不止jdk源码调用,现实中,我们也可以用这个方法实现数组的赋值哦~
突然发现刚刚忘记分析,Arrays.copyof()调用这个方法时传递的长度参数了
Math.min(original.length, newLength))
/**
* 就是返回两个传入参数,小的那个
*/
public static int min(int a, int b) {
return (a <= b) ? a : b;
}
我们继续: Array.newInstance()方法
/**
* 创建一个与传入参数相同类型,并且长度为传入参数长度的数组
* 底层调用的还是native方法
*/
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}
//可以看到这个方法是私有的,是不允许外面访问的,区别在于不再有泛型指定了,意思是你想要创建数组必须传递给我一个明确的类型
private static native Object newArray(Class componentType, int length)
throws NegativeArraySizeException;
还有个finishToArray()
//这个方法就是刚刚的数组如果长度赋值完毕但是集合中还有剩余元素的话
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
// 记录当前数组的长度
int i = r.length;
while (it.hasNext()) {
//如果迭代器有下一个元素
int cap = r.length;//创建容量变量
if (i == cap) {
//如果和之前的数组长度一致,说明当前数组长度不够,需要扩容
//新的容量 等于 原长度 + 原长度的50% + 1
// 这里可以看到jdk底层很多2次幂的运算都是用位移符号去做的
// 并且扩容是按照 50%比例去做的
int newCap = cap + (cap >> 1) + 1;
// 这里的 MAX_ARRAY_SIZE 是一个静态常量,为 Ieteger最大值 - 8
// 而Integer的最大值是 2147483647
if (newCap - MAX_ARRAY_SIZE > 0)
// 如果新的容量 大于 MAX_ARRAY_SIZE
newCap = hugeCapacity(cap + 1);
//通过前面说的 Arrays.copyOf 创建数组
r = Arrays.copyOf(r, newCap);
}
//走到这里数组r 扩容完毕了
// 所以通过迭代器进行赋值,这里用的索引是之前记录的数组长度,
// 也是进这个方法赋值后的起点位置
r[i++] = (T)it.next();
}
// 如果过度分配的话,就会走Arrays.copyof重新对数组长度进行赋值
// 走到这里的时候i 可以说就是代表 数组中最后一个有效位置了,因为后面迭代器已经没有元素了
// 但是可能存在数组长度比i要大,所以需要通过重新分配长度,对后面的空白位置进行去除
// jdk原来注释就是这个意思 trim if overallocated
return (i == r.length) ? r : Arrays.copyOf(r, i);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 正常校验
throw new OutOfMemoryError
("Required array size too large");
//如果传入的容量大于 MAX_ARRAY_SIZE 则返回 Integer的最大值,否则就返回 MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
下面继续带参数的 toArray:
/**
*
*/
public <T> T[] toArray(T[] a) {
// 把当前的集合的大小记录下来
int size = 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()) { // 迭代器没有下一个元素了
if (a == r) {
r[i] = null; // 把剩余元素设为null
} else if (a.length < i) {//参数数组的长度小于 集合的长度
return Arrays.copyOf(r, i);//调用copyof复制一个数组
} else {
System.arraycopy(r, 0, a, 0, i);
if (a.length > i) {
a[i] = null;
}
}
return a;
}
//有元素则对数组的当前索引进行赋值
r[i] = (T)it.next();
}
// 这里同无参 toArray()方法
return it.hasNext() ? finishToArray(r, it) : r;
}
看到这里大概了解到,jdk底层集合转化成数组就是首先根据集合的长度参数创建一个新的数组,然后通过迭代把集合中的元素一个个的赋值给数组中的索引位置
/**
* 删除元素
* 和contains 非常类似
*/
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
//contains的区别就是在这里,通过调用迭代器的remove方法来去除元素
//不同集合迭代器实现可能不同,所以这里只需要委托给具体实现就行了
it.remove();
return true;
}
}
}
return false;
}
剩下的方法就简单了:一目了然
/**
* 之前在Collection接口中介绍过了,
* 本集合是否包含 参数集合的所有元素
*/
public boolean containsAll(Collection<?> c) {
for (Object e : c)//通过迭代 参数的集合
if (!contains(e)) //只要有一个元素不包含在本集合中就返回false
return false;
return true;//全都包含就返回true
}
/**
* 就算add没有实现,这里addAll也是可以实现的,
* 通过迭代参数集合,并且要求泛型是 本集合泛型的子类
* 将参数集合中的每一个元素都添加入本集合
*/
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
/**
* 基本同addAll
*/
public boolean removeAll(Collection<?> c) {
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
/**
* 求交集的方法
*/
public boolean retainAll(Collection<?> c) {
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {//如果本集合中的元素不包含在参数集合中,就把它remove掉
it.remove();
modified = true;
}
}
//所以最后剩下的就是本集合和参数集合交集的那部分元素
return modified;
}
/**
* 就是迭代本集合,把元素全部remove掉
*/
public void clear() {
Iterator<E> it = iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
}
终于到最后一个方法了:
/**
* 重写了Object的toString方法,
* 所以集合是直接可以通过System.out.println()打印在控制台的
* 使用的正是这个方法中定义的格式 [xxx, xxxx, xxx, xxxx]
*/
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";//集合中没有元素则打印一对中括号,表示空集合
//这里使用的是StringBuilder去拼接字符串
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {// 同 while(true)
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
//如果迭代器没有下一个元素了,就用中括号收尾
return sb.append(']').toString();
// 不然就拼接上 逗号 和 空格
sb.append(',').append(' ');
}
}
说到集合打印,就顺便提一下 数组的打印,数组如果直接打印在控制台的话,是一串地址值,没有什么意义,
所以要打印数组的话,可以用数组工具类的方法Arrays.toString(数组) 把想要打印的数组放进去就行了。
AbstractCollection看完了,正所谓轻量级实现,所以还有很多方法是没有实现的,
下一个分析就从List 开始吧~因为大家用的最多嘛~