JDK源码阅读 - 04 集合 之 Deque

Deque

接口,双端队列,继承Queue接口,并新增了扩展方法。

Deque与Queue的区别在于,Deque会允许从队列的两端都可以添加、获取数据。

头部操作的方法有:

  • getFirst、addFirst、offerFirst、peekFirst、pollFirst、removeFirst;

尾部操作的方法有:

  • getLast、addLast、offerLast、peekLast、pollLast、removeLast;

ArrayDeque

继承AbstractCollection,实现Deque接口。

底层是个数组,初始默认数组长度是16;也可以在构造方法参数里指定一个队列元素个数。使用该个数根据复杂计算得到数组的长度。

private static final int MIN_INITIAL_CAPACITY = 8;

// ******  Array allocation and resizing utilities ******

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

双端队列,需要有两个指针,在ArrayDeque里为两个索引位置,分别来指向数组里剩余元素的头部和尾部

/**
 * The index of the element at the head of the deque (which is the
 * element that would be removed by remove() or pop()); or an
 * arbitrary number equal to tail if the deque is empty.
 */
transient int head;

/**
 * 尾部添加数据时,数据被放入的索引位置 (via addLast(E), add(E), or push(E)).
 */
transient int tail;

添加数据(要求数据不能为null)的方法,add,也是尾部添加。

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    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;
}

尾部添加数据代码逻辑

  1. 直接把数据放在tail所代表的索引位置;
  2. tail值 加1后与当前数组的长度减一(最后一个索引位)进行 与运算 (&),得到的结果作为新的tail值,如果此时与head相等(可以看作head指向到数组最后面了),就对数组做2倍扩容。
  3. 新数组容量是当前数组的2倍,同时把当前旧数组head位置到数组最后位置的所有元素copy到新数组里,从新数组的索引位置0开始放置元素。
  4. 把旧数组从索引0开始到tail位置的元素copy到新数组,新数组的索引起始位置是第3步放的数组的结尾位置。
  5. 重置head=0,tail=旧数组长度

解释代码逻辑的原理:

假设通过 new ArrayDeque() 创建一个双端队列,那么它默认的底层数组长度是16,最后一个索引位就是15。此时head和tail都指向了数组的索引0号位。

当添加元素时,会从0号位开始,同时tail指向根据与运算会一直累加。当tail =15时,新添加的元素就会放在15号位,也就是数组的最右边位置。按照计算公式 (15 +1 ) & 15=0,此时会让tail指向0号位。

在队列添加元素的时,如果头部元素没被消费,那么此时head也就是0,此时0号位说明是有值的,元素不能覆盖,所以要在下次添加元素时进行扩容。

如果头部元素有被消费,那么head数值就会累加,此时head指向的数组左边索引位都是空值,允许放新的元素。所以tail的指向会从0号位开始。直到遇到head后,再次进行数组扩容。

头部添加数据

如果说尾部添加数据,是tail从索引低位到高位的循环移动,那么头部添加数据就可以看作是head从高位向地位移动。head索引位的移动规则是 head -1 结果 与数组最后一个索引位进行 与运算。

例如给0号位添加元素后,head的下次指向就是 (0-1) & 15 = 15,直到 head与tail相等时,进行数组扩容。

小结:

  1. head和tail的关系可以理解为双指针在一个数组上进行左右移动,指针相遇就把数组扩容。
  2. 扩容两倍,是为了让与运算方便,head和tail得到的结果都是顺序性的。在构造方法里指定元素个数时,通过的复杂计算得到的就是比输入参数大的,刚好是2的次方的某值,例如输入15得到16,输入16得到32。
  3. 队列里的元素个数,就是head 和 tail 指向的两个索引位之间的元素个数
  4. 清空队列,不仅要把数组每个索引位置为null,还要更改 head = tail = 0;

AbstractSequentialList

抽象类,继承AbstractList。用于LinkedList类方法实现的模板类。

相关操作都是针对抽象方法listIterator查到元素后,再进行后续操作。

LinkedList

继承AbstractSequentialList,实现List、Deque接口。支持头、尾和随机索引位置数据的添加和获取

层使用链表的方式实现,会把链表的头部和尾部先准备好,每个链表的节点都有个双向指针。

/**
* 记录元素个数
*/
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;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

添加数据

不管是头部添加和尾部添加,都会把数据封装在Node对象里,通过first 、 last 进行数据的指向。

如果要根据索引位置添加数据,那么就需要在链表里先找到这个索引对应的位置,返回此处的Node对象

Node<E> node(int index) {
    // assert isElementIndex(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;
    }
}

将数据封装的Node对象插入到查询返回的Node对象的前面。

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

获取数据

list会通过指定的索引来获取数据,在获取数据的时候,也是要通过上述的遍历,找到该索引位置的Node,返回封装的数据。

移除数据

在对LinkedList下的数据直接移除,或者获取了数据后移除,起始就是将first、last的指向进行了变更,被移除的Node,不再属于链表的一部分,同时为了保证能被垃圾回收器回收,将被移除的Node设置为了Null。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值