JDK8源码详解-util篇-ArrayDeque(双端队列)

概述

功能:该类为JAVA数据结构之一,双端队列(Deque)的数组实现

开始版本JDK 1.6

注意
1. 本类为具体的实现类,所有的接口方法和抽象方法均给出了实现
2. 数据结构:底层数据结构为数组(顺序表),因为是双端的,可以作为队列(FIFO)或堆栈(FILO)使用
3. 有序、可储存重复元素,不可储存null
4. 线程不安全
5. 作为栈使用时比Stack类效率高,作为队列使用时比LinkedList快一些
6. 迭代器与大多数集合一样,是快速失败(fail-fast)的,而非安全失败(fail-safe),当集合有结构上的改变时会直接抛出ConcurrentModificationException异常
7. 扩容机制为扩容为原容量的2倍(大于原容量的最小2的幂),例8 -> 16
8. 未指定初始大小时默认容量为16,指定了初始大小时,若指定的初始大小小于8则初始容量为8,指定的初始大小大于8的情况取大于指定值的最小2的幂
9. 调用addFirst()addLast()后,若数组已满则调用扩容方法

继承类java.util.AbstractCollection

实现接口Deque<E>, Cloneable, Serializable

所在包java.util

导入包import java.io.Serializable(序列化)、import java.util.function.Consumer(函数式接口)、import sun.misc.SharedSecrets(获取Java栈帧中存储的类信息)

类声明

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable {}

框架图

image

属性

常量

私有常量

01.双端队列的最小容量 MIN_INITIAL_CAPACITY
// 双端队列的最小容量(8),注意容量必须是2的幂
private static final int MIN_INITIAL_CAPACITY = 8;

变量

包变量

01.储存双端队列元素 elements

拓展transient —— 关键字,不进行序列化,但若实现了Externalizable,以具体实现决定是否序列化,transient关键字失效,而Serializable会受transient影响,有则不进行序列化

// 储存双端队列元素的数组
transient Object[] elements; // non-private to simplify nested class access
02.头部元素的索引 head
// 双端队列头部元素的索引,若数组为空则此值与tail的值相等(0)
transient int head;
03.尾部元素的索引 tail
// 双端队列尾部元素的索引,若数组为空此值为0
transient int tail;

方法

构造器

01.无参构造器 ArrayDeque()

注意:此处可以看到是用无参构造器创建实例时(未指定容量),默认容量为16,而不是元素数组的最小容量8

// 源码
public ArrayDeque() {
    elements = new Object[16];
}

02.根据指定容量创建实例 ArrayDeque(int numElements)

注意:此处可以看出并非指定多少容量,实例就是多少容量,会根据指定容量计算,若指定容量小于8则使用8为容量,否则使用大于等于指定容量最小2的幂

// 源码
public ArrayDeque(int numElements) {
    // 根据指定容量计算出最小2的幂,以计算出的值为容量
    allocateElements(numElements);
}

03.创建实例并根据指定集合初始化 ArrayDeque(Collection<? extends E> c)

注意:容量根据集合元素个数计算,元素个数小于8则使用8为容量,否则使用大于等于元素个数最小2的幂

// 源码
public ArrayDeque(Collection<? extends E> c) {
    // 根据指定集合元素个数计算出最小2的幂,以计算出的值为容量
    allocateElements(c.size());
    // 将集合内元素添加至实例中,此方法后续有详细解析
    addAll(c);
}

内部类

01.迭代器(正序) DeqIterator

注意
1. 本内部类的循环处理方法增加了一些判断(判空、判循环中结构改变导致元素为null),即本循环方法中若有某种原因(例:删除导致结构变更)导致next()null会抛出ConcurrentModificationException异常,正常情况下与Iterator中的默认方法效果一样
2. 直接使用对象的removeFirst()addFrist()等方法会导致结构变更,且迭代器并未进行同步,如需移除请使用迭代器内部的remove()方法
3. 迭代器中的remove()方法禁止连续使用,因为连续使用回导致对象中元素混乱

// 源码
private class DeqIterator implements Iterator<E> {
    // 当前指针位置(指针在索引前)
    private int cursor = head;

    // 尾指针位置(指针在索引前)
    private int fence = tail;

    // 上一个指针位置(结构变更标识),默认-1
    private int lastRet = -1;

    // 当前指针位置等于尾指针位置,即表示后续没有元素了
    public boolean hasNext() {
        return cursor != fence;
    }

    // 获取当前指针后的元素
    public E next() {
        if (cursor == fence)
            throw new NoSuchElementException();
        @SuppressWarnings("unchecked")
        E result = (E) elements[cursor];
        // 当前指针并不等于尾指针(当前指针后应当有元素),但是元素为null(可能结构变更了)
        if (tail != fence || result == null)
            throw new ConcurrentModificationException();
        // 设置当前指针为上一个指针位置
        lastRet = cursor;
        // 指针后移
        cursor = (cursor + 1) & (elements.length - 1);
        return result;
    }

    // 移除当前指针后的元素
    public void remove() {
        // 在本方法前已进行了结构操作并未进行next()操作,lastRet为-1,禁止连续调用,防止元素混乱
        if (lastRet < 0)
            throw new IllegalStateException();
        // 若距离头指针较近,结构移动的是指针前的元素不需要修改当前指针位置,本操作后会调用next(),也不用担心读取到移动过来的元素
        if (delete(lastRet)) {
            cursor = (cursor - 1) & (elements.length - 1);
            fence = tail;
        }
        // 重置前一个指针位置,防止连续修改结构
        lastRet = -1;
    }

