java单列集合--源码级讲解

Java传家宝:微信公众号(Java传家宝)、Java传家宝-B站Java传家宝-知乎Java传家宝-CSND

单列集合

​ 常用的集合分为实现了Collection顶层接口的单列集合,实现了Map顶层接口的双列集合。

​ 先挨个解析一下常用的单列集合,即实现了Collection接口的集合类,包括三大类:SetListQueue,如图

单列集合
单列集合

List

​ 一般是有序的(是指按插入顺序排序)、可重复的。常用的包括了动态数组ArrayList链表LinkedList向量集合Vector。Collection接口继承了Iterable接口,说明单列集合都可以进行迭代器迭代

ArrayList

ArrayList
ArrayList

​ 动态数组,拥有数组的功能,依次添加新的数据,保持添加的顺序,可通过索引取值,动态扩容。特点是查询快增删慢。在看一下结构图:

ArayList

通过结构图也能发现其他几个特点:

  • 实现了Serializable接口, 可序列化
  • 实现了Cloneable接口, 可复制
  • Collection接口继承了Iterable接口,说明单列集合都可以 进行迭代器迭代

接下来研究一下JDK1.8的源码实现原理,先说结论:

  • 空构造创建 默认为一个 空的Object[]数组,后面添加 第一个元素时,扩容默认长度为 10
  • 扩容规则为如果 旧容量的1.5倍不足以满足 最小容量的需求,就扩容为最小需要的容量,反之,扩容为原来的1.5倍
  • 最大容量为 Integer.MAX_VALUE - 8

旧容量:原有的容量

最小容量:将新元素添加后,最小需要的容量

然后我们跟进ArrayList空构造器的源码,以注释做解释,会省略大部分源码:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 可以看到 空构造默认为空的Object[]
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

然后看一下add添加的源码,(来自多个类,我放在一起了):

// 记录当前数组容量大小
private int size;
// 实际存储数据的数组
transient Object[] elementData;
public boolean add(E e) {
     // 1 确保容量足够
        ensureCapacityInternal(size + 1);  // Increments modCount!!
     // 2 在当前索引后一位添加,所以他是有序的
        elementData[size++] = e;
        return true;
}
// 重要的是这段扩容代码
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
     // 原容量,即数组长度
        int oldCapacity = elementData.length;
     // 新容量设置位原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
     // 新容量如果小于最小容量
        if (newCapacity - minCapacity < 0)
            // 新容量=最小容量
            newCapacity = minCapacity;
     // 新容量如果大于最大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 设置为最大容量
            newCapacity = hugeCapacity(minCapacity);
        // 扩容为新容量
        elementData = Arrays.copyOf(elementData, newCapacity);
}

LinkedList

LinkedList
LinkedList

​ 链表,每个节点保存了前后节点的地址,形成双向链表结构。特点是查询慢增删快。在看一下结构图:

LinkedList

通过结构图也能发现其他几个特点:

  • 实现了Serializable接口,可序列化

  • 实现了Cloneable接口,可复制

  • 实现了Queue接口,说明有LinkedList能够作为队列使用

接下来研究一下JDK1.8的源码实现原理,先说结论:

  • LinkedList保存了 首尾节点,所以在首尾做某些操作会很快
  • 查询操作需要通过判断索引位置 从首节点或者尾节点遍历得到
  • 添加操作,如果是 普通添加,只需在尾节点直接添加即可, 很快
  • 删除操作,如果 指明了某个节点,那么只需要更改前后节点内部的连接即可, 很快

然后我们跟进LinkedLis的源码,看一下他的结构:

transient int size = 0;
// 记录了首为节点
transient Node<E> first;
transient Node<E> last;
// 节点静态内部类
private static class Node<E{
     // 元素
        E item;
     // 前后节点地址
        Node<E> next;
        Node<E> prev;
}

再看一下查询操作:

public E get(int index) {
    checkElementIndex(index); // 检查索引是否合法
    return node(index).item;
}
Node<E> node(int index) {
    // 判断索引偏左还是偏右,偏左就按首节点遍历,偏右就按尾节点遍历
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
}

