Queue를 사용할 때 자주 사용하는 구현체로 ArrayDeque와 LinkedList를 사용하는데 언제 어떻게 사용하면 좋을까?
우선 Queue의 대표적인 특징은 선입선출 방식이에요.
Queue 대기열에 데이터를 추가하는 것을 Enqueue, 데이터를 꺼내는 것을 Dequeue라고 하지만, java에서는 추가는 두 메서드 add()와 offer() 존재하며 , 꺼내는 메서드는 poll() 메서드가 존재한다.
출처: https://medium.com/depayse/kotlin-data-structure-collections-4-stack-queue-deque-4c383efebee9
ArrayDeque
는 Queue
의 서브인터페이스인 Deque
의 구현체이며, 배열의 특성을 가지고 있어요.
그리고 데이터 삽입 및 삭제가 큐의 앞, 뒤 모두에서 가능해요.
출처: https://medium.com/depayse/kotlin-data-structure-collections-4-stack-queue-deque-4c383efebee9
우선 ArrayDeque를 기본생성자를 통해 생성하면 16개의 공간이 만들어져요.
public ArrayDeque() {
elements = new Object[16];
}
그리고 값을 추가 할 때, 기존의 큐의 크기를 초과하게 되면 resize가 다음과 같은 순서로 이루어져요.
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[head = dec(head, es.length)] = e;
if (head == tail)
grow(1); // resize 관련된 부분
}
grow() 함수를 보면 다음과 같이 진행되고 있어요.
private void grow(int needed) {
// overflow-conscious code
final int oldCapacity = elements.length;
int newCapacity;
// Double capacity if small; else grow by 50%
int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
// 여기서 newCapacity에 값 할당이 이루어집니다.
if (jump < needed
|| (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
newCapacity = newCapacity(needed, jump);
final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
// Exceptionally, here tail == head needs to be disambiguated
if (tail < head || (tail == head && es[head] != null)) {
// wrap around; slide first leg forward to end of array
int newSpace = newCapacity - oldCapacity;
System.arraycopy(es, head,
es, head + newSpace,
oldCapacity - head);
for (int i = head, to = (head += newSpace); i < to; i++)
es[i] = null;
}
}
// newCapacity를 다시 계산하는 이유는 배열 크기가 MAX_ARRAY_SIZE를 초과할 수 없기 때문입니다.
private int newCapacity(int needed, int jump) {
final int oldCapacity = elements.length, minCapacity;
if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
if (minCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
return Integer.MAX_VALUE;
}
if (needed > jump)
return minCapacity;
return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)
? oldCapacity + jump
: MAX_ARRAY_SIZE;
}
LinkedList
는 List
와 Queue
의 구현체에요. 따라서 LinkedList
는 List의 특징을 가지고 있고, 다음 그림을 통해 자신의 노드와 다음 데이터를 참조하는 데이터를 갖고 있어요.
테스트 하고 있는 환경은 다음과 같아요.
public static void main(String[] args) {
ArrayDeque<Event> arrayDeque = new ArrayDeque<>();
int size = 변경되는 크기 부분
long dequeStart = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
arrayDeque.offer(new Event("test", i));
}
long dequeEnd = System.currentTimeMillis();
System.out.printf("Array Deque Times: %dms\n", dequeEnd - dequeStart);
Queue<Event> linkedListQue = new LinkedList<>();
long linkedListStart = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
linkedListQue.offer(new Event("test", i));
}
long linkedListEnd = System.currentTimeMillis();
System.out.printf("LinkedList Que Times: %dms\n", linkedListEnd - linkedListStart);
}
데이터를 추가하는 상황만 테스트 해보았어요. Deque resize를 자주 하기 때문에 성능이 더 안좋을 것이라고 생각했는데, 300,000 번 부터는 Deque가 성능이 더 좋았어요.
Deque에 값을 넣을 때는 단순히 값을 넣지만, LinkedList Que는 새로운 노드 추가 뿐만 아니라다음 노드에 대한 메모리도 참조해야 하기에 더 많은 연산이 필요해서 그런 것 같아요.
다음은 추가와 삭제가 빈번한 상황을 테스트하려고 해요. 이전 테스트와 큰 차이가 없네요.
자바 공식문서에 다음과 같이 설명이 되어 있어요.
This class is likely to be faster than
Stack
when used as a stack, and faster thanLinkedList
when used as a queue.
위의 공식문서를 토대로 다음과 같은 결론을 내렸어요.
cache locality-friendly
하다.정리하다보니 다음과 같은 생각도 하게 되었어요.
ArrayList vs ArrayDeque vs LinkedList: which one is best?