    // 根据传入的函数式接口,循环处理当前指针后的元素
    public void forEachRemaining(Consumer<? super Integer> action) {
        Objects.requireNonNull(action);
        Object[] a = elements;
        // 不直接使用类变量,是因为类变量可能会被其他操作改变
        int m = a.length - 1, f = fence, i = cursor;
        // 将当前指针设置为尾指针
        cursor = f;
        // 循环至缓存的当前指针等于尾指针
        while (i != f) {
            @SuppressWarnings("unchecked") Integer e = (Integer)a[i];
            i = (i + 1) & m;
            if (e == null)
                throw new ConcurrentModificationException();
            // 处理元素
            action.accept(e);
        }
    }
}
// 循环方法 forEachRemaining 示例
@Test
void contextLoads(){
    Integer[] elements = new Integer[]{7,1,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    Iterator<Integer> iterator = arrayDeque.iterator();
    iterator.next();
    // 1 1 2 3 4 5 6
    iterator.forEachRemaining(l -> System.out.print(l + " "));
}
// 错误示例:生成迭代器后调用对象的removeFirst()方法
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{7,1,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    Iterator<Integer> iterator = arrayDeque.iterator();
    arrayDeque.removeFirst();
    // java.util.ConcurrentModificationException
    iterator.next();
}
// 错误示例:连续调用迭代器的remove()方法
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{7,1,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    Iterator<Integer> iterator = arrayDeque.iterator();
    iterator.next();
    iterator.remove();
    // java.lang.IllegalStateException
    iterator.remove();
}

02.迭代器(逆序) DescendingIterator

注意
1. 本方法未重写循环处理元素的方法
2. 本迭代器取值时取的是指针前的值而不是指针后,但是根据下标获取值时获取的就是指定下标的值,因此调用next()需要先将指针前移再获取下标的值
3. 直接使用对象的removeFirst()addFrist()等方法会导致结构变更,且迭代器并未进行同步,如需移除请使用迭代器内部的remove()方法
4. 迭代器中的remove()方法禁止连续使用,因为连续使用回导致对象中元素混乱

// 源码
private class DescendingIterator implements Iterator<E> {

    // 当前指针位置(指针在索引前),逆序使用尾索引
    private int cursor = tail;
    // 尾指针位置(指针在索引前),逆序使用头索引
    private int fence = head;
    // 上一个指针位置(结构变更标识),默认-1
    private int lastRet = -1;

    // 当前指针位置等于尾指针位置即表示后续没有元素了
    public boolean hasNext() {
        return cursor != fence;
    }

    // 获取当前指针后的元素
    public Integer next() {
        // 当前指针位置等于尾指针位置(当前指针后无元素)
        if (cursor == fence)
            throw new NoSuchElementException();
        // 指针前移(根据指针获取元素均是获取指针后的元素,因此需要先前移,这样获取到的元素才是原当前指针前的数据)
        cursor = (cursor - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        Integer result = (Integer) elements[cursor];
        // 当前指针并不等于尾指针(当前指针后应当有元素),但是元素为null(可能结构变更了)
        if (head != fence || result == null)
            throw new ConcurrentModificationException();
        // 设置当前指针为上一个指针位置
        lastRet = cursor;
        return result;
    }

    // 移除当前指针后的元素
    public void remove() {
        // 在本方法前已进行了结构操作并未进行next()操作,当前指针后已无元素
        if (lastRet < 0)
            throw new IllegalStateException();
        // 若距离尾指针较近,结构移动的是指针后的元素不需要修改当前指针位置(本迭代器获取的是指针前的元素)
        // 本操作后会调用next(),也不用担心读取到移动过来的元素
        if (!delete(lastRet)) {
            cursor = (cursor + 1) & (elements.length - 1);
            fence = head;
        }
        // 重置前一个指针位置,防止连续修改结构
        lastRet = -1;
    }
}
// 错误示例:生成迭代器后调用对象的`removeFirst()`方法
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{7,1,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    // descendingIterator() 生成的是反向迭代器
    Iterator<Integer> iterator = arrayDeque.descendingIterator();
    arrayDeque.removeFirst();
    // java.util.ConcurrentModificationException
    iterator.next();
}
// 错误示例:连续调用迭代器的remove()方法
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{7,1,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    // descendingIterator() 生成的是反向迭代器
    Iterator<Integer> iterator = arrayDeque.descendingIterator();
    iterator.next();
    iterator.remove();
    // java.lang.IllegalStateException
    iterator.remove();
}

03.可拆分迭代器DeqSpliterator<E>

注意
1. 本方法主要用于并行时使用迭代器,可以提高效率,但并非一定会拆分,有些情况会拆分失败
2. 拆分规则 —— 若已经开始遍历或元素太少会拆分失败,若可以拆分则每次拆分一半,将前面一半的元素拆分成一个新的可拆分迭代器,奇数个元素拆分出去的个数是较少的部分(例:元素一共有7个,则拆分出去的是前3个元素)
3. 可拆分迭代器全部的特性 —— ORDERED(定义排序)、DISTINCT(唯一)、IMMUTABLE(不可变)、NONNULL(不可为NULL)、SIZED(有固定大小)、SORTED(有序)、CONCURRENT(可并发修改)、SUBSIZED(拆分后子大小不变)
4. 本可拆分迭代器特性 —— ORDERED(定义排序)、NONNULL(不可为NULL)、SIZED(有固定大小)、SUBSIZED(拆分后子大小不变)
5. 本方法为JDK 1.8新增方法,支持JDK 1.8及以后版本使用

// 源码
static final class DeqSpliterator<E> implements Spliterator<E> {

    // 双端队列对象
    private final ArrayDeque<E> deq;
    // 尾部索引,默认-1
    private int fence;  
    // 当前索引,遍历和分割的时候会修改
    private int index;  

    // 构造器
    DeqSpliterator(ArrayDeque<E> deq, int origin, int fence) {
        this.deq = deq;
        this.index = origin;
        this.fence = fence;
    }

    private int getFence() { 
        int t;
        // 若fence小于0对参数进行初始化,给fence赋值队列的尾索引,给index赋值队列的头索引
        if ((t = fence) < 0) {
            t = fence = deq.tail;
            index = deq.head;
        }
        return t;
    }

    // 拆分迭代器,并不一定成功
    public DeqSpliterator<E> trySplit() {
        int t = getFence(), h = index, n = deq.elements.length;
        // 判断是否有元素,且元素数量不是很少才进行拆分
        if (h != t && ((h + 1) & (n - 1)) != t) {
            if (h > t)
                // 为了便于计算一般的个数,先将循环队列头索引前面的值暂时扩展到后面
                // (只获取计算后的值,并非真的扩容移动元素)
                t += n;
            // 计算头索引后一半元素处的索引    
            int m = ((h + t) >>> 1) & (n - 1);
            return new DeqSpliterator<>(deq, h, index = m);
        }
        return null;
    }

    // 根据传入的函数式接口,循环处理当前索引及之后的元素,实现与上述的普通迭代器基本一致
    public void forEachRemaining(Consumer<? super E> consumer) {
        if (consumer == null)
            throw new NullPointerException();
        Object[] a = deq.elements;
        int m = a.length - 1, f = getFence(), i = index;
        index = f;
        while (i != f) {
            @SuppressWarnings("unchecked") E e = (E)a[i];
            i = (i + 1) & m;
            if (e == null)
                throw new ConcurrentModificationException();
            consumer.accept(e);
        }
    }

    // 根据传入的函数式接口,处理当前索引的元素,本方法会后移索引
    public boolean tryAdvance(Consumer<? super E> consumer) {
        if (consumer == null)
            throw new NullPointerException();
        Object[] a = deq.elements;
        int m = a.length - 1, f = getFence(), i = index;
        // 判断是否已到尾索引,是则返回false
        if (i != fence) {
            @SuppressWarnings("unchecked") E e = (E)a[i];
            index = (i + 1) & m;
            // 若元素为null,抛出 ConcurrentModificationException 异常
            if (e == null)
                throw new ConcurrentModificationException();
            consumer.accept(e);
            return true;
        }
        return false;
    }

    // 当前队列还剩多少元素
    public long estimateSize() {
        // 头尾索引直接距离 = 尾索引 - 当前索引
        int n = getFence() - index;
        if (n < 0)
            // 小于0则表示是循环数组,需要加上数组最大索引计算个数,否则直接返回即可
            n += deq.elements.length;
        return (long) n;
    }

    // 迭代器拥有的特性,此处返回的是特性或运算后的结果
    // ORDERED(定义排序)、NONNULL(不可为NULL)、SIZED(有固定大小)、SUBSIZED(拆分后子大小不变)
    @Override
    public int characteristics() {
        return Spliterator.ORDERED | Spliterator.SIZED |
            Spliterator.NONNULL | Spliterator.SUBSIZED;
    }
}
// 示例
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{0,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    arrayDeque.removeFirst();
    arrayDeque.add(7);
    arrayDeque.removeFirst();
    arrayDeque.add(8);
    // [8, null, 2, 3, 4, 5, 6, 7]
    Spliterator<Integer> spliterator = arrayDeque.spliterator();
    // 16720
    System.out.println(spliterator.characteristics());
    // 剩余元素个数:7
    System.out.println(spliterator.estimateSize());
    // 2
    spliterator.tryAdvance(System.out::println);
    // 3
    spliterator.tryAdvance(System.out::println);
    // 剩余元素个数:5
    System.out.println(spliterator.estimateSize());
    // 4 5 6 7 8
    spliterator.forEachRemaining(l -> System.out.print(l + " "));
}
// 拆分示例
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{0,1,2,3,4,5,6};
    ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(Arrays.asList(elements));
    arrayDeque.removeFirst();
    arrayDeque.add(7);
    arrayDeque.removeFirst();
    arrayDeque.add(8);
    // [8, null, 2, 3, 4, 5, 6, 7]
    Spliterator<Integer> spliterator = arrayDeque.spliterator();
    Spliterator<Integer> trySplit = spliterator.trySplit();
    // 5 6 7 8
    spliterator.forEachRemaining(l -> System.out.print(l + " "));
    System.out.println();
    // 2 3 4
    trySplit.forEachRemaining(l -> System.out.print(l + " "));
}

私有方法

01.计算当前双端队列所需容量 calculateSize(int numElements)

参数numElements —— 指定的容量

注意
1. 双端队列储存元素的数组默认容量为8,但双端队列未指定容量时的初始默认容量并不是8,而是16
2. 本方法主要是为了保证容量为2的幂,若指定容量小于初始容量则返回初始容量8,否则计算出大于等于指定容量的最小2的幂
3. 因为最小容量为8,即0100,所以右移操作有效的右移位数最小为1,即右移1位,若数值在9~16之间,实际进行initialCapacity |= (initialCapacity >>> 1)就可以得到最小2的幂,而17~64就必须执行initialCapacity |= (initialCapacity >>> 2)才可以得到最小2的幂,以此类推
4. 此处最大右移位数为16是因为initialCapacityint型的,最大为 231-1,而进行到最后一步initialCapacity |= (initialCapacity >>> 16)已右移了31位,无需再次右移了

// 源码
private static int calculateSize(int numElements) {
    // MIN_INITIAL_CAPACITY = 8
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // 大于等于8时进行右移操作
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        // 通过右移来计算最小2的幂,指定数值越小有效右移数越小
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        // 以上右移方法会将值计算为末尾均为1的值,需要使用++加一使其变为最小2的幂
        initialCapacity++;
        // 若右移后溢出了(数值小于0)则将其右移一位,容量变为2^30
        if (initialCapacity < 0)   
            initialCapacity >>>= 1;
    }
    return initialCapacity;
}
// 解析示例
@Test
void contextLoads() {
    int initialCapacity = 8;
    int numElements = 159;
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   
            initialCapacity >>>= 1;
    }
    // 256
    System.out.println(initialCapacity);
}