在看一下普通添加操作:

public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
     // 拿到尾节点
        final Node<E> l = last;
     // 用要保存的数据创建一个新节点
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
     // 添加
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
}

在看一下普通删除操作:

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
unlink(Node<E> x) {
        // 拿到所有属性
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
  // 更改连接,达到删除效果
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        x.item = null;
        return element;
}

Vector

​ 向量,线程安全,是遗留的类,一般不使用,所有方法通过Synchronized实现同步,达到线程安全的效果。与ArrayList的区别总结为:

  • 空构造直接创建长度为 10的Object数组
  • 扩容需要创建时指定扩容大小 capacityIncrement,如果 不指定就扩容为原来的 两倍
  • 线程安全,ArrayList线程不安全

先看一下空构造方法:

public Vector() {
    // 调用了另一个有参构造,设置默认长度为10
    this(10);
}
public Vector(int initialCapacity) {
        this(initialCapacity, 0);
}

在看一下扩容规则

private void grow(int minCapacity) {
 // 拿到当前数组容量
    int oldCapacity = elementData.length;
    // 是否定义了扩容长度capacityIncrement,没有就扩容为两倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
  // 将数组数据拷贝到另一个新的数组中,返回新数组,完成扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

Set

​ 一般是不可重复的。常用的包括有HashSetTreeSetLinkedHashSet等。

HashSet

HashSet
HashSet

无序不可重复。可有一个null值,索引为0。底层就是通过HashMap实现的。先看一下结构图:

HashSet

通过结构图也能发现其他几个特点:

  • 实现了Serializable接口, 可序列化
  • 实现了Cloneable接口, 可复制

接下来研究一下JDK1.8的源码实现原理,先说结论:

  • 创建一个HashSet其实是创建了一个加载因子为0.75的HashMap

  • 添加数据时,只是在这个HashMap中添加键值默认为一个Object对象PRESENT

首先看一下构造器

public HashSet() {
    // 创建HashMap
    map = new HashMap<>();
}
// 加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

在看一下添加的源码:

private static final Object PRESENT = new Object();
public boolean add(E e) {
    // 网HashMap添加数据,键为要添加的值,值为一个默认的Object对象
    return map.put(e, PRESENT)==null;
}

TreeSet

无序,即不是按照存储顺序排序。也可以说有序,默认通过从小到大的顺序排序,还可以通过传入比较器,自定义排序规则。底层就是HashSet。特点总结为:

  • 自然排序:根据传入的对象实现的 Comparable接口规则排序,在Java中有部分类有默认的Comparable接口实现。
排序规则
BigDecimal、BigInteger、Byte、 Double、Float、Integer、Long、Short数字按照从小到大的顺序
Character按字符的Unicode值的数字大小排序
String按字符串中字符的Unicode值的数字大小排序
  • 自定义排序:可以自己实现一个Comparable比较器,自定义排序规则
TreeSet

通过结构图也能发现其他几个特点:

  • 实现了Serializable接口, 可序列化
  • 实现了Cloneable接口, 可复制

接下来研究一下JDK1.8的源码实现原理,先说结论:

  • 创建一个TreeSet其实是创建了一个没有比较器的TreeMap

  • 添加数据时,只是在这个TreeMap中添加键值默认为一个Object对象PRESENT

源码就不解析了,类似HashSet与HashMap的关系。

LinkedHashSet

​ 特别得,他是有序的维护了数据存储顺序,不可重复。底层采用LinkedHashMap实现。先看一下结构图:

LinkedHashSet

通过结构图也能发现其他几个特点:

  • 实现了Serializable接口, 可序列化
  • 实现了Cloneable接口, 可复制
  • 继承于HashSet,他的 添加方法与HashSet一样, 键添加数据值为默认的Object对象

接下来研究一下JDK1.8的源码实现原理,先说结论:

  • 空构造默认创建一个 加载因子为0.75长度为16的LinkedHashMap

首先看一下空构造函数:

public LinkedHashSet() {
    // 调用父类构造
    super(16, .75ftrue);
}
// HashSet构造,创建LinkedHashMap对象
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

public LinkedHashMap(int initialCapacity, float loadFactor) {
     // 调用父类构造 LinkedHashMap的父类是HashMap 创建了一个HashMap对象
        super(initialCapacity, loadFactor);
        accessOrder = false;
}

Queue

​ 队列,我们更多的是在多线程时使用到。Java实现了很多种队列,这里就说一个优先级队列,之后总结多线程时,会详解各种阻塞队列的实现。

PriorityQueue

​ 优先级队列,采用的是小顶堆的数据结构实现,无论何时进行增删改操作,都会将优先级最小的移动到根节点。典型的应用场景就是任务调度,任务随机添加,每次出队优先级最高的任务。先看一下结构图:

PriorityQueue

通过结构图也能发现其他几个特点:

  • 实现了Serializable接口, 可序列化

接下来研究一下JDK1.8的源码实现原理,先说结论:

  • 空构造器构造一个默认 长度为11的object数组,无比较器
  • 数据结构采用 小顶堆的方式实现
  • 旧容量**<64**,扩容为旧容量的 2倍+2(跟可变字符串一样),否 则扩容为旧容量的1.5倍(跟动态数组一样)
  • 添加到 无比较器的优先级队列的对象必须 实现Comparable接口
  • 不可添加null

先看一下构造函数:

// 默认长度为11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityQueue() {
    // 调用有参构造,比较器为null
    this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator)
 
