java集合源码分析一:Collection 与 AbstractCollection

概述

我们知道,java 中容器分为 Map 集合和 Collection 集合,其中 Collection 中的又分为 Queue,List,Set 三大子接口。

其中, List 应该是日常跟我们打交道最频繁的接口了,按照 JavaDoc 的说明,List 是一种:

有序集合(也称为序列)。此接口的用户可以精确控制列表中每个元素的插入位置。用户可以通过其整数索引(在列表中的位置)访问元素,并在列表中搜索元素。

我们以 List 下 Vector,ArrayList,LinkedList 三大实现为主,下面是他们之间的一个关系图。其中,红色表示抽象类,蓝色表示接口。

List集合的实现类关系图

根据上图的类关系图,我们研究一下源码中,类与类之间的关系,方法是如何从抽象到具体的。

一、Iterable 接口

Iterable 是最顶层的接口,继承这个接口的类可以被迭代。

Iterable 接口的方法

  • iterator():用于获取一个迭代器。

  • forEach() :JDK8 新增。一个基于函数式接口实现的新迭代方法。

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    
  • spliterator():JDK8 新增。用于获取一个可分割迭代器。默认实现返回一个IteratorSpliterator类。

    这个跟迭代器类似,但是是用于并行迭代的,关于具体的情况可以参考一下掘金的一个讨论:Java8里面的java.util.Spliterator接口有什么用?

二、Collection 接口

Collection 接口的方法

Collection 是集合容器的顶级接口,他继承了 Iterable 接口,即凡是 Collection 的实现类都可以迭代,List 也是 Collection 的子接口,因此也拥有此特性。

可以看到, Collection 接口提供了十九个抽象方法,这些方法的命名都很直观的反应的这些方法的功能。通过这些方法规定了 Collection的实现类的一些基本特性:可迭代,可转化为数组,可对节点进行添加删除,集合间可以合并或者互相过滤,可以使用 Stream 进行流式处理。

1.抽象方法

我们可以根据功能简单的分类介绍一下 Collection 接口提供的方法。

判断类:

  • isEmpty():判断集合是否不含有任何元素;
  • contains():判断集合中是否含有至少一个对应元素;
  • containsAll():判断集合中是否含另一个集合的所有元素;

操作类:

  • add():让集合包含此元素。如果因为除了已经包含了此元素以外的任何情况而不能添加,则必须抛出异常;
  • addAll():将指定集合中的所有元素添加到本集合;
  • remove():从集合移除指定元素;
  • removeAll():删除也包含在指定集合中的所有此集合的元素;
  • retainAll:从此集合中删除所有未包含在指定集合中的元素;
  • clear():从集合中删除所有元素;

辅助类:

  • size():获取集合的长度。如果长度超过 Integer.MAX_VALU 就返回 Integer.MAX_VALU;

  • iterator():获取集合的迭代器;

  • toArray():返回一个包含此集合中所有元素的新数组实例。因为是新实例,所以对原数组的操作不会影响新数组,反之亦然;

    它有一多态方法参数为T[],此时调用 toArray()会将内部数组中的元素全部放入指定数组,如果结束后指定数组还有剩余空间,那剩余空间都放入null。

2.JDK8 新增抽象方法

此外,在 JDK8 中新增了四个抽象方法,他们都提供了默认实现:

  • removeIf:相当于一个filter(),根据传入的函数接口的匿名实现类方法来判断是否要删除集合中的某些元素;
  • stream():JDK8 新特性中流式编程的灵魂方法,可以将集合转为 Stream 流式进行遍历,配合 Lambda 实现函数式编程;
  • parallelStream():同 stream() ,但是是生成并行流;
  • spliterator():重写了 Iterable 接口的 iterator()方法。

3.equals 和 hashCode

值得一提的是 Collection 还重写了 Object 的 equals()hashCode() 方法(或者说变成了抽象方法?),这样实现 Collection 的类就必须重新实现 equals()hashCode() 方法

三、AbstractCollection 抽象类

AbstractCollection 是一个抽象类,他实现了 Collection 接口的一些基本方法。JavaDoc 也是如此描述的:

此类提供了Collection接口的基本实现,以最大程度地减少实现此接口所需的工作。

通过类的关系图,AbstractCollection 下面还有一个子抽象类 AbstractList ,进一步提供了对 List 接口的实现。 我们不难发现,这正是模板方法模式在 JDK 中的一种运用。

0.不支持的实现

在这之前,需要注意的是,AbstractCollection 中有一些比较特别的写法,即实现了方法,但是默认一调用立刻就抛出 UnsupportedOperationException异常:

public boolean add(E e) {
    throw new UnsupportedOperationException();
}

如果想要使用这个方法,就必须自己去重写他。这个写法让我纠结了很久,网上找了找也没找到一个具体的说法。

参考 JDK8 新增的接口方法默认实现这个特性,我大胆猜测,这应该是针对一些实现 Collection 接口,但是又不想要实现 add(E e)方法的类准备的。在 JDK8 之前,接口没有默认实现,如果抽象类还不提供一个实现,那么无论实现类是否需要这个方法,那么他都一定要实现这个方法,这明显不太符合我们设计的初衷。