02.初始化元素数组 allocateElements(int numElements)

参数numElements —— 指定的容量

注意
1. elements是用于储存双端队列元素的数组
2. 本方法主要用于指定了初始化内容调用构造器时创建数组对象

// 源码
private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}

02.初始化元素数组 allocateElements(int numElements)

参数numElements —— 指定的容量

注意
1. elements是用于储存双端队列元素的数组
2. 本方法主要用于指定了初始化内容调用构造器时创建数组对象

// 源码
private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}
03.数组扩容 doubleCapacity()

拓展assert —— 关键字(断言),用于判断后面的表达式结果是否为true,不是则抛出AssertionError异常

注意
1. 双端队列扩容机制为扩容为大于原容量的最小2的幂
2. 本方法仅当数组满了才会调用
3. 双端队列尾索引有可能小于头索引,当头索引不等于0,而最大索引值已使用且头索引前未使用,尾索引会从0索引开始依次后移 (例:数组[2,null,5,6,9,10,6,9],头索引为2,则此时尾索引为1,读取出数组为[5,6,9,10,6,9,2,null])

局部变量说明
1. p —— 头索引
2. n —— 数组原容量
3. r —— 包括头索引位置其后可存放元素个数,例如头索引1,容量8,则可存放元素个数为7
4. newCapacity —— 扩容后的容量(左移一位,例8->16)

// 源码
private void doubleCapacity() {
    // 判断数组是否已满(头尾索引一致则表示数组为空或数组已满)
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; 
    int newCapacity = n << 1;
    // 判断扩容后的容量是否溢出(超过int型最大值),溢出则抛出IllegalStateException异常
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    // 创建新数组,因为此处储存数组元素的是非扩容型数组,所以需要新创建一个新的数组    
    Object[] a = new Object[newCapacity];
    // 拷贝原数组元素至新数组,由于ArrayDeque为双端队列,头索引并不一定为0
    // 所以要先将头索引后的元素拷贝至新数组,再将头索引之前的元素拷贝至新数组
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    // 重置双端队列参数(头索引、尾索引、储存元素的数组)
    elements = a;
    // 因为上面拷贝元素时先拷贝了头索引之后的元素,因此新数组头索引为0
    head = 0;
    // 尾索引为原数组的长度(即原数组头索引后最后一个元素的索引 + 1)
    tail = n;
}
// 解析示例
@Test
void contextLoads() {

    Object[] elements = new Object[]{1,2,3,4,8,17,16,95};

    // 头索引为1,数组满时尾索引也会变为1
    int head = 1;
    int tail = 1;

    // 源码逻辑
    assert head == tail;
    // 头索引 1
    int p = head;
    // 原数组容量 8
    int n = elements.length;
    // 头索引后元素个数(包括头索引) 7
    int r = n - p;
    // 扩容后容量 16
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    // 生成容量为16的数组
    Object[] a = new Object[newCapacity];
    // 拷贝头索引后的元素(包括头索引)
    System.arraycopy(elements, p, a, 0, r);
    // 打印:[2, 3, 4, 8, 17, 16, 95, null, null, null, null, null, null, null, null, null]
    System.out.println(Arrays.toString(a));
    // 拷贝头索引前的元素
    System.arraycopy(elements, 0, a, r, p);
    // 打印:[2, 3, 4, 8, 17, 16, 95, 1, null, null, null, null, null, null, null, null]
    System.out.println(Arrays.toString(a));
    elements = a;
    // 设置头索引为0
    head = 0;
    // 设置尾索引为8
    tail = n;
}

04.拷贝元素到指定数组 copyElements(T[] a)

参数a —— 用于接收拷贝出元素的数组

注意
1. 本方法未判断指定数组大小是否可以存放全部当前队列元素,在使用时需要先判断,否则会抛出ArrayIndexOutOfBoundsException异常
2. 若指定的元素内有元素,从0下标开始拷贝元素,会将之前的元素覆盖,但不会清空原数组,本方法也没有将分隔出置0,因此不建议使用已有元素的数组接收拷贝元素

// 源码
private <T> T[] copyElements(T[] a) {
    // 根据头索引前是否有元素,决定直接复制还是分成两部分复制,拷贝后的数组为普通数组,没有头尾索引
    if (head < tail) {
        System.arraycopy(elements, head, a, 0, size());
    } else if (head > tail) {
        int headPortionLen = elements.length - head;
        System.arraycopy(elements, head, a, 0, headPortionLen);
        System.arraycopy(elements, 0, a, headPortionLen, tail);
    }
    return a;
}
// 解析示例1
@Test
void contextLoads6() {
    Object[] elements = new Object[]{1,2,3,4,8,17,16,95,15,11};
    Object[] a = new Object[16];
    int head = 0;
    int tail = 9;
    int size = elements.length;

    // 源码逻辑
    if (head < tail) {
        // 头索引小于尾索引,正常的数组顺序,直接拷贝有元素的部分即可
        System.arraycopy(elements, head, a, 0,size);
    } else if (head > tail) {
        // 尾索引小于头索引,有部分元素在前面,需要查分开拷贝
        int headPortionLen = elements.length - head;
        // 先拷贝头索引之后的元素(包括头索引)
        System.arraycopy(elements, head, a, 0, headPortionLen);
        // 再拷贝头索引之前的元素
        System.arraycopy(elements, 0, a, headPortionLen, tail);
    }

    // [1, 2, 3, 4, 8, 17, 16, 95, 15, 11, null, null, null, null, null, null]
    System.out.println(Arrays.toString(a));
}
// 解析示例2:指定数组有元素
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,17,16,95,15,11};
    Object[] a = new Object[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};
    int head = 0;
    int tail = 9;
    int size = elements.length;

    // 源码逻辑
    if (head < tail) {
        // 头索引小于尾索引,正常的数组顺序,直接拷贝有元素的部分即可
        System.arraycopy(elements, head, a, 0,size);
    } else if (head > tail) {
        // 尾索引小于头索引,有部分元素在前面,需要查分开拷贝
        int headPortionLen = elements.length - head;
        // 先拷贝头索引之后的元素(包括头索引)
        System.arraycopy(elements, head, a, 0, headPortionLen);
        // 再拷贝头索引之前的元素
        System.arraycopy(elements, 0, a, headPortionLen, tail);
    }

    // [1, 2, 3, 4, 8, 17, 16, 95, 15, 11, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    System.out.println(Arrays.toString(a));
}

05.数组校验 checkInvariants()

注意
1. 本方法用于判断队列是否可以移除指定元素,移除指定元素操作会改变结构,导致元素移动因此需要加判断
2. 本方法主要针对两种情况判断,一个是数组不能已满,一个是数组结构必须合理

