小言_互联网的博客

Java源码之ArrayDeque源码分析

305人阅读  评论(0)

1.声明

当前内容用于本人学习和复习之用,主要为ArrayDequeue的源码分析

2.查看ArrayDeque的继承和实现

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

查看发现ArrayDeque和LinkedList的继承实现都差不多

3.查看ArrayDeque的属性

// 内部维护的数据元素
transient Object[] elements; // non-private to simplify nested class access

// 记录头部的下标
transient int head;

// 记录尾部的下标
transient int tail;

// 最小初始化容量为8,是2的次方
private static final int MIN_INITIAL_CAPACITY = 8;

分析发现:该ArrayDeque内部维护的是一个Object的数组,感觉和ArrayList内部很像

但是该ArrayDeque具有头记录下标和尾记录下标,还有默认最小初始化容量为8

当前的ArrayDeque没有modCount和size(那么它用什么获取实际数据长度?不会出现并发修改?)

4.查看ArrayDeque的构造函数

// 无参构造函数,直接初始化当前数组的容量为16
public ArrayDeque() {
    elements = new Object[16];
}

// 有参的构造函数
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

// 带集合参数的构造函数
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());//计算出容量
    addAll(c);// 添加集合数据
}

// 通过计算方式设定当前需要初始化的容量大小
private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];//所以这里初始化容量就是2的倍数(最小是8)
}

// 计算容量的大小(结果为2的倍数),最小为8
private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;// 获取最小容量值8
    // 如果传递的数比8大,那么需要计算出一个最好的2的次方的数,;例如:传递9返回16,传递17返回32
    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;
}

通过分析发现:

  1. 无参的构造函数中默认创建大小为16的数组对象
  2. 使用有参的时候,需要通过计算方式获取最好的容量(该容量是2的倍数,最小为8)
  3. 所以ArrayDeque中只要new出来那么必定初始化当前的容量

5.查看addAll方法

// 该方法为父类提供的方法
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    // 默认循环遍历集合中的所有元素然后进行添加
    for (E e : c)
        if (add(e))// 这里使用多态方式
            modified = true;
    return modified;
}
// 实际调用的方法
public boolean add(E e) {
    addLast(e);//实际上是向后面添加数据
    return true;
}

public void addLast(E e) {
    // 添加的数据不能为null,否者报错
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;// 直接向尾部添加数据
    // 然后让该尾下标增加,判断当前的尾下标是否等于最大下标或者是否达到头下标(即判断当前是否需要扩容)
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();//扩容操作,变成原来容量的两倍
}
// 扩容两倍容量
private void doubleCapacity() {
    assert head == tail;
    int p = head; //获取头下标
    int n = elements.length;//获取数组总长
    int r = n - p; // 计算左右
    int newCapacity = n << 1;// 将原来容量扩容两倍
    if (newCapacity < 0) // 判断是否超出最大Integer的最大值,则报错
        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. 添加数据的时候不能放入null值,否则直接空指针异常
  2. 判断是否需要扩容即尾下标是否达到当前数组的最大值,并且head的前面也是有值的
  3. 如果需要就开始扩容,扩容为原来数组的两倍,并且使用拷贝方式将有数据的部分放入新数组的前面,前面没有数据的部分放在新数组的后面,最后完成替换操作,并将head设置为0,将尾部下标设置为原来数组的长度
  4. 默认使用add方法则直接向后面追加值

所以可以发现,这个当前的ArrayDeque使用两个指针方式进行判断哪个部分具有数据,通过头指针方式访问头部,使用尾部指针指向尾部,那么就可实现头部添加和尾部添加(因为知道了当前的头尾部分)

6.查看addFirst方法

 public void addFirst(E e) {
     if (e == null)
         throw new NullPointerException();
     // 这里表示head的值计算出来,如果当前的head为0那么结果就是head=31(表示head向后面移动)
     elements[head = (head - 1) & (elements.length - 1)] = e;
     if (head == tail)//如果头部和尾部相碰撞那么认为认为需要扩容了
         doubleCapacity();
 }

发现这个addFirst方法中使用的如果这个head大于0那么这个头部向后面移动,如果这个数=0那么就从最后一个开始,巧妙地利用逻辑并方式计算出来

7.查看remove方法

//调用删除默认直接删除前面地数据
public E remove() {
    return removeFirst();
}

public E removeFirst() {
    E x = pollFirst();//通过获取头部小标地方式获取元素
    if (x == null)//如果这个元素没有赋值那么直接扔出空指针异常
        throw new NoSuchElementException();
    return x;
}

public E pollFirst() {
    int h = head;//获取头下标
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];//直接获取头下标地元素
    // Element is null if deque empty
    if (result == null)//如果下标元素为null,直接返回null,然后外面出现空指针异常
        return null;
    // 当前存在元素
    elements[h] = null;     // 取出后将该元素置空
    head = (h + 1) & (elements.length - 1);//然后通过计算方式重新写入头下标(如果在31位置那么直接变成0,如果在1位置那么直接变成2)
    return result;
}

通过测试发现:

  1. remove方法默认是直接移除head下标的元素,如果这个下标中不存在元素,那么直接报错
  2. 否者认为存在该元素,直接将该元素置空,然后重新计算当前的head下标

