Talk about collection.Queue

Keywords: Java JDK less

As mentioned before, the main function of collections in Java is to hold other data and implement common data structures. So when we want to use common data structures such as "stack", "Queue", "linked list" and "array", we should think that we can directly use the Collection framework provided by JDK. For example, when we want to use queues, we should think about using LinkedList and ArrayDeque. This blog will introduce queues in the Collection framework.

The Queue interface inherits the Collection interface, so all the methods of Collection are included in the implementation class of Queue, and there is also a sub interface Dqueue, which represents a two-end Queue. For Queue, we mainly master ArrayDeque and LinkedList, and understand priority Queue.

1. Interface introduction

Queue is also an interface defined in the Java Collection framework, which directly inherits from the Collection interface. In addition to the basic Collection interface that specifies test operations, the queue interface also defines a set of special operations for queues. In general, queue manages its elements in a first in, first out (FIFO) fashion, with the exception of priority queues.

Deque interface inherits from Queue interface, but deque supports adding or removing elements from both ends at the same time, so it is also called double end Queue. In view of this, the implementation of deque interface can be used as FIFO Queue or LIFO Queue (Stack). It is also officially recommended to use deque's implementation to replace Stack. The main implementation classes of deque are ArrayDeque and LinkedList.

ArrayDeque is a concrete implementation of Deque interface, which depends on variable array. ArrayDeque has no capacity limit and can be expanded automatically according to the demand. ArrayDeque does not support null valued elements.

The specific features of LinkedList have been introduced in the previous blog, and will not be re introduced here.

1.1 Queue interface overview

Queue can be used as a queue to implement FIFO operations, mainly providing the following operations

public interface Queue<E> extends Collection<E> {
    //Insert an element to the end of the queue and return true
    //Throw IllegalStateException if the queue is full
    boolean add(E e);

    //Insert an element to the end of the queue and return true
    //false if the queue is full
    boolean offer(E e);

    //Take out the elements in the queue header and remove them from the queue
    //Queue is empty, NoSuchElementException exception is thrown
    E remove();

    //Take out the element of the queue header and remove it from the queue
    //Queue is empty, return null
    E poll();

    //Takes out the elements of the queue header, but does not remove them
    //If the queue is empty, a NoSuchElementException exception is thrown
    E element();

    //Takes out the elements of the queue header, but does not remove them
    //Queue is empty, return null
    E peek();
}

1.2 Deque interface

Deque interface is a two terminal queue, which can operate on the head and tail of the queue, so it can also be used as a stack.

The following table lists the corresponding methods for the Queue and Deque interfaces

Queue method Deque method
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

Another important function of Deque is that it can be used as a stack

Stack method Deque method
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

2. ArrayDeque

ArrayDeque is an array based implementation of Deque.

The following content comes from the Internet Blog

2.1 member variables of arraydeque

    //Array storage element
    transient Object[] elements;
    //Head element index
    transient int head;
    //Tail element index
    transient int tail;
    //Minimum capacity
    private static final int MIN_INITIAL_CAPACITY = 8;

ArrayDeque uses array to store elements, and head and tail to represent index. But notice that tail is not the index of tail element, but the next bit of tail element, that is, the index of the next element to be added.

2.2 initialization

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) {
    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
    }
    elements = new Object[initialCapacity];
}

Let's talk about the private void allocateElements(int numElements) method. The initialization capacity of ArrayDeque must be 2^n. So if the initialization capacity you transmit is 10, then the actual applied array capacity is 16. If the applied capacity is 33, then the actual capacity is 62. If the requested capacity is 62, the actual requested capacity is 128.

2.3 add method

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

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    //The index of the element to be added to the end is saved in the tail
    elements[tail] = e;
    //tail moves one bit backward
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        //tail and head meet, space is exhausted, need to expand
        doubleCapacity();
}

In the storage process, there is an interesting algorithm here, which is the tail calculation formula (tail = (tail+1) & (elements. Length - 1)). Note that the storage here is in the form of circular queue, that is, when the tail reaches the last capacity, the tail is equal to 0, otherwise the tail value is tail+1.

Head takes a similar approach. Every time you add an element to the head, the head points to the location of the latest element. When the head is less than 0, it will also take the form of a ring to store elements. For example, when the head has pointed to position 0 and added an element to the head of the queue, the head will become length-1.

For head and tail, the main thing is that head always points to the index position of the first element, and tail always points to the tail position (there are no elements in this position for the moment, if you insert elements in the tail, insert them in this position)

2.4 capacity expansion mechanism

private void doubleCapacity() {
    assert head == tail; //When expanding, the head index and the tail index must be equal
    int p = head;
    int n = elements.length;
    //How many elements are there from the header index to the end of the array (at length-1)
    int r = n - p; // number of elements to the right of p
    //Capacity doubling
    int newCapacity = n << 1;
    //Too much capacity, overflow
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    //Allocate new space
    Object[] a = new Object[newCapacity];
    //Copy the header index to the element at the end of the array to the header of the new array
    System.arraycopy(elements, p, a, 0, r);
    //Copy remaining elements
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    //Reset head end index
    head = 0;
    tail = n;
}

The following figure is the schematic diagram of capacity expansion

3. summary

ArrayDeque is a concrete implementation of Deque interface, which depends on variable array. ArrayDeque has no capacity limit and can be expanded automatically according to the demand. ArrayDeque can be used as a Stack with higher efficiency than Stack; ArrayDeque can also be used as a queue with better efficiency than LinkedList based on two-way linked list.

So if we want to use the data structure of "queue" and "stack" in our program, we should first think of LinkedList and ArrayDeque. I don't think the performance of the two is much different when used as a queue or a stack, but ArrayDeque needs to be expanded and continuous memory space needs to be applied, so I prefer LinkedList. I don't know if my understanding is correct.

Posted by lth2h on Mon, 09 Mar 2020 03:32:56 -0700