// 源码
private void checkInvariants() {
    // 判断1:判断尾索引处是否有元素,有元素抛出异常(此时数组可能已满或异常)
    assert elements[tail] == null;
    // 判断2:头索引等于尾索引时再次判断了一遍数组是否已满(头索引不为null表示尾索引也不为null)
    // 判断3:数组非空未满,若尾索引前一位索引的元素为空则抛出异常(出现了未知错误导致尾索引前一位元素无值)
    assert head == tail ? elements[head] == null :
        (elements[head] != null && elements[(tail - 1) & (elements.length - 1)] != null);
    // 判断4:数组非空未满,若头索引前一位索引的元素有值则抛出异常
    // (出现了未知错误导致数组未满,尾索引未达到最后一位,头索引前一位却有值)
    assert elements[(head - 1) & (elements.length - 1)] == null;
}
// 异常示例1:数组已满
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,17,16,95,15,11};
    int head = 0;
    int tail = 0;
    int size = elements.length;

    // 数组已满 java.lang.AssertionError
    assert elements[tail] == null;
    assert head == tail ? elements[head] == null :
            (elements[head] != null &&
                    elements[(tail - 1) & (elements.length - 1)] != null);
    assert elements[(head - 1) & (elements.length - 1)] == null;
}
// 异常示例2:数组异常,尾索引后面有位置却没有后移
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,17,16,95,15,null};
    int head = 0;
    int tail = 8;
    int size = elements.length;

    // 数组异常,尾索引后面有位置却没有后移 java.lang.AssertionError
    assert elements[tail] == null;
    assert head == tail ? elements[head] == null :
            (elements[head] != null &&
                    elements[(tail - 1) & (elements.length - 1)] != null);
    assert elements[(head - 1) & (elements.length - 1)] == null;
}
// 异常示例3:数组非空未满,尾索引前一位索引的元素为空
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,17,16,95,null,null};
    int head = 0;
    int tail = 9;
    int size = elements.length;

    assert elements[tail] == null;
    // 头尾索引不一致,尾索引前一位索引的元素为空
    // 此处 [(tail - 1) & (elements.length - 1)] = 8 
    // java.lang.AssertionError
    assert head == tail ? elements[head] == null : 
            (elements[head] != null && elements[(tail - 1) & (elements.length - 1)] != null);
    assert elements[(head - 1) & (elements.length - 1)] == null;
}
// 异常示例4:数组非空未满,尾索引未到达最后一个位置(头索引前),但最后一个位置已有值
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,17,16,95,11,null};
    int head = 1;
    int tail = 9;
    int size = elements.length;

    assert elements[tail] == null;
    assert head == tail ? elements[head] == null :
            (elements[head] != null && elements[(tail - 1) & (elements.length - 1)] != null);
    System.out.println((head - 1) & (elements.length - 1));
    // 尾索引未到达最后一个位置(头索引前),但最后一个位置已有值
    // 此处 [(head - 1) & (elements.length - 1)] = 0
    // java.lang.AssertionError
    assert elements[(head - 1) & (elements.length - 1)] == null;
}

06.删除指定元素 delete(int i)

参数i —— 要删除的元素的索引值

注意
1. 本方法会改变结构导致元素移动
2. 队列已满且未扩容的情况下不能删除指定元素
3. 对于调用本方法的removeFirstOccurrence(Object o)removeLastOccurrence(Object o),本方法中返回的值并未使用,上述两个方法没有进行返回值的判断处理
4. 本方法的返回值不能作为是否删除成功的标识,其只标识移动的是头索引(返回false)还是尾索引(返回true)

备注
1. 本详解中的正常顺序包括整个数组就是正常顺序和数组为循环数组但是指定下标与头索引/尾索引之间的关系是正常顺序两种情况
2. 本详解中的循环顺序仅表示指定下标与头索引/尾索引之间的关系是非正常(循环)顺序

// 源码
private boolean delete(int i) {
    // 数组校验,数组不能已满,结构必须合理
    checkInvariants();
    final Object[] elements = this.elements;
    // 数组最后一位元素下标(此处与头索引和尾索引无关) 
    final int mask = elements.length - 1;
    final int h = head;
    final int t = tail;
    // 需移除的索引到达头索引的距离
    final int front = (i - h) & mask;
    // 需移除的索引到达尾索引的距离
    final int back  = (t - i) & mask;

    // 此处计算了尾索引到头索引的距离,与指定索引到头索引的距离进行比较,以下两种情况会抛异常
    // 情况1:不变性检测后,增加元素后队列满了这种情况(距离为0)
    // 情况2:不变性检测后,移除元素导致指定索引到头索引的距离小于尾索引到头索引的距离(指定元素可能已被删除)
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();

    // 判断索引元素距头索引远还是距尾索引远,选择元素较少的方向移动元素和头尾索引
    // 距头索引较近,变更头索引,移除成功返回false
    if (front < back) {
        // 头索引小于等于指定索引(正常的数组顺序,从前往后)
        if (h <= i) {
            // 复制指定索引前面的元素全部后移一位(复制到后面)
            System.arraycopy(elements, h, elements, h + 1, front);
        } else { 
            // 头索引大于指定索引(非正常的数组顺序,循环型数组循环部分)
            // 将索引[0~指定索引元素前]的元素后移一位(复制到后面)
            System.arraycopy(elements, 0, elements, 1, i);
            // 将数组最大索引值复制至索引0
            elements[0] = elements[mask];
            // 将头索引后的元素全部后移一位(正常顺序)
            System.arraycopy(elements, h, elements, h + 1, mask - h);
        }
        // 原头索引设置为null,此索引已不再是头索引了
        elements[h] = null;
        // 重新计算头索引并赋值(原头索引后一位)
        head = (h + 1) & mask;
        return false;
    } else {
        // 距尾索引较近,变更尾索引,移除成功返回true
        // 指定索引小于尾索引(正常的数组顺序,从前到后)
        if (i < t) { 
            // 直接将指定索引后尾索引之前的元素前移一位(复制到前面)
            System.arraycopy(elements, i + 1, elements, i, back);
            // 尾索引前移一位
            tail = t - 1;
        } else { 
            // 指定索引大于等于尾索引(非正常的数组顺序,循环型数组循环部分)
            // 将指定索引后的元素前移一位
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            // 将索引0复制至数组最大索引位
            elements[mask] = elements[0];
            // 将索引[1~尾索引前]元素前移一位
            System.arraycopy(elements, 1, elements, 0, t);
            // 重新设置尾索引并赋值(原尾索引前一位)
            tail = (t - 1) & mask;
        }
        return true;
    }
}
// 测试源码调用方法:后续示例测试时调用本方法,原方法为私有方法
Map<String,Object> testDelete(Object[] elements, int head, int tail, int i) {
        
    Boolean result = null;
    
    final int mask = elements.length - 1;
    final int h = head;
    final int t = tail;
    final int front = (i - h) & mask;
    final int back  = (t - i) & mask;
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();
    if (front < back) {
        if (h <= i) {
            System.arraycopy(elements, h, elements, h + 1, front);
        } else {
            System.arraycopy(elements, 0, elements, 1, i);
            elements[0] = elements[mask];
            System.arraycopy(elements, h, elements, h + 1, mask - h);
        }
        elements[h] = null;
        head = (h + 1) & mask;
        // 原 return false
        result = false;
    } else {
        if (i < t) {
            System.arraycopy(elements, i + 1, elements, i, back);
            tail = t - 1;
        } else {
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            elements[mask] = elements[0];
            System.arraycopy(elements, 1, elements, 0, t);
            tail = (t - 1) & mask;
        }
        // 原 return true
        result = true;
    }

    // 测试结果返回
    Map<String,Object> map = new HashMap<>(4);
    map.put("result",result);
    map.put("head",head);
    map.put("tail",tail);
    map.put("elements",Arrays.toString(elements));

    return map;
}
// 示例1:距头索引近,正常顺序
@Test
void contextLoads() {
    // 模拟数据
    final Object[] elements = new Object[]{1, null, 9, 2, 8, 4, 8, 18};
    int head = 2;
    int tail = 1;
    // 距头索引近,正常顺序
    int i = 2;
    // 调用测试方法
    Map<String, Object> map = testDelete(elements, head, tail, i);
    // {result=false, head=3, tail=1, elements=[1, null, null, 2, 8, 4, 8, 18]}
    System.out.println(map);
}
// 示例2:距尾索引近,循环顺序循环部分
@Test
void contextLoads(){
    // 模拟数据
    final Object[] elements = new Object[]{1, null, 9, 2, 8, 4, 8, 18};
    int head = 2;
    int tail = 1;
    // 距尾索引近,循环顺序
    int i = 0;
    // 调用测试方法
    Map<String, Object> map = testDelete(elements, head, tail, i);
    // {result=true, head=2, tail=0, elements=[null, null, 9, 2, 8, 4, 8, 18]}
    System.out.println(map);
}
// 示例3:距头索引近,循环顺序循环部分
@Test
void contextLoads() {
    // 模拟数据
    final Object[] elements = new Object[]{9, 2, 8, 4, 1, null, 8, 18};
    int head = 6;
    int tail = 5;
    // 距头索引近,循环顺序
    int i = 0;
    // 调用测试方法
    Map<String, Object> map = testDelete(elements, head, tail, i);
    // {result=false, head=7, tail=5, elements=[18, 2, 8, 4, 1, null, null, 8]}
    System.out.println(map);
}
// 示例4:距尾索引近,正常顺序
@Test
void contextLoads() {
    // 模拟数据
    final Object[] elements = new Object[]{9, 2, 8, 4, 1, null, 8, 18};
    int head = 6;
    int tail = 5;
    // 距尾索引近,正常顺序
    int i = 3;
    // 调用测试方法
    Map<String, Object> map = testDelete(elements, head, tail, i);
    // {result=true, head=6, tail=4, elements=[9, 2, 8, 1, null, null, 8, 18]}
    System.out.println(map);
}