查看remove(object)方法

public boolean remove(Object o) {
    return removeFirstOccurrence(o);
}

public boolean removeFirstOccurrence(Object o) {
    if (o == null)// 判断移除的元素是否为null,是则失败,因为当前的ArrayDeque中不能存放null值
        return false;
    // 否者可以移除该元素
    int mask = elements.length - 1;//获取元素最大下标
    int i = head; // 获取头下标的位置
    Object x;
    // 下面就是使用从head下标向tail下标开始迭代(如果head大于tail,那么从head迭代到最后,然后从0开始到tail)
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) { // 如果找到它则按照下标方式删除它
            delete(i); 
            return true;
        }
        i = (i + 1) & mask;
    }
    return false;
}


从分析removeFirstOccurrence(object)方法发现

  1. 删除默认数据的时候如果head大于tail则从head迭代到最后,然后从0开始迭代到tail位置
  2. 如果head小于tail那么直接向tail开始迭代
  3. 找到需要删除的下标,则使用下标方式删除

分析delete(int)方法

// 按照下标方式删除数据
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; // 计算出head到删除的下标i之间的距离(肯定是整数)
    final int back  = (t - i) & mask; //计算出找到尾节点到当前需要删除的下标i的距离(肯定是整数)

    // Invariant: head <= i < tail mod circularity
    //如果出现head到i的距离大于尾到头的距离,就认为出现并发操原因作(head <= i < tail)
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();

    // Optimize for least element motion
    //如果i距离头节点比i距离尾节点近一些
    if (front < back) {
        if (h <= i) {//判断h是否小于i(即i在h的右边)
            System.arraycopy(elements, h, elements, h + 1, front);//直接拷贝方式覆盖即可使用h=h+1,则i位置被h位置的数据覆盖
        } else { // Wrap around //整体向后面移动,最后面向第一移动
            // 否者认为i在h的左边
            System.arraycopy(elements, 0, elements, 1, i);//将第0到i之间的个元素,拷贝到1的后面
            elements[0] = elements[mask];//将最后一个元素放到第一个元素中
            System.arraycopy(elements, h, elements, h + 1, mask - h);//然后将头下标的元素放到h+1元素的后面
            
            // 整体的思想是,将0到i之间的元素全部向后面移动一位(导致i下标元素被覆盖掉),空出第一个元素,将头部元素放入第一位
            // 然后将h方到h+1的地方形成整体移动的效果,形成最后向第一元素前进1位到达0
        }
        elements[h] = null;//然后将head下标置空
        head = (h + 1) & mask;//head下标改变
        return false;
    } else {
        // 否者认为i距离尾节点比i距离头节点近一些
        if (i < t) { // Copy the null tail as well,如果i在尾节点的前面
            // 直接拷贝i+1后面的数据拷贝到到i的位置
            System.arraycopy(elements, i + 1, elements, i, back);
            tail = t - 1;//尾节点减少
        } else { // Wrap around// 整体向前移动,将第一个放到最后去
            //如果i在尾节点的后面
            //将i+1赋值到i上面从而达到覆盖i下标元素的效果(向前拷贝)
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            elements[mask] = elements[0];//将第一个元素放到最后一个元素中
            System.arraycopy(elements, 1, elements, 0, t);//然后将i到尾下标的元素放到0的位置,覆盖0
            tail = (t - 1) & mask;// 尾下标改变
        }
        return true;
    }
}

通过分析delete(int)方法发现

  1. 该方法主要用于删除数据,首先获取删除下标到head下标和到tail下标之间的距离
  2. 然后通过判断是否存在并发操作问题
  3. 组后比较删除下标到head和tail之间哪个近一些
    1. 如果距离头部比较近
      1. 在头部的右边,是则直接拷贝覆盖(向右移动),最后将head赋值为空即可
      2. 在头部的左边,认为需要移动整体的数组,全体向右移动,尾部向0开始移动(就像一个环一样)
    2. 如果距离尾部比较近
      1. 在尾部的后面,则认为需要移动整体的数组,全体向左移动,头部向数组最大下标移动(向一个环一样)
      2. 在尾部的前面,直接向前面拷贝

8.分析获取数据的方法

public E getFirst() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[head];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}

由于使用head标记了头元素的下标,可以直接获取

 public E getLast() {
     @SuppressWarnings("unchecked")
     E result = (E) elements[(tail - 1) & (elements.length - 1)];
     if (result == null)
         throw new NoSuchElementException();
     return result;
 }

直接获取尾元素,所在位置的下标

由于当前的ArrayDeque中将使用head和tail方式所以进行获取操作的时候都是比较简单的!

9.总结

1.ArrayDeque默认无参直接创建16长度的数组,并且每次扩容为原理的两倍

2.ArrayDeque使用Object数组实现与ArrayList一致,但是ArrayDeque中是使用一个head下标和tail进行标记方便删除操作和查找(使用两个指针方式)

3.从理念上来看ArrayDeque的查找和删除效率比当前的LinkedList要高

以上纯属个人见解,如有问题请联本人!


转载:https://blog.csdn.net/weixin_45492007/article/details/106193865
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场