1.isEmpty

非常简短的方法,通过判断容器 size 是否为0判断集合是否为空。

public boolean isEmpty() {
    return size() == 0;
}

2.contains/containsAll

判断元素是否存在。

public boolean contains(Object o) {
    Iterator<E> it = iterator();
    // 如果要查找的元素是null
    if (o==null) {
        while (it.hasNext())
            if (it.next()==null)
                return true;
    } else {
        while (it.hasNext())
            if (o.equals(it.next()))
                return true;
    }
    return false;
}

containsAll()就是在contains()基础上进行了遍历判断。

public boolean containsAll(Collection<?> c) {
    for (Object e : c)
        if (!contains(e))
            return false;
    return true;
}

3.addAll

addAll()方法就是在 for 循环里头调用 add()

public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

4.remove/removeAll

remove()这个方法与 contains()逻辑基本一样,因为做了null判断,所以List是默认支持传入null的

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())) {
                it.remove();
                return true;
            }
        }
    }
    return false;
}

5.removeAll/retainAll

removeAll()retainAll()的逻辑基本一致,都是通过 contains()方法判断元素在集合中是否存在,然后选择保存或者删除。由于 contains()方法只看是否存在,而不在意有几个,所以如果目标元素有多个,会都删除或者保留。

public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(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) {
    Objects.requireNonNull(c);
    boolean modified = false;
    Iterator<E> it = iterator();
    while (it.hasNext()) {
        if (!c.contains(it.next())) {
            it.remove();
            modified = true;
        }
    }
    return modified;
}

5.toArray(扩容)

用于将集合转数组。有两个实现。一般常用的是无参的那个。

public Object[] toArray() {
    // 创建一个和List相同长度的数字
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        // 如果数组长度大于集合长度
        if (! it.hasNext())
            // 用Arrays.copyOf把剩下的位置用null填充
            return Arrays.copyOf(r,i);
        r[i] = it.next();
    }
    // 如果数组长度反而小于集合长度,就扩容数组并且重复上述过程
    return it.hasNext() ? finishToArray(r,it) : r;
}

其中,在 finishToArray(r,it) 这个方法里涉及到了一个扩容的过程:

// 位运算,扩大当前容量的一半+1
int newCap = cap + (cap >> 1) + 1;
// 如果扩容后的大小比MAX_ARRAY_SIZE还大
if (newCap - MAX_ARRAY_SIZE > 0)
    // 使用原容量+1,去判断要直接扩容到MAX_ARRAY_SIZE,Integer.MAX_VALUE还是直接抛OutOfMemoryError异常
    newCap = hugeCapacity(cap + 1);
r = Arrays.copyOf(r,newCap);

这里的 MAX_ARRAY_SIZE 是一个常量:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

这里又通过hugeCapacity()方法进行了大小的限制:

private static int hugeCapacity(int minCapacity) {
    // 如果已经大到溢出就抛异常
    if (minCapacity < 0)
        throw new OutOfMemoryError
        ("Required array size too large");
    // 容量+1是否还是大于允许的数组最大大小
    return (minCapacity > MAX_ARRAY_SIZE) ?
        // 如果是,就把容量直接扩大到Integer.MAX_VALUE
        Integer.MAX_VALUE :
    // 否则就直接扩容到运行的数组最大大小
    MAX_ARRAY_SIZE;
}

6.clear

迭代并且删除全部元素。

Iterator<E> it = iterator();
while (it.hasNext()) {
    it.next();
    it.remove();
}

7.toString

AbstractCollection 重写了 toString 方法,这也是为什么调用集合的toStirng() 不是像数组那样打印一个内存地址的原因。

public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

四、总结

Collection

Collection 接口类是 List ,Queue,Set 三大子接口的父接口,他继承了 Iterable 接口,因而所有 Collection 的实现类都可以迭代。

Collection 中提供了规定了实现类应该实现的大部分增删方法,但是并没有规定关于如何使用下标进行操作的方法。

值得注意的是,他重规定了 equlas()hashCode()的方法,因此 Collection 的实现类的这两个方法不再跟 Object 类一样了。

AbstractCollection

AbstractCollection 是实现 Collection 接口的一个抽象类,JDK 在这里使用了模板方法模式,Collection 的实现类可以通过继承 AbstractCollection 获得绝大部分实现好的方法。

在 AbstractCollection 中,为add()抽象方法提供了不支持的实现:即实现了方法,但是调用却会抛出 UnsupportedOperationException。根据推测,这跟 JDK8 接口默认实现的特性一样,是为了让子类可以有选择性的去实现接口的抽象方法,不必即使不需要该方法,也必须提供一个无意义的空实现。

AbstractCollection 提供了对添加复数节点,替换、删除的单数和复数节点的方法实现,在这些实现里,因为做了null判断,因此是默认是支持传入的元素为null,或者集合中含有为null的元素,但是不允许传入的集合为null。