公有方法

01.新增元素至首位(无返回值) addFirst(E e)

参数e —— 新增的元素

注意
1. 借助该方法可以将此对象看作堆栈(FIFO),数组遍历是从头到尾的,每次将新元素新增至首位即可实现先进先出(堆栈特点)
2. 该方法等价于堆栈的push(E e)方法
3. 本方法并不是在头索引处依次后移再插入实现的,而是直接在头索引前一个位置(头索引即则是最后一个元素位置)处储存元素,再重新设置头索引实现的,效率比一般的数组高

// 源码
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    // 若新增元素后容量满了则扩容
    if (head == tail)
        doubleCapacity();
}
// 解析示例
@Test
void contextLoads() {

    Object[] elements = new Object[]{1,2,3,4,8,null,null,null};
    Integer e = 16;
    int head = 0;
    int tail = 5;

    // 核心源码
    // head = (0 - 1) & (8 - 1) -> head = 7
    // 此处未标注符号位(-1为已取反+1后的值) 1111 & 0111 = 0111
    elements[head = (head - 1) & (elements.length - 1)] = e;

    // 此处不是ArrayDeque,而是模拟的内部数组,因此输出顺序不同与ArrayDeque对象 [1,2,3,4,8,null,null,16]  
    System.out.println(Arrays.toString(elements));

}
// 方法示例
@Test
void contextLoads() {
    // 与解析示例相同的初始元素,注意模拟初始元素不能有null,否则addAll会报错
    Object[] elements = new Object[]{1,2,3,4,8};

    ArrayDeque arrayDeque = new ArrayDeque();
    arrayDeque.addAll(Arrays.asList(elements));
    arrayDeque.addFirst(16);
    // ArrayDeque未获取为null的元素,且根据头索引和尾索引使其有序
    // [16, 1, 2, 3, 4, 8]
    System.out.println(arrayDeque);
}

02.新增元素至末尾(无返回值) addLast(E e)

参数e —— 新增的元素

注意
1. 借助该方法可以将此对象看作队列(FILO),数组遍历是从头到尾的,每次将新元素新增至末尾即可实现先进后出(队列特点)
2. 该方法等价于堆栈的add(E e)
3. 需注意队列不能储存null

// 源码
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
// 解析示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{null,null,null,1,2,5,6,null};
    Integer e = 16;
    int head = 3;
    int tail = 7;

    // 核心源码
    elements[tail] = e;
    // tail = (7 + 1) & (8 - 1) -> tail = 0
    // 1000 & 0111 = 0000
    tail = (tail + 1) & (elements.length - 1);

    // 此处不是ArrayDeque因此输出顺序不同 [null, null, null, 1, 2, 5, 6, 16]
    System.out.println(Arrays.toString(elements));
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,5,6};

    ArrayDeque arrayDeque = new ArrayDeque();
    arrayDeque.addAll(Arrays.asList(elements));
    arrayDeque.addLast(16);
    // [1, 2, 5, 6, 16]
    System.out.println(arrayDeque);
}

03.新增元素至首位(有返回值) offerFirst(E e)

参数e —— 新增的元素

注意:本方法与addFirst(E e)唯一的区别就是有返回值,便于判断

// 源码
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8};

    ArrayDeque arrayDeque1 = new ArrayDeque();
    arrayDeque1.addAll(Arrays.asList(elements));
    // 无返回值
    arrayDeque1.addFirst(16);
    // [16, 1, 2, 3, 4, 8]
    System.out.println(arrayDeque1);

    ArrayDeque arrayDeque2 = new ArrayDeque();
    arrayDeque2.addAll(Arrays.asList(elements));
    // 有返回值 true
    System.out.println(arrayDeque2.offerFirst(16));
    // [16, 1, 2, 3, 4, 8]
    System.out.println(arrayDeque2);
}

04.新增元素至末尾(有返回值) offerLast(E e)

参数e —— 新增的元素

注意
1. 本方法与addLast(E e)唯一的区别就是有返回值,便于判断
2. 本方法等效于队列的offer()方法

// 源码
public boolean offerLast(E e) {
    addLast(e);
    return true;
}
// 示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8};

    ArrayDeque arrayDeque1 = new ArrayDeque();
    arrayDeque1.addAll(Arrays.asList(elements));
    // 无返回值
    arrayDeque1.addLast(16);
    // [1, 2, 3, 4, 8, 16]
    System.out.println(arrayDeque1);

    ArrayDeque arrayDeque2 = new ArrayDeque();
    arrayDeque2.addAll(Arrays.asList(elements));
    // 有返回值 true
    System.out.println(arrayDeque2.offerLast(16));
    // [1, 2, 3, 4, 8, 16]
    System.out.println(arrayDeque2);
}

05.移除首位元素(判空) removeFirst()

注意
1. 本方法既可以作为堆栈的出栈(等价于pop(e)),又可以作为队列的出队(等价于remove()),因为它们在该对象中先出的都是首位元素
2. 本方法调用的是popFirst(),只是在调用后判断了是否有移除元素,若未移除元素(队列无元素)则抛出NoSuchElementException异常,而直接调用popFirst()返回null
3. 本方法会返回被移除的元素

// 源码
public E removeFirst() {
    E x = pollFirst();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}
// 示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8};

    // 正常移除
    ArrayDeque arrayDeque1 = new ArrayDeque();
    arrayDeque1.addAll(Arrays.asList(elements));
    // 1
    System.out.println(arrayDeque1.removeFirst());
    // [2, 3, 4, 8]
    System.out.println(arrayDeque1);

    // pollFirst() 空队列移除
    ArrayDeque arrayDeque2 = new ArrayDeque();
    // null
    System.out.println(arrayDeque2.pollFirst());
    // []
    System.out.println(arrayDeque2);

    // removeFirst() 空队列移除
    ArrayDeque arrayDeque3 = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque3.removeFirst());
    System.out.println(arrayDeque3);
}

06.移除首位元素(不判空) pollFirst()

注意
1. 本方法基本等价于removeFirst(),只是本方法若队列为空返回null,而removeFirst()抛出异常
2. 本方法等效于队列的poll()方法
3. 本方法会返回被移除的元素