{  
     // 创建一个长度为11的数组
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
}

在看一下添加元素的过程:

public boolean add(E e) {
    // 调用offer方法
    return offer(e);
}
public boolean offer(E e) {
     // 不可添加null元素
        if (e == null)
            throw new NullPointerException();
        int i = size; // 当前使用的容量
        if (i >= queue.length)
            // 扩容
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            // 第一个元素直接添加到根节点
            queue[0] = e;
        else
            // 非第一个元素,输入新长度,和添加的元素
            siftUp(i, e);
        return true;
}
// 扩容
private void grow(int minCapacity) {
     // 旧容量
        int oldCapacity = queue.length;
     // 旧容量<64,扩容为旧容量的2倍+2,否则扩容为旧容量的1.5倍
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        queue = Arrays.copyOf(queue, newCapacity);
}
// 非第一个节点的添加过程
private void siftUp(int k, E x) {
        if (comparator != null)
            // 有比较器的规则
            siftUpUsingComparator(k, x);
        else
            // 无比较器的规则
            siftUpComparable(k, x);
}
// 无比较器的规则 向上调整算法
private void siftUpComparable(int k, E x) {
     // 添加到无比较器的优先级队列的对象必须实现Comparable接口,这里强转为比较器
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            // 逻辑右移1位,相当于除2,目的是找到k索引对应的父亲节点的索引parent
            int parent = (k - 1) >>> 1;
            // 拿到该位置的元素
            Object e = queue[parent];
            // 新元素与其比较,如果大于旧元素,直接退出循环
            // 小顶堆规则:孩子节点必须大于等于父亲节点
            if (key.compareTo((E) e) >= 0)
                break;
            // 小于父亲节点,则交换两节点的位置,继续网上调整
            // 小于旧元素 则将旧元素放在索引k处
            queue[k] = e;
            // 更新索引k为旧元素的旧索引位置
            k = parent;
        }
     // 比较完成。将新元素设置在索引k上
        queue[k] = key;
}

想必没基础的看到这里,优先级队列还是迷迷糊糊的,首先,优先级队列实际是通过数组存储数据,但是逻辑结构是堆,且本身采用小顶堆的实现方式。

说一下向上调整算法:

  • 第一步,先通过int parent = (k - 1) >>> 1拿到索引k对应父亲节点,k就是目前就是叶子节点

  • 第二步,与父亲节点比较,是否满足小顶堆的条件:孩子节点必须大于等于父亲节点

  • 满足可直接退出,添加索引k为新元素

  • 不满足,旧交换父亲节点和孩子节点的位置,继续网上比较即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值