Android ArrayDeque 分析

本文深入剖析了JDK容器中ArrayDeque的实现原理,包括其数组结构、双端队列特性及作为栈的应用。详细介绍了ArrayDeque的初始化、元素添加与删除、数组扩容等核心方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

ArrayDeque是JDK容器中的一个双端队列实现,不过它内部使用的是数组来对元素进行操作,不允许存储null值,同时可以当做队列,双端队列,栈来进行使用。上篇文章的时候我们分析过LinkedList也是双端队列,不过用的是双向链表结构实现的。

使用示例

public static void main(String[] args) {
    ArrayDeque<String> deque = new ArrayDeque<>();
    //添加一个元素(内部也是添加一个元素到尾部)
    deque.add("adbc");
    deque.add("123");
    //在最后一个位置添加元素
    deque.addLast("last Element");
    deque.add("456");
    //对队列的最后一个位置添加元素
    deque.offer("yahoo");
    //往队列的最后一个位置添加元素
	deque.offerLast("tencent");
    deque.add("789");
    //往队列中的第一个位置插入一个元素
    deque.push("Hello");
    //在第一个位置添加元素
    deque.addFirst("first Element");
    //删除第一个元素,并且返回该删除的元素
    deque.poll();
    String data = deque.poll();
}

类结构图

image_1dtsshcl215ourmr1bdd36l1sad9.png-19.7kB

image_1dtr7e11p1b8e1jmk18fk12jv9t89.png-29.4kB

注意: 我们知道ArrayDeque内部是由数组实现的,但是我们 知道数组是从左边到右边的。由此我们可知 tail就是数组的第0个元素,head就是数组中的第 length-1个元素

代码分析

通过上面的学习以及类结构图,我们能很清楚的知道ArrayDeque基本使用以及复用关系,它的内部api方法其实很多之间也都是复用的。比如说 add() 和 offer()、offerLast()最后都是调用的 addLast()方法来进行实现的;push()和 offerFirst()最后都是调用的addFirst()方法进行实现的。所以我们只需要分析核心方法,其他找到他们的调用关系就行。

  • 初始化
public class ArrayDeque {
    //队列内部存储元素的 数组
    transient Object[] elements;
    //用于记录 队列 头部 位置信息
    transient int head;
    //用于记录 队列 尾部 位置信息
    transient int tail;
    private static final int MIN_INITIAL_CAPACITY = 8;
    
    public ArrayDeque() {
        elements = new Object[16];
    }
    。。。。。。。
}

根据我们最简单的构造方法,发现首先会创建一个长度为16的数组,同时还有两个很重要的属性head和tail用来指向数组的头和尾巴,同时我们也可以自定义数组的长度

//我们也可以传入我们自定义的容量大小,同时jdk帮我们内部创建一个数组。
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

/**
 * 这段代码的核心是找到一个数字 大于等于 numElements同时也是 2的n次幂的数字,同时创建的数组的大小就以这个数字为大小,举个例子,如果我们传入29的话,这个时候会进行运算同时找到32这个数字,同时创建一个大小为32的数组。
 */
private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
      // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    //我们传进来的数字小于8的话,那么就创建一个大小为8的数组。否则就找到最接近于numElements(同时还需要大于或者等于numElements),同时还要是2的n次幂的数字。
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        //这里的 >>> 是无符号向右移动多少位然后再跟原来的数字进行 或 运算的。
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        //最大只能创建 2的30次幂大小的数组。
        if (initialCapacity < 0)    // Too many elements, must back off
            initialCapacity >>>= 1; // Good luck allocating 2^30 elements
    }
    elements = new Object[initialCapacity];
}

上面主要分析的时候了构造方法里面创建数组时的一些注意问题,如果是调用者自己传入大小的话,内部代码还会做一个转换,就是会找到最接近于我们传入这个数字,同时还需要是 2^n = numElements,

  • 添加元素

尾部添加元素

public void addLast(E e) {
    //从这个代码中我们可以看出该队列不可以添加 null元素
    if (e == null)
        throw new NullPointerException();
    //在初始化的时候 tail为0,所以直接在[0] 添加元素就行
    elements[tail] = e;
    //我们知道 elements length = 2 ^ n,所以 length - 1转换为 二进制的话,所有的数字都是1的。
    比如说 32 - 1转换为2进制的话就是 11111的,所以 (tail + 1) & 31的话还是原来的数字。
    这里相当于是 tail + 1,也就是 尾部的位置想右边移动了一个位置。
    //这里有一个非常关键的地方就是 tail = tail + 1同时要判断跟head的位置是否重合,如果重合的话,就需要进行扩容了。
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

头部添加元素

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    //通过上面的结构图中我们知道head默认指向的是数组的最后一个数字。当 head = 0时 head - 1 为 -1,这个时候 -1 & 31的话还是31,也就是指向数组的最后一个元素。同时 head 指向31。如果再往数组的头部添加元素的话,数组的位置依次向左边移动,相当于 index--的实现。
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

数组扩容

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}
  • 删除元素

删除头部元素

public E pollFirst() {
    final Object[] elements = this.elements;
    //将head索引赋值给 h
    final int h = head;
    //然后获取数组 elements 数组的h位置的值
    E result = (E) elements[h];
    //如果 elements[h] 不为空的话,表示数组中存在该数据,需要将该位置数据置为null。
    if (result != null) {
        elements[h] = null; // Must null out slot
        //head的位置需要向右边移动一位(head = head + 1)。然后返回该元素
        head = (h + 1) & (elements.length - 1);
    }
    return result;
}

删除尾部元素

public E pollLast() {
    final Object[] elements = this.elements;
    //首先我们需要将 tail 位置向 左边移动一位(也就说 tail = tail - 1)。
    final int t = (tail - 1) & (elements.length - 1);
    //然后获取数组中的元素
    E result = (E) elements[t];
    if (result != null) {
        //需要将 tail - 1位置的元素置为null
        elements[t] = null;
        //同时 tail - 1 赋值给tail
        tail = t;
    }
    return result;
}

其实上面的不管在首尾添加数据还是在首尾删除数据,其实跟我们现实中的逻辑都是一样的,就是通过控制 head和tail位置来操纵数组,对数组进行增删操作的。如果我们要判断某个元素是否在队列中,则需要去遍历数组,然后每个元素进行比对匹配。

  • 清除所有元素
public void clear() {
    int h = head;
    int t = tail;
    //首先判断首尾不相等。
    if (h != t) {
        head = tail = 0;
        //然后 head赋值 i
        int i = h;
        int mask = elements.length - 1;
        do {
            elements[i] = null;
            //这里有个需要注意的是,如果 i+1 大于 elements.lenth以后,再 & mask的话,是循环从mask 到 tail的。其实我很好奇为啥不直接 遍历数组,从0开始遍历到 length -1 呢?
            i = (i + 1) & mask;
        } while (i != t);
    }
}

在文章的开头的时候我们讲过可以当做栈来进行使用

//在栈顶添加元素
 public void push(E e) {
    addFirst(e);
}

//弹出栈顶的元素
public E poll() {
    return pollFirst();
}

通过上面的代码我们可以分析的出来, head是作为栈底的,而tail是在栈的最上面。这个时候我们就需要把把 ArrayDeque竖立起来看待了。其原型图:

image_1dtveghpltt310qp1pp7pqi46k9.png-63.2kB

总结

  1. ArrayDeque 采用数组方式实现的双端队列,通过内部变量 head和tail来控制位置的偏移
  2. ArrayDeque容量不足时会进行扩容,每次扩容的容量在原有基础上扩大一倍
  3. Array还可以当做栈来直接使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值