// 源码
public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    // 获取移除的元素便于返回
    E result = (E) elements[h];
    // 如果当前队列为空此处为null(双端队列不能储存null值),直接返回null
    if (result == null)
        return null;
    // 设置被移除的元素为null(数组储存需要设置为null)
    elements[h] = null;     
    // 计算新的头索引
    head = (h + 1) & (elements.length - 1);
    return result;
}
// 解析示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,7,8,5};
    int head = 7;

    int h = head;
    Integer result = (Integer) elements[h];
    elements[h] = null;
    // (7 + 1) & (8 - 1) = (8 & 7) = (1000 & 0111 = 0000) = 0
    head = (h + 1) & (elements.length - 1);
    // 5
    System.out.println(result);
    // [1, 2, 3, 4, 8, 7, 8, null]
    System.out.println(Arrays.toString(elements));
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8};

    // 正常移除
    ArrayDeque arrayDeque1 = new ArrayDeque();
    arrayDeque1.addAll(Arrays.asList(elements));
    // 1
    System.out.println(arrayDeque1.pollFirst());
    // [2, 3, 4, 8]
    System.out.println(arrayDeque1);

    // pollFirst() 空队列移除
    ArrayDeque arrayDeque2 = new ArrayDeque();
    // null
    System.out.println(arrayDeque2.pollFirst());
    // []
    System.out.println(arrayDeque2);

    // removeFirst() 空队列移除
    ArrayDeque arrayDeque3 = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque3.removeFirst());
    System.out.println(arrayDeque3);
}

07.移除末尾元素(判空) removeLast()

注意
1. 本方法调用的是pollLast(),只是在调用后判断了是否有移除元素,若未移除元素(队列无元素)则抛出NoSuchElementException异常,而直接调用pollLast()返回null
2. 本方法会返回移除的元素

// 源码
public E removeLast() {
    E x = pollLast();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}
// 示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8};

    // 正常移除
    ArrayDeque arrayDeque1 = new ArrayDeque();
    arrayDeque1.addAll(Arrays.asList(elements));
    // 8
    System.out.println(arrayDeque1.removeLast());
    // [1, 2, 3, 4]
    System.out.println(arrayDeque1);

    // pollLast() 空队列移除
    ArrayDeque arrayDeque2 = new ArrayDeque();
    // null
    System.out.println(arrayDeque2.pollLast());
    // []
    System.out.println(arrayDeque2);

    // removeLast() 空队列移除
    ArrayDeque arrayDeque3 = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque3.removeLast());
    System.out.println(arrayDeque3);
}

08.移除末尾元素(不判空) pollLast()

注意
1. 本方法基本等价于removeLast(),只是本方法若队列为空返回null,而removeLast()抛出异常
2. 本方法返回被移除的元素

// 源码
public E pollLast() {
    // 计算新的尾索引
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    // 获取移除的元素便于返回
    E result = (E) elements[t];
    // 若当前数组为空此处为null(双端队列不能储存null值),直接返回null值
    if (result == null)
        return null;
    // 设置被移除的元素为null(数组储存需要设置为null)    
    elements[t] = null;
    tail = t;
    return result;
}
// 解析示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{null,2,3,4,8,7,8,5};
    int tail = 0;

    // (0 - 1) & (8 - 1) = -1 & 7 = 1111 & 0111 = 0111 = 7
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    Integer result = (Integer) elements[t];
    elements[t] = null;
    tail = t;
    // 5
    System.out.println(result);
    // [null, 2, 3, 4, 8, 7, 8, null]
    System.out.println(Arrays.toString(elements));
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8};

    // 正常移除
    ArrayDeque arrayDeque1 = new ArrayDeque();
    arrayDeque1.addAll(Arrays.asList(elements));
    // 8
    System.out.println(arrayDeque1.pollLast());
    // [1, 2, 3, 4]
    System.out.println(arrayDeque1);

    // pollLast() 空队列移除
    ArrayDeque arrayDeque2 = new ArrayDeque();
    // null
    System.out.println(arrayDeque2.pollLast());
    // []
    System.out.println(arrayDeque2);

    // removeLast() 空队列移除
    ArrayDeque arrayDeque3 = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque3.removeLast());
    System.out.println(arrayDeque3);
}

09.获取头元素(判空) getFirst()

注意
1. 本方法若队列为空会抛出NoSuchElementException异常
2. 本方法等效于队列的element()方法
3. 本方法等价于peekFirst(),唯一的区别是若队列为空,peekFirst()直接返回null,而本方法会抛出异常

// 源码
public E getFirst() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[head];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}
// 错误示例
@Test
void contextLoads() {
    ArrayDeque arrayDeque = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque.getFirst());
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,3,4,8,18,19,11};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // 1
    System.out.println(arrayDeque.getFirst());
    // [1, 2, 3, 4, 8, 18, 19, 11]
    System.out.println(arrayDeque);
}

10.获取头元素(不判空) peekFirst()

注意
1. 本方法等价于getFirst(),唯一的区别是若队列为空,getFirst()会抛出异常,而本方法是直接返回null
2. 本方法等效于队列的peek()方法
3. 可以根据本方法的返回值判断对象是否为空(为空会返回null),但不建议这样使用

// 源码
public E peekFirst() {
    // 此处若当前数组位空返回null
    return (E) elements[head];
}
// 方法示例
@Test
void contextLoads() {
    ArrayDeque arrayDeque = new ArrayDeque();
    // null
    System.out.println(arrayDeque.peekFirst());
}

11.获取末尾元素(判空) getLast()

注意:本方法等价于peekLast()方法,区别是若当前队列为空,本方法抛出NoSuchElementException异常,而peekLast()返回null

// 源码
public E getLast() {
    // 尾索引是下一个元素储存的位置,需计算尾索引的前一个索引位获取元素
    E result = (E) elements[(tail - 1) & (elements.length - 1)];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}
// 解析示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{null,2,3,4,8,7,8,5};
    int tail = 0;
    // (0 - 1) & (8 - 1) = -1 & 7 = 1111 & 0111 = 0111 = 7
    Integer result = (Integer) elements[(tail - 1) & (elements.length - 1)];
    // 5
    System.out.println(result);
}
// 错误示例
@Test
void contextLoads() {
    ArrayDeque arrayDeque = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque.getLast());
}
// 方法示例
@Test
void contextLoads(){
    Object[] elements = new Object[]{1,2,3,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // 19
    System.out.println(arrayDeque.getLast());
    // [1, 2, 3, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

12.获取末尾元素(不判空) peekLast()

注意:本方法等价于getLast()方法,区别是若当前队列为空,本方法返回null,而getLast()抛出异常

// 源码
public E peekLast() {
    // 尾索引是下一个元素储存的位置,需计算尾索引的前一个索引位获取元素
    return (E) elements[(tail - 1) & (elements.length - 1)];
}
// 解析示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{null,2,3,4,8,7,8,5};
    int tail = 0;
    // (0 - 1) & (8 - 1) = -1 & 7 = 1111 & 0111 = 0111 = 7
    // 5
    System.out.println(elements[(tail - 1) & (elements.length - 1)]);
}
// 方法示例
@Test
void contextLoads() {
    ArrayDeque arrayDeque = new ArrayDeque();
    // null
    System.out.println(arrayDeque.peekLast());
}

13.移除首个指定的元素 removeFirstOccurrence(Object o)

参数o —— 要移除的元素

注意:本方法循环储存队列元素的数组一一匹配,找到则移除并返回true

// 源码
public boolean removeFirstOccurrence(Object o) {
    // 若队列为空则返回false
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    Object x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) {
            // 移除此元素
            delete(i);
            // 移除首个匹配元素时直接返回true
            return true;
        }
        // 计算后一个索引位置(可能小于头索引,需要计算不能直接+1)
        i = (i + 1) & mask;
    }
    return false;
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.removeFirstOccurrence(8);
    // [1, 2, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

14.移除最后一个指定的元素 removeLastOccurrence(Object o)

参数o —— 要移除的元素

注意
1. 本方法循环储存队列元素的数组一一匹配,找到则移除并返回true
2. 因为本队列为双端队列,因此并不需要从头索引往尾索引循环或获取元素数递减循环,直接从尾索引往头索引循环匹配首个元素即可

