diff --git a/src/main/java/org/apache/commons/collections4/deque/CircularDeque.java b/src/main/java/org/apache/commons/collections4/deque/CircularDeque.java index cc8e148ca..a867e5921 100644 --- a/src/main/java/org/apache/commons/collections4/deque/CircularDeque.java +++ b/src/main/java/org/apache/commons/collections4/deque/CircularDeque.java @@ -23,8 +23,16 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -import java.util.*; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Deque; +import java.util.NoSuchElementException; +import java.util.List; + +import java.util.ConcurrentModificationException; +import java.util.Objects; +import java.util.Iterator; /** * CircularDeque is a deque with a fixed size that replaces its oldest * element if full. @@ -99,7 +107,13 @@ public class CircularDeque extends AbstractCollection * @throws NullPointerException if the collection is null */ public CircularDeque(final Collection coll) { - this(coll.size()); + if (coll == null) { + throw new NullPointerException("Collection must not be null"); + } + + elements = (E[]) new Object[coll.size()]; + maxElements = elements.length; + addAll(coll); } @@ -167,7 +181,7 @@ public class CircularDeque extends AbstractCollection * @param index the index to increment * @return the updated index */ - private int indexIncrease(int index) { + private int increaseIndex(int index) { index++; if (index >= maxElements) { index = 0; @@ -336,58 +350,138 @@ public class CircularDeque extends AbstractCollection @Override public boolean removeFirstOccurrence(final Object o) { - int mask = elements.length - 1; int i = head; - Object x; - while ((x = elements[i]) != null) { - if (Objects.equals(o,x)) { + while (true) { + if (Objects.equals(o,elements[i])) { delete(i); - ArrayDeque; return true; } - i = (i + 1) & mask; + if (i == decreaseIndex(tail)) { + return false; + } + i = increaseIndex(i); } - return false; } @Override public boolean removeLastOccurrence(final Object o) { - return false; + int i = decreaseIndex(tail); + while (true) { + if (Objects.equals(o,elements[i])) { + delete(i); + return true; + } + if (i == head) { + return false; + } + i = decreaseIndex(i); + } } + @Override + public boolean add(final E e) { + if (isAtFullCapacity()) { + remove(); + } + + elements[tail++] = e; + + if (tail >= maxElements) { + tail = 0; + } + + if (tail == head) { + full = true; + } + + return true; + } + @Override public boolean offer(final E e) { - return false; + return offerLast(e); } @Override public E remove() { - return null; + return removeFirst(); } @Override public E poll() { - return null; + return pollFirst(); } @Override public E element() { - return null; + return getFirst(); } @Override public E peek() { - return null; + return peekFirst(); } @Override public void push(final E e) { - + addFirst(e); } @Override public E pop() { - return null; + return removeFirst(); + } + + + /** + * Removes the element at the specified position in the elements array, + * adjusting head and tail as necessary. This can result in motion of + * elements backwards or forwards in the array. + * + *

This method is called delete rather than remove to emphasize + * that its semantics differ from those of {@link List#remove(int)}. + * + * @return true if elements moved backwards + */ + private boolean delete(final int i) { + if (i == head) { + elements[i] = null; + head = increaseIndex(head); + full = false; + return true; + } + + if (i == decreaseIndex(tail)) { + elements[i] = null; + tail = decreaseIndex(tail); + full = false; + return true; + } + + final int front = i - head; + final int back = tail - i - 1; + + if (head < tail) { + if (front < back) { + System.arraycopy(elements, head, elements, head + 1, front); + head++; + } else { + System.arraycopy(elements, i + 1, elements, i, back); + tail--; + } + } else if (head >tail) { + if (i > head) { + System.arraycopy(elements, head, elements, head + 1, front); + head++; + } else if (i < tail) { + System.arraycopy(elements, i + 1, elements, i, back); + tail--; + } else { + throw new ConcurrentModificationException(); + } + } + full = false; + return true; } /** @@ -425,16 +519,115 @@ public class CircularDeque extends AbstractCollection @Override public int maxSize() { - return 0; + return maxElements; } @Override public Iterator iterator() { - return null; + return new Iterator() { + + private int index = head; + private int lastReturnedIndex = -1; + private boolean isFirst = full; + + @Override + public boolean hasNext() { + return isFirst || index != tail; + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + isFirst = false; + lastReturnedIndex = index; + index = increaseIndex(index); + return elements[lastReturnedIndex]; + } + + @Override + public void remove() { + if (lastReturnedIndex == -1) { + throw new IllegalStateException(); + } + + // First element can be removed quickly + if (lastReturnedIndex == head) { + CircularDeque.this.remove(); + lastReturnedIndex = -1; + return; + } + + int pos = lastReturnedIndex + 1; + if (head < lastReturnedIndex && pos < tail) { + // shift in one part + System.arraycopy(elements, pos, elements, lastReturnedIndex, tail - pos); + } else { + // Other elements require us to shift the subsequent elements + while (pos != tail) { + if (pos >= maxElements) { + elements[pos - 1] = elements[0]; + pos = 0; + } else { + elements[decreaseIndex(pos)] = elements[pos]; + pos = increaseIndex(pos); + } + } + } + + lastReturnedIndex = -1; + tail = decreaseIndex(tail); + elements[tail] = null; + full = false; + index = decreaseIndex(index); + } + + }; } + @Override public Iterator descendingIterator() { - return null; + return new DescendingIterator(); + } + + private class DescendingIterator implements Iterator { + /* + * This class is nearly a mirror-image of DeqIterator, using + * tail instead of head for initial cursor, and head instead of + * tail for fence. + */ + private int cursor = tail; + private int fence = head; + private int lastRet = -1; + private boolean isFull = full; + + public boolean hasNext() { + return isFull || cursor != fence; + } + + public E next() { + if (!hasNext()) + throw new NoSuchElementException(); + cursor = decreaseIndex(cursor); + @SuppressWarnings("unchecked") + E result = (E) elements[cursor]; + if (head != fence ) + throw new ConcurrentModificationException(); + lastRet = cursor; + return result; + } + + public void remove() { + if (lastRet < 0) + throw new IllegalStateException(); + if (!delete(lastRet)) { + cursor = increaseIndex(cursor); + fence = head; + } + lastRet = -1; + isFull = false; + } } } diff --git a/src/main/java/org/apache/commons/collections4/deque/package-info.java b/src/main/java/org/apache/commons/collections4/deque/package-info.java new file mode 100644 index 000000000..99540598e --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/deque/package-info.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * This package contains implementations for the {@link java.util.Deque Deque} interface. + *

+ * The following implementations are provided in the package: + *

    + *
  • CircularDeque - implements a Deque with a fixed size that discards oldest when full + *
+ *

+ * The following decorators are provided in the package: + *

    + *
  • Predicated - ensures that only elements that are valid according to a predicate can be added + *
  • Transformed - transforms elements added to the queue + *
  • Blocking - ensures the collection is thread-safe and can be operated concurrently + *
  • Synchronized - ensures the collection is thread-safe + *
+ * + */ +package org.apache.commons.collections4.deque; \ No newline at end of file diff --git a/src/test/java/org/apache/commons/collections4/deque/CircularDequeTest.java b/src/test/java/org/apache/commons/collections4/deque/CircularDequeTest.java new file mode 100644 index 000000000..8175aeda2 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/deque/CircularDequeTest.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.deque; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Extension of {@link AbstractDequeTest} for exercising the + * {@link CircularDeque} implementation. + * + * @since 4.5 + */ +public class CircularDequeTest extends AbstractDequeTest { + /** + * JUnit constructor. + * + * @param testName the test class name + */ + public CircularDequeTest(String testName) { + super(testName); + } + + /** + * Returns an empty CircularDeque that won't overflow. + * + * @return an empty CircularDeque + */ + @Override + public Deque makeObject() { + return new CircularDeque<>(100); + } + + /** + * Tests that the removal operation actually removes the first element. + */ + @SuppressWarnings("unchecked") + public void testCircularDequeCircular() { + final List list = new LinkedList<>(); + list.add((E) "A"); + list.add((E) "B"); + list.add((E) "C"); + final Deque deque = new CircularDeque<>(list); + + assertEquals(true, deque.contains("A")); + assertEquals(true, deque.contains("B")); + assertEquals(true, deque.contains("C")); + + deque.add((E) "D"); + + assertEquals(false, deque.contains("A")); + assertEquals(true, deque.contains("B")); + assertEquals(true, deque.contains("C")); + assertEquals(true, deque.contains("D")); + + assertEquals("B", deque.peek()); + assertEquals("B", deque.remove()); + assertEquals("C", deque.remove()); + assertEquals("D", deque.remove()); + } + + /** + * Tests that the constructor correctly throws an exception. + */ + public void testConstructorException1() { + try { + new CircularDeque(0); + } catch (final IllegalArgumentException ex) { + return; + } + fail(); + } + + /** + * Tests that the constructor correctly throws an exception. + */ + public void testConstructorException2() { + try { + new CircularDeque(-20); + } catch (final IllegalArgumentException ex) { + return; + } + fail(); + } + + /** + * Tests that the constructor correctly throws an exception. + */ + public void testConstructorException3() { + try { + new CircularDeque(null); + } catch (final NullPointerException ex) { + return; + } + fail(); + } + + /** + * Tests that the removal operation using method removeFirstOccurrence() and removeFirstOccurrence(). + */ + public void testRemoveElement() { + final List list = new LinkedList<>(); + list.add((E) "A"); + list.add((E) "B"); + list.add((E) "C"); + list.add((E) "D"); + list.add((E) "E"); + final Deque deque = new CircularDeque<>(list); + assertEquals(5, ((CircularDeque) deque).maxSize()); + + deque.addLast((E) "F"); + deque.addLast((E) "G"); + deque.addLast((E) "H"); + + assertEquals(false, deque.removeFirstOccurrence("A")); + assertEquals(false, deque.removeFirstOccurrence("B")); + assertEquals(false, deque.removeLastOccurrence("C")); + assertEquals("[D, E, F, G, H]", deque.toString()); + + assertEquals(true, deque.removeLastOccurrence("H")); + assertEquals("[D, E, F, G]", deque.toString()); + + assertEquals(true, deque.removeLastOccurrence("E")); + assertEquals("[D, F, G]", deque.toString()); + + assertEquals(true, deque.removeLastOccurrence("F")); + assertEquals("[D, G]", deque.toString()); + + assertEquals("D", deque.removeFirst()); + assertEquals("G", deque.removeLast()); + } + + public void testDescendingIterator() { + final List list = new LinkedList<>(); + list.add((E) "A"); + list.add((E) "B"); + list.add((E) "C"); + list.add((E) "D"); + list.add((E) "E"); + final Deque deque = new CircularDeque<>(list); + + final Iterator iterator = deque.descendingIterator(); + while (iterator.hasNext()) { + iterator.next(); + iterator.remove(); + } + assertTrue(deque.isEmpty()); + } + + @Override + public String getCompatibilityVersion() { + return "4.5"; + } +}