AbstractCollection 在集合转数组的 toArrays() 中提供了关于扩容的初步实现:一般情况下新容量=旧容量 + (旧容量/2 + 1),如果新容量大于 MAX_ARRAY_SIZE,就会使用 旧容量+1去做判断,如果已经溢出则抛OOM溢出,大于 MAX_ARRAY_SIZE 就使用 Integer.MAX_VALUE 作为新容量,否则就使用 MAX_ARRY_SIZE。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 连接 连接池产生原因 连接池实现原理 小结 TEMPERANCE:Eat not to dullness;drink not to elevation.节制
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 一个优秀的工程师和一个普通的工程师的区别,不是满天飞的架构图,他的功底体现在所写的每一行代码上。-- 毕玄 1. 命名风格 【书摘】类名用 UpperCamelC
今天犯了个错:“接口变动,伤筋动骨,除非你确定只有你一个人在用”。哪怕只是throw了一个新的Exception。哈哈,这是我犯的错误。一、接口和抽象类类,即一个对象。先抽象类,就是抽象出类的基础部分,即抽象基类(抽象类)。官方定义让人费解,但是记忆方法是也不错的 —包含抽象方法的类叫做抽象类。接口
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket一、引子文件,作为常见的数据源。关于操作文件的字节流就是 —FileInputStream&amp;FileOutputStream。
作者:泥沙砖瓦浆木匠网站:http://blog.csdn.net/jeffli1993个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节。交流QQ群:【编程之美 365234583】http://qm.qq.com/cgi-bin/qm/qr?k=FhFAoaWwjP29_Aonqz
本文目录 线程与多线程 线程的运行与创建 线程的状态 1 线程与多线程 线程是什么? 线程(Thread)是一个对象(Object)。用来干什么?Java 线程(也称 JVM 线程)是 Java 进程内允许多个同时进行的任务。该进程内并发的任务成为线程(Thread),一个进程里至少一个线程。 Ja
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket在面向对象编程中,编程人员应该在意“资源”。比如?1String hello = &quot;hello&quot;; 在代码中,我们
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 这是泥瓦匠的第103篇原创 《程序兵法:Java String 源码的排序算法(一)》 文章工程:* JDK 1.8* 工程名:algorithm-core-le
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 一、父子类变量名相同会咋样? 有个小故事,今天群里面有个人问下面如图输出什么? 我回答:60。但这是错的,答案结果是 40 。我知错能改,然后说了下父子类变
作者:泥瓦匠 出处:https://www.bysocket.com/2021-10-26/mac-create-files-from-the-root-directory.html Mac 操作系统挺适合开发者进行写代码,最近碰到了一个问题,问题是如何在 macOS 根目录创建文件夹。不同的 ma
作者:李强强上一篇,泥瓦匠基础地讲了下Java I/O : Bit Operation 位运算。这一讲,泥瓦匠带你走进Java中的进制详解。一、引子在Java世界里,99%的工作都是处理这高层。那么二进制,字节码这些会在哪里用到呢?自问自答:在跨平台的时候,就凸显神功了。比如说文件读写,数据通信,还
1 线程中断 1.1 什么是线程中断? 线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,通过其他线程调用了该线程的 方法,使得该线程中断标志位属性改变。 深入思考下,线程中断不是去中断了线程,恰恰是用来通知该线程应该被中断了。具体是一个标志位属性,
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want需求 项目在设计表的时候,要处理并发多的一些数据,类似订单号不能重复,要保持唯一。原本以为来个时间戳,精确到毫秒应该不错了。后来觉得是错了,测试环境下很多一
纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 加微信:bysocket01
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want.文章Points:1、介绍RESTful架构风格2、Spring配置CXF3、三层初设计,实现WebService接口层4、撰写HTTPClient 客户
Writer :BYSocket(泥沙砖瓦浆木匠)什么是回调?今天傻傻地截了张图问了下,然后被陈大牛回答道“就一个回调…”。此时千万个草泥马飞奔而过(逃哈哈,看着源码,享受着这种回调在代码上的作用,真是美哉。不妨总结总结。一、什么是回调回调,回调。要先有调用,才有调用者和被调用者之间的回调。所以在百
Writer :BYSocket(泥沙砖瓦浆木匠)一、什么大小端?大小端在计算机业界,Endian表示数据在存储器中的存放顺序。百度百科如下叙述之:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加
What is a programming language? Before introducing compilation and decompilation, let&#39;s briefly introduce the Programming Language. Programming la
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket泥瓦匠喜欢Java,文章总是扯扯Java。 I/O 基础,就是二进制,也就是Bit。一、Bit与二进制什么是Bit(位)呢?位是CPU
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocket一、前言 泥瓦匠最近被项目搞的天昏地暗。发现有些要给自己一些目标,关于技术的目标:专注很重要。专注Java 基础 + H5(学习) 其他操作系统,算法,数据结构当成课外书博览。有时候,就是那样你越是专注方面越