// 源码
public boolean removeLastOccurrence(Object o) {
    // 若队列为空则返回false
    if (o == null)
        return false;
    int mask = elements.length - 1;
    // 获取最后一位元素索引
    int i = (tail - 1) & mask;
    Object x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) {
            // 移除此元素
            delete(i);
            // 移除首个匹配元素时直接返回true(此处是从尾索引往头索引循环,移除首个匹配即可)
            return true;
        }
        // 计算前一个索引位置(可能小于头索引,需要计算不能直接-1)
        i = (i - 1) & mask;
    }
    return false;
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.removeLastOccurrence(8);
    // [1, 2, 8, 4, 18, 19]
    System.out.println(arrayDeque);
}

公有队列方法

01.入队 add(E e)

参数e —— 要入队的元素

注意:本方法的实现与offerFirst()完全一致,若指定元素为null会抛出异常

// 源码
public boolean add(E e) {
    addLast(e);
    // 本方法有返回,不抛出异常就返回true
    return true;
}
// 错误示例
@Test
void tes1t(){
    ArrayDeque arrayDeque = new ArrayDeque();
    // NullPointerException
    arrayDeque.add(null);
    System.out.println(arrayDeque);
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.add(8);
    // [1, 2, 8, 4, 8, 18, 19, 8]
    System.out.println(arrayDeque);
}

02.入队 offer(E e)

参数e —— 要入队的元素

注意:因为add(E e)方法实现与offerFirst()完全一致,本方法调用的又是offerFirst(),因此本方法与add(E e)没有区别

// 源码
public boolean offer(E e) {
    // offerLast(e) 方法中已经添加了返回
    return offerLast(e);
}
// 错误示例
@Test
void tes1t(){
    ArrayDeque arrayDeque = new ArrayDeque();
    // NullPointerException
    arrayDeque.offer(null);
    System.out.println(arrayDeque);
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.offer(8);
    // [1, 2, 8, 4, 8, 18, 19, 8]
    System.out.println(arrayDeque);
}

03.出队(判空) remove()

注意:该方法会返回出队的元素

// 源码
public E remove() {
    return removeFirst();
}
// 空队列示例
@Test
void contextLoads() {
    ArrayDeque arrayDeque = new ArrayDeque();
    // NoSuchElementException
    arrayDeque.remove();
    System.out.println(arrayDeque);
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.remove();
    // [2, 8, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

04.出队(不判空) poll()

注意:该方法会返回出队的元素

// 源码
public E poll() {
    return pollFirst();
}
// 空队列示例
@Test
void tes1t(){
    ArrayDeque arrayDeque = new ArrayDeque();
    // null
    System.out.println(arrayDeque.poll());
    // []
    System.out.println(arrayDeque);
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // 1
    System.out.println(arrayDeque.poll());
    // [2, 8, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

05.获取首位元素(判空) element()

// 源码
public E element() {
    return getFirst();
}
// 空队列示例
@Test
void tes1t(){
    ArrayDeque arrayDeque = new ArrayDeque();
    // NoSuchElementException
    System.out.println(arrayDeque.element());
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // 1
    System.out.println(arrayDeque.element());
}

06.获取首位元素(不判空) peek()

注意:本方法队列和堆栈通用

// 源码
public E peek() {
    return peekFirst();
}
// 空队列示例
@Test
void tes1t(){
    ArrayDeque arrayDeque = new ArrayDeque();
    // null
    System.out.println(arrayDeque.peek());
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // 1
    System.out.println(arrayDeque.peek());
}

公有堆栈方法

01.入栈push(E e)

参数e —— 要入栈的元素

注意:与队列方法不同,本方法无返回,而队列方法有返回(不报错则返回true)

// 源码
public void push(E e) {
    addFirst(e);
}
// 示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.push(11);
    // [11, 1, 2, 8, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

02.出栈pop(E e)

参数e —— 要出栈的元素

注意
1. 该方法会返回出栈的元素
2. 堆栈方法只有一个出栈方法,若堆栈为空,会抛出NoSuchElementException异常

// 源码
public E pop() {
    return removeFirst();
}
// 空队列示例
@Test
void contextLoads(){
    ArrayDeque arrayDeque = new ArrayDeque();
    // NoSuchElementException
    arrayDeque.pop();
    System.out.println(arrayDeque.peek());
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.pop();
    // [2, 8, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

公有集合方法

01.队列元素个数 size()

// 源码
public int size() {
    return (tail - head) & (elements.length - 1);
}
// 解析示例
@Test
void tes1t(){
    Object[] elements = new Object[]{1,null,2,8,4,8,18,19};
    int head = 2;
    int tail = 1;
    // (1 - 2) & (8 - 1) = -1 & 7 = 1111 & 0111 = 7
    System.out.println((tail - head) & (elements.length - 1));
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // 7
    System.out.println(arrayDeque.size());
}

02.队列是否为空 isEpty()

// 源码
public boolean isEmpty() {
    return head == tail;
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // false
    System.out.println(arrayDeque.isEmpty());
}

03.队列是否包含某一元素 contains(Object o)

参数o —— 要判断的元素

// 源码
public boolean contains(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    Object x;
    // 循环对元素一一匹配,只要找到第一个匹配的则返回true
    while ( (x = elements[i]) != null) {
        if (o.equals(x))
            return true;
        i = (i + 1) & mask;
    }
    return false;
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // true
    System.out.println(arrayDeque.contains(8));
}

04.移除队列中指定元素 remove(Object o)

参数o —— 要移除的元素

注意:本方法移除的是从头索引开始首个匹配的元素

// 源码
public boolean remove(Object o) {
    return removeFirstOccurrence(o);
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // true
    System.out.println(arrayDeque.remove(8));
    // [1, 2, 4, 8, 18, 19]
    System.out.println(arrayDeque);
}

05.清空队列 clear()

注意:当头尾索引一致,队列为空不处理也不报错

// 源码
public void clear() {
    int h = head;
    int t = tail;
    if (h != t) { 
        // 先重置头尾索引为0
        head = tail = 0;
        int i = h;
        int mask = elements.length - 1;
        // 当元素索引不为0时循环,先运行后判断便于将索引0的元素也移除,不使用while或for循环为了防止下标越界
        do {
            elements[i] = null;
            i = (i + 1) & mask;
        } while (i != t);
    }
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    arrayDeque.clear();
    // []
    System.out.println(arrayDeque);
}

06.队列转换为数组 toArray()

注意:调用私有的复制数组的方法,从头索引开始,到尾索引结束,不包含尾索引(尾索引为下一次新增元素储存的位置,除非数组已满否则为null)

// 源码
public Object[] toArray() {
    return copyElements(new Object[size()]);
}
// 方法示例
@Test
void contextLoads() {
    Object[] elements = new Object[]{1,2,8,4,8,18,19};
    // [1, 2, 8, 4, 8, 18, 19, null]
    // head = 0  tail = 7
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    // [null, 2, 8, 4, 8, 18, 19, null]
    // head = 1  tail = 7
    arrayDeque.removeFirst();
    // [null, 2, 8, 4, 8, 18, 19, 19]
    // head = 1  tail = 0
    arrayDeque.add(19);
    // [null, null, 8, 4, 8, 18, 19, 19]
    // head = 2  tail = 0
    arrayDeque.removeFirst();
    // [1, null, 8, 4, 8, 18, 19, 19]
    // head = 2  tail = 1
    arrayDeque.add(1);
    Object[] array = arrayDeque.toArray();
    // [8, 4, 8, 18, 19, 19, 1]
    System.out.println(Arrays.toString(array));
}

07.队列转换到指定数组 toArray(T[] a)

参数a —— 接收拷贝数据的数组

注意
1. 本方法虽然提供了接收数据的数组,但若容量不够,返回的其实并不是原数组,而是重新创建了一个,提供的数组主要的作用是确定类型
2. 若指定数组原来有元素并不会清空会直接覆盖,若指定数组元素多于要拷贝的元素,为了区分会将拷贝的元素后一位元素设置为null,因此不建议使用已有元素的数组接收拷贝数据(除非业务需要)

// 源码
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    int size = size();
    // 指定数组容量不够创建一个新的
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                a.getClass().getComponentType(), size);
    // 拷贝元素,按照头索引到尾索引的顺序拷贝            
    copyElements(a);
    // 若原数组容量足够,且指定数组中原来有元素,为了区分会将拷贝的元素后一位元素设置为null
    if (a.length > size)
        a[size] = null;
    return a;
}
// 示例1:指定数组容量不足
@Test
void contextLoads(){
    Integer[] i = new Integer[8];
    // [Ljava.lang.Integer;@15dc339f
    System.out.println(i);

    Integer[] elements = new Integer[]{1,2,8,4,8,18,19,0,66,97};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    Object[] objects = arrayDeque.toArray(i);
    // 并非指定数组对象 [Ljava.lang.Integer;@6cd56321
    System.out.println(objects);
}
// 示例2:指定数组容量足够
@Test
void contextLoads(){
    Integer[] i = new Integer[16];
    // [Ljava.lang.Integer;@15dc339f
    System.out.println(i);

    Integer[] elements = new Integer[]{1,2,8,4,8,18,19,0,66,97};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    Object[] objects = arrayDeque.toArray(i);
    // 指定数组对象 [Ljava.lang.Integer;@15dc339f
    System.out.println(objects);
}
// 示例3:指定数组有元素且多于拷贝元素
@Test
void contextLoads(){
    Integer[] i = new Integer[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1};
    // [Ljava.lang.Integer;@15dc339f
    System.out.println(i);

    Integer[] elements = new Integer[]{1,2,8,4,8,18,19,0,66,97};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    Object[] objects = arrayDeque.toArray(i);
    // 指定数组对象,null分隔原数据 [Ljava.lang.Integer;@15dc339f
    System.out.println(objects);
    // [1, 2, 8, 4, 8, 18, 19, 0, 66, 97, null, 1, 1, 1]
    System.out.println(Arrays.toString(i));
}

08.迭代器(正常顺序) iterator()

// 源码
public Iterator<E> iterator() {
    // 对象实现参考内部类解析
    return new DeqIterator();
}
// 方法示例
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{1,2,8,4,8,18,19,0,66,97};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    System.out.println("正序迭代器:");
    Iterator iterator = arrayDeque.iterator();
    while ((iterator.hasNext())){
        // 1 2 8 4 8 18 19 0 66 97
        System.out.print(iterator.next() + " ");
    }
}

09.迭代器(反向顺序) descendingIterator()

// 源码
public Iterator<E> descendingIterator() {
    // 对象实现参考内部类解析
    return new DescendingIterator();
}
// 方法示例
@Test
void contextLoads() {
    Integer[] elements = new Integer[]{1,2,8,4,8,18,19,0,66,97};
    ArrayDeque arrayDeque = new ArrayDeque(Arrays.asList(elements));
    System.out.println("逆序迭代器:");
    Iterator iterator1 = arrayDeque.descendingIterator();
    while (iterator1.hasNext()){
        // 97 66 0 19 18 8 4 8 2 1 
        System.out.print(iterator1.next() + " ");
    }
}

10.可拆分迭代器 spliterator()

// 源码
public Spliterator<E> spliterator() {
    return new DeqSpliterator<E>(this, -1, -1);
}

公有对象方法

01.克隆当前对象 clone()

注意:本克隆方法使用了Object的克隆方法,本方法是浅克隆,如果元素是实例对象,克隆的是对象的指向,而不是直接新建了对象,修改被克隆或克隆生成的元素对象,会对两个对象都有影响

// 源码
public ArrayDeque<E> clone() {
    try {
        @SuppressWarnings("unchecked")
        // 默认调用Object
        ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
        result.elements = Arrays.copyOf(elements, elements.length);
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}
// 示例:克隆队列的元素为对象
@Test
void contextLoads() throws CloneNotSupportedException {
    StringBuilder sb = new StringBuilder("1");
    StringBuilder sb1 = new StringBuilder("2");
    StringBuilder sb2 = new StringBuilder("3");
    StringBuilder sb3 = new StringBuilder("4");
    StringBuilder sb4 = new StringBuilder("5");
    StringBuilder sb5 = new StringBuilder("6");
    StringBuilder[] elements = new StringBuilder[]{sb,sb1,sb2,sb3,sb4,sb5};
    ArrayDeque<StringBuilder> arrayDeque = new ArrayDeque(Arrays.asList(elements));
    ArrayDeque<StringBuilder> clone = arrayDeque.clone();
    // [1, 2, 3, 4, 5, 6]
    System.out.println(clone);

    sb.append("+");
    // [1+, 2, 3, 4, 5, 6]
    System.out.println(arrayDeque);
    // [1+, 2, 3, 4, 5, 6]
    System.out.println(clone);
}

序列化

01.序列号

private static final long serialVersionUID = 2340985798034038923L;

02.写入writeObject(java.io.ObjectOutputStream s)

参数s —— 对象转换为的二进制流

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    s.defaultWriteObject();
    // 写入元素数量
    s.writeInt(size());
    // 循环写入元素
    int mask = elements.length - 1;
    // 循环将元素一个个写入对象
    for (int i = head; i != tail; i = (i + 1) & mask)
        s.writeObject(elements[i]);
}

03.读取readObject(java.io.ObjectOutputStream s)

参数s —— 对象转换为的二进制流

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    // 读取元素个数
    int size = s.readInt();
    // 读取队列容量
    int capacity = calculateSize(size);
    // 根据容量创建储存元素的数组对象
    // SharedSecrets是用来获取Java栈帧中存储的类信息的(日志、序列化等需要获取JVM储存的类信息时使用)
    SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
    // 根据元素个数初始化数组
    allocateElements(size);
    // 设置头尾索引
    head = 0;
    tail = size;
    // 循环将元素存入要读取出的数组
    for (int i = 0; i < size; i++)
        elements[i] = s.readObject();
}

总结

数据结构

  1. 本类中存在多个增删读的方法,可以根据需要使用的数据结构和想要的校验/返回值配套使用
  2. 本方法主要用于实现队列或堆栈,因此没有修改的方法
  3. 如需将本类作为堆栈使用,可以直接使用peek()作为获取元素的方法,将push(E e)pop(E e)作为入栈、出栈的方法,但是pop(E e)是用来出栈指定元素的,正常的出栈可以使用poll()remove(),当然使用removeFirst()pollFirst()也可以
  4. 如需将本类作为队列使用,可以直接使用add(E e)offer(E e)作为入队方法,将poll()remove()作为出队方法,将element()peek()作为获取一个元素方法
  5. 当然不想使用方法准备好的堆栈方法或队列方法,使用公共方法自己配套使用也可以,但尤其要注意入队和入栈的方法,这两个方法是决定对象实现的数据结构的关键
  6. 对于公有方法,简单来说,以一般集合命名方式的新增方法(add)没有返回值,以一般集合命名方式的删除方法(remove)和获取元素方法(get)会判空;而其他的方法则相反(有返回值,不会判空),更加便利和约束较小一点
  7. 对于队列方法,基本与第6点一致,除了element()方法,可以将它看作get()方法
  8. 对于堆栈方法,虽然命名不同,但其规则与普通集合规则一致

双端队列

  1. 通过名字可以看出,本类与普通数组不同移除主要为双端,两端都可进行操作,而非一般的数组只能从后面增删
  2. 因为其双端的特性,本类主要用来实现一些数据结构,如果仅用于储存元素使用ArrayListHashMap等都可以
  3. 本类中最重要的就是后尾索引两个参数,他们实现了双端的特性,也因此本类在一些方面效率和空间利用率会较高
  4. 本类为了便于实现不同的数据结构遍历,也提供的正向和反向两种迭代器
  5. 也因为本类主要用来实现不同的数据结构,因此本类并未提供修改元素的方法,毕竟队列、堆栈这种数据结构的使用主要因为它的存取值顺序特性

—— END ——

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值