diff --git a/pom.xml b/pom.xml index cf92a5901e..a54e73ec3e 100644 --- a/pom.xml +++ b/pom.xml @@ -441,6 +441,9 @@ Claude Warren + + Oleksandr Maksymenko + diff --git a/src/main/java/org/apache/commons/collections4/list/AbstractIndexedTreeList.java b/src/main/java/org/apache/commons/collections4/list/AbstractIndexedTreeList.java new file mode 100644 index 0000000000..fd19935d0f --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/AbstractIndexedTreeList.java @@ -0,0 +1,997 @@ +/* + * 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.list; + +import org.apache.commons.collections4.OrderedIterator; + +import java.util.*; + +/** + * Common class for indexed tree lists. + * + * Code is based on apache common collections TreeList. + * + * @author Aleksandr Maksymenko + */ +abstract class AbstractIndexedTreeList extends AbstractList { + + /** The root node in the AVL tree */ + protected AVLNode root; + + /** Size of a List */ + protected int size = 0; + + /** + * Methods set(obj) in ListIterator can't be implemented to satisfy specification in IndexedTreeListSet. + * So these methods are disabled by default and throws UnsupportedOperationException. + * It's possible to enable this feature but in this case exception may be thrown if someone tries to add/set + * an element which is already in a collection. + */ + boolean supportSetInIterator = true; + + //----------------------------------------------------------------------- + /** + * Gets the element at the specified index. + * + * @param index the index to retrieve + * @return the element at the specified index + */ + @Override + public E get(final int index) { + return getNode(index).getValue(); + } + + /** + * Gets the current size of the list. + * + * @return the current size + */ + @Override + public int size() { + return size; + } + + /** + * Gets an iterator over the list. + * + * @return an iterator over the list + */ + @Override + public Iterator iterator() { + // override to go 75% faster + return listIterator(0); + } + + /** + * Gets a ListIterator over the list. + * WARN: ListIterator may not update list on set(obj) and add(obj) methods if collection is + * IndexedTreeListSet and obj is already in collection. + * + * @return the new iterator + */ + @Override + public ListIterator listIterator() { + // override to go 75% faster + return listIterator(0); + } + + /** + * Gets a ListIterator over the list. + * WARN: ListIterator may not update list on set(obj) and add(obj) methods if collection is + * IndexedTreeListSet and obj is already in collection. + * + * @param fromIndex the index to start from + * @return the new iterator + */ + @Override + public ListIterator listIterator(final int fromIndex) { + // override to go 75% faster + // cannot use EmptyIterator as iterator.add() must work + checkInterval(fromIndex, 0, size()); + return new TreeListIterator(this, fromIndex); + } + + /** + * Converts the list into an array. + * + * @return the list as an array + */ + @Override + public Object[] toArray() { + // override to go 20% faster + final Object[] array = new Object[size()]; + if (root != null) { + root.toArray(array, root.relativePosition); + } + return array; + } + + @Override + public boolean add(E e) { + if (!canAdd(e)) { + return false; + } + return super.add(e); + } + + /** + * Adds a new element to the list. + * + * @param index the index to add before + * @param obj the element to add + */ + @Override + public void add(final int index, final E obj) { + if (!canAdd(obj)) { + return; + } + modCount++; + checkInterval(index, 0, size()); + if (root == null) { + setRoot(new AVLNode(index, obj, null, null, null)); + } else { + setRoot(root.insert(index, obj)); + } + size++; + } + + /** + * Inserts all of the elements in the specified collection into this + * list at the specified position. + * + * @param index index at which to insert the first element from the + * specified collection + * @param collection collection containing elements to be added to this list + * @return true if this list changed as a result of the call + */ + @Override + public boolean addAll(final int index, final Collection collection) { + modCount++; + checkInterval(index, 0, size()); + + int currentIndex = index; + + for (E obj : collection) { + if (canAdd(obj)) { + if (root == null) { + setRoot(new AVLNode(currentIndex, obj, null, null, null)); + } else { + setRoot(root.insert(currentIndex, obj)); + } + size++; + currentIndex++; + } + } + + return currentIndex > index; + } + + /** + * Sets the element at the specified index. + * + * @param index the index to set + * @param obj the object to store at the specified index + * @return the previous object at that index + * @throws IndexOutOfBoundsException if the index is invalid + */ + @Override + public E set(final int index, final E obj) { + final AVLNode node = getNode(index); + final E result = node.value; + node.setValue(obj); + return result; + } + + /** + * Removes the element at the specified index. + * + * @param index the index to remove + * @return the previous object at that index + */ + @Override + public E remove(final int index) { + modCount++; + checkInterval(index, 0, size() - 1); + final E result = get(index); + setRoot(root.remove(index)); + size--; + return result; + } + + /** + * Removes the element at the specified index. + * + * @param o element to be removed from this list, if present + * @return true if this list contained the specified element + */ + @Override + public boolean remove(Object o) { + // Some optimization can be done here + int index = indexOf(o); + if (index < 0) { + return false; + } + remove(index); + return true; + } + + /** + * Clears the list, removing all entries. + */ + @Override + public void clear() { + modCount++; + root = null; + size = 0; + } + + + /** + * Creates a {@link Spliterator} over the elements in this list. + * + * @return spliterator + */ + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.ORDERED); + } + + /** + * Get node by it's index + * @param index index + * @return node + */ + private AVLNode getNode(final int index) { + checkInterval(index, 0, size() - 1); + return root.get(index); + } + + /** + * Set root node. + * @param node new root node + */ + private void setRoot(AVLNode node) { + root = node; + if (node != null) { + node.parent = null; + } + } + + /** + * Check if object can be added to list (e.g. check uniqueness) + * + * @param e element to check if it can be added to collection + * @return true if specified element can be added to collection + */ + abstract protected boolean canAdd(E e); + + /** + * Add node to nodeMap. + * + * @param node node to add + */ + abstract protected void addNode(AVLNode node); + + /** + * Remove node from nodeMap. + * + * @param node node to remove + */ + abstract protected void removeNode(AVLNode node); + + //----------------------------------------------------------------------- + /** + * Checks whether the index is valid. + * + * @param index the index to check + * @param startIndex the first allowed index + * @param endIndex the last allowed index + * @throws IndexOutOfBoundsException if the index is invalid + */ + private void checkInterval(final int index, final int startIndex, final int endIndex) { + if (index < startIndex || index > endIndex) { + throw new IndexOutOfBoundsException("Invalid index:" + index + ", size=" + size()); + } + } + + /** + * Used for tests. + */ + void assertConsistent() { + if (root == null) { + assert(size() == 0); + } else { + assert(size() == root.countNodes()); + } + } + + + //----------------------------------------------------------------------- + /** + * Implements an AVLNode which keeps the offset updated. + *

+ * This node contains the real work. + * TreeList is just there to implement {@link List}. + * The nodes don't know the index of the object they are holding. They + * do know however their position relative to their parent node. + * This allows to calculate the index of a node while traversing the tree. + *

+ * The Faedelung calculation stores a flag for both the left and right child + * to indicate if they are a child (false) or a link as in linked list (true). + */ + class AVLNode { + /** Parent node */ + private AVLNode parent; + /** The left child node or the predecessor if {@link #leftIsPrevious}.*/ + private AVLNode left; + /** Flag indicating that left reference is not a subtree but the predecessor. */ + private boolean leftIsPrevious; + /** The right child node or the successor if {@link #rightIsNext}. */ + private AVLNode right; + /** Flag indicating that right reference is not a subtree but the successor. */ + private boolean rightIsNext; + /** How many levels of left/right are below this one. */ + private int height; + /** The relative position, root holds absolute position. */ + private int relativePosition; + /** The stored element. */ + private E value; + + /** + * Constructs a new node with a relative position. + * + * @param relativePosition the relative position of the node + * @param obj the value for the node + * @param rightFollower the node with the value following this one + * @param leftFollower the node with the value leading this one + */ + private AVLNode(final int relativePosition, final E obj, + final AVLNode parent, final AVLNode rightFollower, final AVLNode leftFollower) { + this.relativePosition = relativePosition; + this.rightIsNext = true; + this.leftIsPrevious = true; + this.parent = parent; + setRight(rightFollower); + setLeft(leftFollower); + setValue(obj); + } + + /** + * Gets the value. + * + * @return the value of this node + */ + E getValue() { + return value; + } + + /** + * Sets the value. + * + * @param obj the value to store + */ + void setValue(final E obj) { + if (this.value != null) { + removeNode(this); + } + this.value = obj; + addNode(this); + } + + /** + * Locate the element with the given index relative to the + * offset of the parent of this node. + */ + AVLNode get(final int index) { + final int indexRelativeToMe = index - relativePosition; + + if (indexRelativeToMe == 0) { + return this; + } + + final AVLNode nextNode = indexRelativeToMe < 0 ? getLeftSubTree() : getRightSubTree(); + if (nextNode == null) { + return null; + } + return nextNode.get(indexRelativeToMe); + } + + /** + * Get position of this node. + */ + int getPosition() { + int position = 0; + AVLNode node = this; + while (node != null) { + position += node.relativePosition; + node = node.parent; + } + return position; + } + + /** + * Stores the node and its children into the array specified. + * + * @param array the array to be filled + * @param index the index of this node + */ + void toArray(final Object[] array, final int index) { + array[index] = value; + if (getLeftSubTree() != null) { + left.toArray(array, index + left.relativePosition); + } + if (getRightSubTree() != null) { + right.toArray(array, index + right.relativePosition); + } + } + + /** + * Gets the next node in the list after this one. + * + * @return the next node + */ + AVLNode next() { + if (rightIsNext || right == null) { + return right; + } + return right.min(); + } + + /** + * Gets the node in the list before this one. + * + * @return the previous node + */ + AVLNode previous() { + if (leftIsPrevious || left == null) { + return left; + } + return left.max(); + } + + /** + * Inserts a node at the position index. + * + * @param index is the index of the position relative to the position of + * the parent node. + * @param obj is the object to be stored in the position. + */ + AVLNode insert(final int index, final E obj) { + final int indexRelativeToMe = index - relativePosition; + + if (indexRelativeToMe <= 0) { + return insertOnLeft(indexRelativeToMe, obj); + } + return insertOnRight(indexRelativeToMe, obj); + } + + private AVLNode insertOnLeft(final int indexRelativeToMe, final E obj) { + if (relativePosition >= 0) { + relativePosition++; + } + if (getLeftSubTree() == null) { + setLeft(new AVLNode(-1, obj, this, this, left), null); + } else { + setLeft(left.insert(indexRelativeToMe, obj), null); + } + final AVLNode ret = balance(); + recalcHeight(); + return ret; + } + + private AVLNode insertOnRight(final int indexRelativeToMe, final E obj) { + if (relativePosition < 0) { + relativePosition--; + } + if (getRightSubTree() == null) { + setRight(new AVLNode(+1, obj, this, right, this), null); + } else { + setRight(right.insert(indexRelativeToMe, obj), null); + } + final AVLNode ret = balance(); + recalcHeight(); + return ret; + } + + //----------------------------------------------------------------------- + /** + * Gets the left node, returning null if its a faedelung. + */ + private AVLNode getLeftSubTree() { + return leftIsPrevious ? null : left; + } + + /** + * Gets the right node, returning null if its a faedelung. + */ + private AVLNode getRightSubTree() { + return rightIsNext ? null : right; + } + + /** + * Gets the rightmost child of this node. + * + * @return the rightmost child (greatest index) + */ + private AVLNode max() { + return getRightSubTree() == null ? this : right.max(); + } + + /** + * Gets the leftmost child of this node. + * + * @return the leftmost child (smallest index) + */ + private AVLNode min() { + return getLeftSubTree() == null ? this : left.min(); + } + + /** + * Removes the node at a given position. + * + * @param index is the index of the element to be removed relative to the position of + * the parent node of the current node. + */ + AVLNode remove(final int index) { + final int indexRelativeToMe = index - relativePosition; + + if (indexRelativeToMe == 0) { + return removeSelf(true); + } + if (indexRelativeToMe > 0) { + setRight(right.remove(indexRelativeToMe), right.right); + if (relativePosition < 0) { + relativePosition++; + } + } else { + setLeft(left.remove(indexRelativeToMe), left.left); + if (relativePosition > 0) { + relativePosition--; + } + } + recalcHeight(); + return balance(); + } + + private AVLNode removeMax() { + if (getRightSubTree() == null) { + return removeSelf(false); + } + setRight(right.removeMax(), right.right); + if (relativePosition < 0) { + relativePosition++; + } + recalcHeight(); + return balance(); + } + + private AVLNode removeMin() { + if (getLeftSubTree() == null) { + return removeSelf(false); + } + setLeft(left.removeMin(), left.left); + if (relativePosition > 0) { + relativePosition--; + } + recalcHeight(); + return balance(); + } + + /** + * Removes this node from the tree. + * + * @return the node that replaces this one in the parent + */ + private AVLNode removeSelf(boolean removeValue) { + removeNode(this); + if (removeValue) { + // avoid further calling removeNode(this) when value is overwritten + value = null; + } + if (getRightSubTree() == null && getLeftSubTree() == null) { + return null; + } + if (getRightSubTree() == null) { + if (relativePosition > 0) { + left.relativePosition += relativePosition + (relativePosition > 0 ? 0 : 1); + } + left.max().setRight(null, right); + return left; + } + if (getLeftSubTree() == null) { + right.relativePosition += relativePosition - (relativePosition < 0 ? 0 : 1); + right.min().setLeft(null, left); + return right; + } + + if (heightRightMinusLeft() > 0) { + // more on the right, so delete from the right + final AVLNode rightMin = right.min(); + if (leftIsPrevious) { + // WARN: This line is not covered by tests. I'm not sure if it's possible to reach this line somehow. + // Original TreeList has the same issue. + setLeft(rightMin.left); + } + setRight(right.removeMin()); + if (relativePosition < 0) { + relativePosition++; + } + setValue(rightMin.value); + } else { + // more on the left or equal, so delete from the left + final AVLNode leftMax = left.max(); + if (rightIsNext) { + // WARN: This line is not covered by tests. I'm not sure if it's possible to reach this line somehow. + // Original TreeList has the same issue. + setRight(leftMax.right); + } + final AVLNode leftPrevious = left.left; + setLeft(left.removeMax()); + if (left == null) { + // special case where left that was deleted was a double link + // only occurs when height difference is equal + leftIsPrevious = true; + setLeft(leftPrevious); + } + if (relativePosition > 0) { + relativePosition--; + } + setValue(leftMax.value); + } + recalcHeight(); + return this; + } + + //----------------------------------------------------------------------- + /** + * Balances according to the AVL algorithm. + */ + private AVLNode balance() { + switch (heightRightMinusLeft()) { + case 1 : + case 0 : + case -1 : + return this; + case -2 : + if (left.heightRightMinusLeft() > 0) { + setLeft(left.rotateLeft(), null); + } + return rotateRight(); + case 2 : + if (right.heightRightMinusLeft() < 0) { + setRight(right.rotateRight(), null); + } + return rotateLeft(); + default : + throw new RuntimeException("tree inconsistent!"); + } + } + + /** + * Gets the relative position. + */ + private int getOffset(final AVLNode node) { + if (node == null) { + return 0; + } + return node.relativePosition; + } + + /** + * Sets the relative position. + */ + private int setOffset(final AVLNode node, final int newOffest) { + if (node == null) { + return 0; + } + final int oldOffset = getOffset(node); + node.relativePosition = newOffest; + return oldOffset; + } + + /** + * Sets the height by calculation. + */ + private void recalcHeight() { + height = Math.max( + getLeftSubTree() == null ? -1 : getLeftSubTree().height, + getRightSubTree() == null ? -1 : getRightSubTree().height) + 1; + } + + /** + * Returns the height of the node or -1 if the node is null. + */ + private int getHeight(final AVLNode node) { + return node == null ? -1 : node.height; + } + + /** + * Returns the height difference right - left + */ + private int heightRightMinusLeft() { + return getHeight(getRightSubTree()) - getHeight(getLeftSubTree()); + } + + private AVLNode rotateLeft() { + final AVLNode newTop = right; // can't be faedelung! + final AVLNode movedNode = getRightSubTree().getLeftSubTree(); + + final int newTopPosition = relativePosition + getOffset(newTop); + final int myNewPosition = -newTop.relativePosition; + final int movedPosition = getOffset(newTop) + getOffset(movedNode); + + setRight(movedNode, newTop); + newTop.setLeft(this, null); + + setOffset(newTop, newTopPosition); + setOffset(this, myNewPosition); + setOffset(movedNode, movedPosition); + return newTop; + } + + private AVLNode rotateRight() { + final AVLNode newTop = left; // can't be faedelung + final AVLNode movedNode = getLeftSubTree().getRightSubTree(); + + final int newTopPosition = relativePosition + getOffset(newTop); + final int myNewPosition = -newTop.relativePosition; + final int movedPosition = getOffset(newTop) + getOffset(movedNode); + + setLeft(movedNode, newTop); + newTop.setRight(this, null); + + setOffset(newTop, newTopPosition); + setOffset(this, myNewPosition); + setOffset(movedNode, movedPosition); + return newTop; + } + + /** + * Sets the left field to the node, or the previous node if that is null + * + * @param node the new left subtree node + * @param previous the previous node in the linked list + */ + private void setLeft(final AVLNode node, final AVLNode previous) { + leftIsPrevious = node == null; + setLeft(leftIsPrevious ? previous : node); + recalcHeight(); + } + + /** + * Sets the left field to the node, or the previous node if that is null + * + * @param node the new left subtree node + */ + private void setLeft(final AVLNode node) { + left = node; + if (left != null && !leftIsPrevious) { + left.parent = this; + } + } + + /** + * Sets the right field to the node, or the next node if that is null + * + * @param node the new left subtree node + * @param next the next node in the linked list + */ + private void setRight(final AVLNode node, final AVLNode next) { + rightIsNext = node == null; + setRight(rightIsNext ? next : node); + recalcHeight(); + } + + /** + * Sets the right field to the node, or the next node if that is null + * + * @param node the new left subtree node + */ + private void setRight(final AVLNode node) { + right = node; + if (right != null && !rightIsNext) { + right.parent = this; + } + } + + /** + * Used for tests. + */ + private int countNodes() { + int c = 1; + if (!leftIsPrevious && left != null) { + assert(left.parent == this); + c += left.countNodes(); + } + if (!rightIsNext && right != null) { + assert(right.parent == this); + c += right.countNodes(); + } + return c; + } + + /** + * Used for debugging. + */ + @Override + public String toString() { + return new StringBuilder() + .append("AVLNode(") + .append(relativePosition) + .append(',') + .append(left != null) + .append(',') + .append(value) + .append(',') + .append(getRightSubTree() != null) + .append(", faedelung ") + .append(rightIsNext) + .append(" )") + .toString(); + } + } + + /** + * A list iterator over the linked list. + */ + private class TreeListIterator implements ListIterator, OrderedIterator { + /** The parent list */ + private final AbstractIndexedTreeList parent; + /** + * Cache of the next node that will be returned by {@link #next()}. + */ + private AVLNode next; + /** + * The index of the next node to be returned. + */ + private int nextIndex; + /** + * Cache of the last node that was returned by {@link #next()} + * or {@link #previous()}. + */ + private AVLNode current; + /** + * The index of the last node that was returned. + */ + private int currentIndex; + /** + * The modification count that the list is expected to have. If the list + * doesn't have this count, then a + * {@link ConcurrentModificationException} may be thrown by + * the operations. + */ + private int expectedModCount; + + /** + * Create a ListIterator for a list. + * + * @param parent the parent list + * @param fromIndex the index to start at + */ + protected TreeListIterator(final AbstractIndexedTreeList parent, final int fromIndex) throws IndexOutOfBoundsException { + super(); + this.parent = parent; + this.expectedModCount = parent.modCount; + this.next = parent.root == null ? null : parent.root.get(fromIndex); + this.nextIndex = fromIndex; + this.currentIndex = -1; + } + + /** + * Checks the modification count of the list is the value that this + * object expects. + * + * @throws ConcurrentModificationException If the list's modification + * count isn't the value that was expected. + */ + protected void checkModCount() { + if (parent.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + public boolean hasNext() { + return nextIndex < parent.size(); + } + + public E next() { + checkModCount(); + if (!hasNext()) { + throw new NoSuchElementException("No element at index " + nextIndex + "."); + } + if (next == null) { + next = parent.root.get(nextIndex); + } + final E value = next.getValue(); + current = next; + currentIndex = nextIndex++; + next = next.next(); + return value; + } + + public boolean hasPrevious() { + return nextIndex > 0; + } + + public E previous() { + checkModCount(); + if (!hasPrevious()) { + throw new NoSuchElementException("Already at start of list."); + } + if (next == null || next.previous() == null) { + next = parent.root.get(nextIndex - 1); + } else { + next = next.previous(); + } + final E value = next.getValue(); + current = next; + currentIndex = --nextIndex; + return value; + } + + public int nextIndex() { + return nextIndex; + } + + public int previousIndex() { + return nextIndex() - 1; + } + + public void remove() { + checkModCount(); + if (currentIndex == -1) { + throw new IllegalStateException(); + } + parent.remove(currentIndex); + if (nextIndex != currentIndex) { + // remove() following next() + nextIndex--; + } + // the AVL node referenced by next may have become stale after a remove + // reset it now: will be retrieved by next call to next()/previous() via nextIndex + next = null; + current = null; + currentIndex = -1; + expectedModCount++; + } + + public void set(final E obj) { + if (!supportSetInIterator) { + throw new UnsupportedOperationException("Set operation is not supported"); + } + if (canAdd(obj)) { + checkModCount(); + if (current == null) { + throw new IllegalStateException(); + } + current.setValue(obj); + } + } + + public void add(final E obj) { + if (canAdd(obj)) { + checkModCount(); + parent.add(nextIndex, obj); + current = null; + currentIndex = -1; + nextIndex++; + expectedModCount++; + } + } + } + +} diff --git a/src/main/java/org/apache/commons/collections4/list/IndexedTreeList.java b/src/main/java/org/apache/commons/collections4/list/IndexedTreeList.java new file mode 100644 index 0000000000..572d9d109f --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/IndexedTreeList.java @@ -0,0 +1,252 @@ +/* + * 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.list; + +import java.util.*; +import java.util.function.Function; + +/** + *

+ * As a List this data structure stores order of elements and + * provides access by index. It's is optimised for fast insertions, removals + * and searching by any index or object in the list. + *

+ *

+ * IndexedTreeList can be suitable for tasks which requires fast modification in the + * middle of a list and provides fast contains and indexOf. + *

+ *

+ * Get by index is O(log n). + * Insert (head, tail, middle) and remove(by index or by value) are O(log n) + * in most cases but can be up to O((log n) ^ 2) in cases when List contains big + * amount of elements equals to each other. Actual complexity is + * O((log n) * (1 + log m)) where m is amount of elements equal to inserted/removed. + * indexOf is O(log n). + * Contains is O(1) or O(log n) depending on Map implementation. + *

+ *

+ * Internally it uses Map (HashMap by default) and AVL tree. + * HashMap can be replaced to TreeMap, this will slightly reduce overall performance + * but will eliminate problems with hash collisions and hash table resizing. + * Using TreeMap with custom Comparator will provide indexOf by custom criteria. + * Using IdentityHashMap will provide indexOf by object's identity. + *

+ *

+ * Objects equality is checked by Map, so objects should be immutable for Map + * consistency. + *

+ *

+ * Code is based on apache common collections TreeList. + *

+ * Comparing to TreeList this data structure: + *
    + *
  • Has slightly slower insertion/removing operations, O(log n) in most cases, O((log n) ^ 2) in + * worst cases (if TreeMap is used)
  • + *
  • Requires more memory, however it's still O(n)
  • + *
  • Has greatly improved contains and indexOf operations, O(log n) while TreeList has O(n)
  • + *
+ * + *

+ * As this implementation is slightly slower and require more memory it's recommended to use + * TreeList in cases when no searching is required or IndexedTreeListSet + * in cases where unique elements should be stored. + *

+ * + * @author Aleksandr Maksymenko + */ +public class IndexedTreeList extends AbstractIndexedTreeList { + + private final Comparator NODE_COMPARATOR = Comparator.comparingInt(AVLNode::getPosition); + private final Function> NEW_NODE_TREE_SET = k -> new TreeSet(NODE_COMPARATOR); + + /** Map from element to it's node or nodes */ + protected final Map> nodeMap; + + //----------------------------------------------------------------------- + /** + * Constructs a new empty list. + */ + public IndexedTreeList() { + this(new HashMap<>()); + } + + /** + * Constructs a new empty list. + * @param map Map implementation. It defines how elements would be compared. For example HashMap (by hashcode/equals), + * TreeMap (by compareTo or Comparator), IdentityHashMap (by identity). Specified map should be empty. + */ + public IndexedTreeList(final Map map) { + this.nodeMap = map; + } + + /** + * Constructs a new list that copies the specified collection. + * + * @param coll The collection to copy + * @throws NullPointerException if the collection is null + */ + public IndexedTreeList(final Collection coll) { + this(coll, new HashMap<>()); + } + + /** + * Constructs a new list that copies the specified collection. + * + * @param coll The collection to copy + * @param map Map implementation. It defines how elements would be compared. For example HashMap (by hashcode/equals), + * TreeMap (by compareTo or Comparator), IdentityHashMap (by identity). Specified map should be empty. + * @throws NullPointerException if the collection is null + */ + public IndexedTreeList(final Collection coll, final Map map) { + this.nodeMap = map; + for (E e : coll) { + add(e); + } + } + + //----------------------------------------------------------------------- + + /** + * Searches for the index of an object in the list. + * + * @param object the object to search + * @return the index of the object, -1 if not found + */ + @Override + public int indexOf(final Object object) { + TreeSet nodes = nodeMap.get(object); + if (nodes == null || nodes.isEmpty()) { + return -1; + } + return nodes.first().getPosition(); + } + + /** + * Searches for the last index of an object in the list. + * + * @param object the object to search + * @return the index of the object, -1 if not found + */ + @Override + public int lastIndexOf(final Object object) { + TreeSet nodes = nodeMap.get(object); + if (nodes == null || nodes.isEmpty()) { + return -1; + } + return nodes.last().getPosition(); + } + + /** + * Searches for all indexes of an objects in the list equals to specified object. + * + * @param object the object to search + * @return array of indexes of the objects + */ + public int[] indexes(final Object object) { + TreeSet nodes = nodeMap.get(object); + if (nodes == null || nodes.isEmpty()) { + return new int[0]; + } + int[] indexes = new int[nodes.size()]; + int i = 0; + for (AVLNode node : nodes) { + indexes[i++] = node.getPosition(); + } + return indexes; + } + + /** + * Get amount of objects in the list equals to specified object. + * + * @param object the object to search + * @return amount of objects + */ + public int count(final Object object) { + TreeSet nodes = nodeMap.get(object); + if (nodes == null || nodes.isEmpty()) { + return 0; + } + return nodes.size(); + } + + /** + * Searches for the presence of an object in the list. + * + * @param object the object to check + * @return true if the object is found + */ + @Override + public boolean contains(final Object object) { + return nodeMap.containsKey(object); + } + + /** + * Clears the list, removing all entries. + */ + @Override + public void clear() { + super.clear(); + nodeMap.clear(); + } + + /** + * Get unordered Set of unique values. + * + * @return unordered Set of unique values + */ + public Set uniqueValues() { + return nodeMap.keySet(); + } + + /** + * Check if set does not contains an object. + * + * @param e element to check if it can be added to collection + * @return true if specified element can be added to collection + */ + @Override + protected boolean canAdd(E e) { + if (e == null) { + throw new NullPointerException("Null elements are not allowed"); + } + return true; + } + + /** + * Add node to nodeMap. + */ + @Override + protected void addNode(AVLNode node) { + nodeMap.computeIfAbsent(node.getValue(), NEW_NODE_TREE_SET).add(node); + } + + /** + * Remove node from nodeMap. + */ + @Override + protected void removeNode(AVLNode node) { + TreeSet nodes = nodeMap.remove(node.getValue()); + if (nodes == null) { + return; + } + nodes.remove(node); + if (!nodes.isEmpty()) { + nodeMap.put(nodes.first().getValue(), nodes); + } + } + +} diff --git a/src/main/java/org/apache/commons/collections4/list/IndexedTreeListSet.java b/src/main/java/org/apache/commons/collections4/list/IndexedTreeListSet.java new file mode 100644 index 0000000000..374046fa18 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/IndexedTreeListSet.java @@ -0,0 +1,220 @@ +/* + * 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.list; + +import java.util.*; + +/** + *

+ * Data structure which implements both List and Set. + *

+ *

+ * As a List this data structure stores order of elements and + * provides access by index. It's is optimised for fast insertions, removals + * and searching by any index or object in the list. + * As a Set this data structure stores unique elements only. + *

+ *

+ * IndexedTreeListSet can be suitable for tasks which requires fast modification + * in the middle of a list and provides fast contains and indexOf operations. + *

+ *

+ * Get by index, insert (head, tail, middle), remove(by index or by value) + * and indexOf are all O(log n). Contains is O(1) or O(log n) depending on Map + * implementation. + *

+ *

+ * Internally it uses Map (HashMap by default) and AVL tree. + * HashMap can be replaced to TreeMap, this will slightly reduce overall performance + * but will eliminate problems with hash collisions and hash table resizing. + *

+ *

+ * Objects equality is checked by Map, so objects should be immutable for Map + * consistency. + *

+ *

+ * Code is based on apache common collections TreeList. + *

+ * Comparing to TreeList this data structure: + *
    + *
  • Contains unique elements
  • + *
  • Has almost the same or slightly slower insertion/removing operations, O(log n)
  • + *
  • Requires more memory, however it's still O(n)
  • + *
  • Has greatly improved contains and indexOf operations, O(log n) while TreeList has O(n)
  • + *
  • Has limited usage of set(index, object) method due to requirement of object uniqueness
  • + *
  • Sorting may not work correctly as same object could be set in 2 places during objects swapping
  • + *
+ * + * @author Aleksandr Maksymenko + */ +public class IndexedTreeListSet extends AbstractIndexedTreeList implements Set { + + /** Map from element to it's node or nodes */ + protected final Map nodeMap; + + //----------------------------------------------------------------------- + /** + * Constructs a new empty list. + */ + public IndexedTreeListSet() { + this(new HashMap<>()); + } + + /** + * Constructs a new empty list. + * @param map Map implementation. It defines how elements would be compared. For example HashMap (by hashcode/equals), + * TreeMap (by compareTo or Comparator), IdentityHashMap (by identity). Specified map should be empty. + */ + public IndexedTreeListSet(final Map map) { + this.nodeMap = map; + } + + /** + * Constructs a new list that copies the specified collection. + * + * @param coll The collection to copy + * @throws NullPointerException if the collection is null + */ + public IndexedTreeListSet(final Collection coll) { + this(coll, new HashMap<>()); + } + + /** + * Constructs a new list that copies the specified collection. + * + * @param coll The collection to copy + * @param map Map implementation. It defines how elements would be compared. For example HashMap (by hashcode/equals), + * TreeMap (by compareTo or Comparator), IdentityHashMap (by identity). Specified map should be empty. + * @throws NullPointerException if the collection is null + */ + public IndexedTreeListSet(final Collection coll, final Map map) { + this.nodeMap = map; + for (E e : coll) { + add(e); + } + } + + //----------------------------------------------------------------------- + + /** + * Searches for the index of an object in the list. + * + * @param object the object to search + * @return the index of the object, -1 if not found + */ + @Override + public int indexOf(final Object object) { + AVLNode node = nodeMap.get(object); + if (node == null) { + return -1; + } + return node.getPosition(); + } + + /** + * Searches for the last index of an object in the list. + * + * @param object the object to search + * @return the index of the object, -1 if not found + */ + @Override + public int lastIndexOf(final Object object) { + return indexOf(object); + } + + /** + * Returns an unmodifiable view of the portion of this list between the specified + * fromIndex, inclusive, and toIndex, exclusive. + * + * @param fromIndex low endpoint (inclusive) of the subList + * @param toIndex high endpoint (exclusive) of the subList + * @return an unmodifiable view of the specified range within this list + */ + @Override + public List subList(int fromIndex, int toIndex) { + return Collections.unmodifiableList(super.subList(fromIndex, toIndex)); + } + + /** + * Searches for the presence of an object in the list. + * + * @param object the object to check + * @return true if the object is found + */ + @Override + public boolean contains(final Object object) { + return nodeMap.containsKey(object); + } + + /** + * Sets the element at the specified index. + * If specified value already exist in Set, it will be removed at old position. + * E.g. if TreeListSet contains ["A", "B", "C"] and set(2, "A") is invoked, then result will be ["B", "A"]. + * + * @param index the index to set + * @param obj the object to store at the specified index + * @return previous value + */ + @Override + public E set(int index, final E obj) { + final int pos = indexOf(obj); + if (pos >= 0 && pos != index) { + remove(pos); + if (pos < index) { + index--; + } + } + return super.set(index, obj); + } + + /** + * Clears the list, removing all entries. + */ + @Override + public void clear() { + super.clear(); + nodeMap.clear(); + } + + /** + * Check if set does not contains an object. + */ + @Override + protected boolean canAdd(E e) { + if (e == null) { + throw new NullPointerException("Null elements are not allowed"); + } + return !nodeMap.containsKey(e); + } + + /** + * Add node to nodeMap. + */ + @Override + protected void addNode(AVLNode node) { + nodeMap.put(node.getValue(), node); + } + + /** + * Remove node from nodeMap. + */ + @Override + protected void removeNode(AVLNode node) { + nodeMap.remove(node.getValue()); + } + +} diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedTreeListIteratorRandomizedTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListIteratorRandomizedTest.java new file mode 100644 index 0000000000..b1f4dfde3d --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListIteratorRandomizedTest.java @@ -0,0 +1,339 @@ +/* + * 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.list; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.*; + +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class IndexedTreeListIteratorRandomizedTest { + + private Random random; + private Set elementsSet; + private List elementsList; + private List removedList; + + private Class listClass; + private List testList; + + private int seed; + private int iterations; + + public IndexedTreeListIteratorRandomizedTest(Class listClass, int seed, int iterations) { + this.listClass = listClass; + this.seed = seed; + this.iterations = iterations; + } + + @Before + public void setUp() throws Exception { + random = new Random(seed); + elementsSet = new HashSet<>(); + elementsList = new ArrayList<>(); + removedList = new ArrayList<>(); + testList = (List) listClass.newInstance(); + } + + @Parameterized.Parameters(name = "{0} {1} {2}") + public static Collection parameters() { + return Arrays.asList(new Object[][] { + { IndexedTreeListSet.class, 9999, 1}, + { IndexedTreeListSet.class, 9999, 2}, + { IndexedTreeListSet.class, 9999, 3}, + { IndexedTreeListSet.class, 9999, 4}, + { IndexedTreeListSet.class, 9999, 5}, + { IndexedTreeListSet.class, 9999, 10}, + { IndexedTreeListSet.class, 9999, 100}, + { IndexedTreeListSet.class, 9999, 1000}, +// {IndexedTreeListSet.class, 9999, 10000}, + + {IndexedTreeList.class, 9999, 1}, + {IndexedTreeList.class, 9999, 2}, + {IndexedTreeList.class, 9999, 3}, + {IndexedTreeList.class, 9999, 4}, + {IndexedTreeList.class, 9999, 5}, + {IndexedTreeList.class, 9999, 10}, + {IndexedTreeList.class, 9999, 100}, + {IndexedTreeList.class, 9999, 1000}, +// {IndexedTreeList.class, 9999, 10000}, + +// {TreeList.class, 9999, 1}, +// {TreeList.class, 9999, 2}, +// {TreeList.class, 9999, 3}, +// {TreeList.class, 9999, 4}, +// {TreeList.class, 9999, 5}, +// {TreeList.class, 9999, 10}, +// {TreeList.class, 9999, 100}, +// {TreeList.class, 9999, 1000}, +//// {TreeList.class, 9999, 10000}, + }); + } + +// @Parameterized.Parameters(name = "{0} {1} {2}") +// public static Collection parameters() { +// ArrayList params = new ArrayList(); +// Random r = new Random(); +// for (int i = 0; i < 1000; i++) { +// params.add(new Object[] {IndexedTreeListSet.class, r.nextInt(), r.nextInt(1 << (r.nextInt(12))) + 1}); +// params.add(new Object[] {IndexedTreeList.class, r.nextInt(), r.nextInt(1 << (r.nextInt(12))) + 1}); +// params.add(new Object[] {TreeList.class, r.nextInt(), r.nextInt(1 << (r.nextInt(12))) + 1}); +// } +// return params; +// } + + @Test + public void iteratorTest() throws Exception { + init(); + Iterator arrayListIterator = elementsList.iterator(); + Iterator testListIterator = testList.iterator(); + assertReference(); + while (arrayListIterator.hasNext()) { + Long element = arrayListIterator.next(); + assertEquals(element, testListIterator.next()); + if (random.nextBoolean()) { + arrayListIterator.remove(); + testListIterator.remove(); + removedList.add(element); + } + } + elementsSet.removeAll(removedList); + assertReference(); + } + + @Test + public void listIteratorForwardTest() throws Exception { + init(); + ListIterator arrayListIterator = elementsList.listIterator(); + ListIterator testListIterator = testList.listIterator(); + assertReference(); + while (arrayListIterator.hasNext()) { + Long element = arrayListIterator.next(); + assertEquals(element, testListIterator.next()); + assertEquals(arrayListIterator.nextIndex(), testListIterator.nextIndex()); + assertEquals(arrayListIterator.previousIndex(), testListIterator.previousIndex()); + if (random.nextBoolean()) { + arrayListIterator.remove(); + testListIterator.remove(); + removedList.add(element); + } else if (random.nextBoolean()) { + Long val = getRandomNotExisting(); + arrayListIterator.set(val); + testListIterator.set(val); + removedList.add(element); + elementsSet.add(val); + } + if (random.nextBoolean()) { + Long val = getRandomNotExisting(); + arrayListIterator.add(val); + testListIterator.add(val); + elementsSet.add(val); + } + } + elementsSet.removeAll(removedList); + assertReference(); + } + + @Test + public void listIteratorBackwardTest() throws Exception { + init(); + ListIterator arrayListIterator = elementsList.listIterator(elementsList.size() - 1); + ListIterator testListIterator = testList.listIterator(elementsList.size() - 1); + assertReference(); + while (arrayListIterator.hasPrevious()) { + Long element = arrayListIterator.previous(); + assertEquals(element, testListIterator.previous()); + assertEquals(arrayListIterator.nextIndex(), testListIterator.nextIndex()); + assertEquals(arrayListIterator.previousIndex(), testListIterator.previousIndex()); + if (random.nextBoolean()) { + arrayListIterator.remove(); + testListIterator.remove(); + removedList.add(element); + } else if (random.nextBoolean()) { + Long val = getRandomNotExisting(); + arrayListIterator.set(val); + testListIterator.set(val); + removedList.add(element); + elementsSet.add(val); + } + if (random.nextBoolean()) { + Long val = getRandomNotExisting(); + arrayListIterator.add(val); + testListIterator.add(val); + elementsSet.add(val); + } + } + elementsSet.removeAll(removedList); + assertReference(); + } + + @Test + public void listIteratorRandomizedTest() throws Exception { + init(); + int initialPosition = random.nextInt(elementsList.size()); + ListIterator arrayListIterator = elementsList.listIterator(initialPosition); + ListIterator testListIterator = testList.listIterator(initialPosition); + assertReference(); + + Long element = null; + for (int i = 0; i < iterations; i++) { + assertEquals(arrayListIterator.hasNext(), testListIterator.hasNext()); + assertEquals(arrayListIterator.hasPrevious(), testListIterator.hasPrevious()); + assertEquals(arrayListIterator.nextIndex(), testListIterator.nextIndex()); + assertEquals(arrayListIterator.previousIndex(), testListIterator.previousIndex()); + + int operation = random.nextInt(5); + //System.out.println("" + i + " : " + arrayListIterator.previousIndex() + ", " + arrayListIterator.nextIndex() + " : " + operation); + + switch (operation) { + case 0: + if (arrayListIterator.hasNext()) { + element = arrayListIterator.next(); + assertEquals(element, testListIterator.next()); + } + break; + case 1: + if (arrayListIterator.hasPrevious()) { + element = arrayListIterator.previous(); + assertEquals(element, testListIterator.previous()); + } + break; + case 2: + if (element != null) { + arrayListIterator.remove(); + testListIterator.remove(); + removedList.add(element); + element = null; + } + break; + case 3: + if (element != null) { + Long val = getRandomNotExisting(); + arrayListIterator.set(val); + testListIterator.set(val); + removedList.add(element); + elementsSet.add(val); + element = val; + } + break; + case 4: + Long val = getRandomNotExisting(); + arrayListIterator.add(val); + testListIterator.add(val); + elementsSet.add(val); + element = null; + break; + } + assertEquals(elementsList, testList); + } + elementsSet.removeAll(removedList); + assertReference(); + } + + private void init() { + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size() + 1); + testList.add(index, addRandom(index)); // can be optimized + } + assertReference(); + } + + private Long addRandom() { + while (true) { + Long value = random.nextLong(); + if (elementsSet.add(value)) { + elementsList.add(value); + return value; + } + } + } + + private Long addRandom(int index) { + while (true) { + Long value = random.nextLong(); + if (elementsSet.add(value)) { + elementsList.add(index, value); + return value; + } + } + } + + private Long getRandomExisting() { + if (elementsList.isEmpty()) { + return null; + } + return elementsList.get(random.nextInt(elementsList.size())); + } + + private Long getRandomNotExisting() { + while (true) { + Long value = random.nextLong(); + if (!elementsSet.contains(value)) { + return value; + } + } + } + + private Long getRandomRemoved() { + if (removedList.isEmpty()) { + return null; + } + return removedList.get(random.nextInt(removedList.size())); + } + + private Long removeRandomValue() { + if (elementsList.isEmpty()) { + return null; + } + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + elementsSet.remove(value); + elementsList.remove(index); + removedList.add(value); + return value; + } + + private int removeRandomIndex() { + if (elementsList.isEmpty()) { + return -1; + } + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + elementsSet.remove(value); + elementsList.remove(index); + removedList.add(value); + return index; + } + + private void assertReference() { + if (testList instanceof Set) { + assertEquals(elementsSet, testList); + } else { + assertEquals(elementsSet, new HashSet<>(testList)); + } + assertEquals(elementsList, testList); + + if (testList instanceof AbstractIndexedTreeList) { + ((AbstractIndexedTreeList) testList).assertConsistent(); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedTreeListRandomizedTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListRandomizedTest.java new file mode 100644 index 0000000000..bef847cea5 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListRandomizedTest.java @@ -0,0 +1,480 @@ +/* + * 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.list; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class IndexedTreeListRandomizedTest { + + private Random random; + private Set elementsSet; + private List elementsList; + private List removedList; + + private IndexedTreeList testList; + + private int seed; + private int iterations; + + public IndexedTreeListRandomizedTest(int seed, int iterations) { + this.seed = seed; + this.iterations = iterations; + } + + @Before + public void setUp() throws Exception { + random = new Random(seed); + elementsSet = new HashSet<>(); + elementsList = new ArrayList<>(); + removedList = new ArrayList<>(); + testList = new IndexedTreeList<>(); + } + + @Parameterized.Parameters(name = "{0} {1}") + public static Collection parameters() { + return Arrays.asList(new Object[][] { + {9999, 1}, + {9999, 2}, + {9999, 3}, + {9999, 4}, + {9999, 5}, + {9999, 10}, + {9999, 100}, + {9999, 1000}, +// {9999, 10000}, + }); + } + +// @Parameterized.Parameters(name = "{0} {1}") +// public static Collection parameters() { +// ArrayList params = new ArrayList(); +// Random r = new Random(); +// for (int i = 0; i < 1000; i++) { +// params.add(new Object[] {r.nextInt(), r.nextInt(1 << (r.nextInt(12))) + 1}); +// } +// return params; +// } + + @Test + public void addToTail() throws Exception { + for (int i = 0; i < iterations; i++) { + testList.add(addRandom()); + assertReference(); + } + } + + @Test + public void addToHead() throws Exception { + for (int i = 0; i < iterations; i++) { + testList.add(0, addRandom(0)); + assertReference(); + } + } + + @Test + public void addToMiddle() throws Exception { + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size() + 1); + testList.add(index, addRandom(index)); + assertReference(); + } + } + + @Test + public void setNewValue() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size()); + final Long value = elementsList.get(index); + elementsSet.remove(value); + final long newValue = random.nextLong(); + elementsSet.add(newValue); + elementsList.set(index, newValue); + testList.set(index, newValue); + assertReference(); + } + } + + @Test + public void setExistingValue() throws Exception { + init(); + final List indexes = IntStream.range(0, iterations) + .mapToObj(Integer::valueOf) + .collect(Collectors.toList()); + Collections.shuffle(indexes, random); + for (int i = 0; i < indexes.size() - 1; i += 2) { + int indexFrom = indexes.get(i); + int indexTo = indexes.get(i + 1); + final Long value = elementsList.get(indexFrom); + elementsSet.remove(elementsList.get(indexTo)); + elementsList.set(indexTo, value); + testList.set(indexTo, value); + assertReference(); + } + } + + @Test + public void removeByIndex() throws Exception { + init(); + while (!testList.isEmpty()) { + int index = removeRandomIndex(); + testList.remove(index); + assertReference(); + } + } + + @Test + public void removeByValue() throws Exception { + init(); + while (!testList.isEmpty()) { + Long value = removeRandomValue(); + assertTrue(testList.contains(value)); + testList.remove(value); + assertFalse(testList.contains(value)); + assertReference(); + } + } + + @Test + public void removeFirstLast() throws Exception { + init(); + while (!testList.isEmpty()) { + removeByIndex(0); + testList.remove(0); + if (testList.size() > 0) { + removeByIndex(testList.size() - 1); + testList.remove(testList.size() - 1); + } + assertReference(); + } + } + + @Test + public void removeMiddle() throws Exception { + init(); + while (!testList.isEmpty()) { + removeByIndex(testList.size() / 2); + testList.remove(testList.size() / 2); + assertReference(); + } + } + + @Test + public void contains() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + assertTrue(testList.contains(getRandomExisting())); + assertFalse(testList.contains(getRandomNotExisting())); + } + assertReference(); + } + + @Test + public void get() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + assertEquals(value, testList.get(index)); + } + assertReference(); + } + + @Test + public void indexOf() throws Exception { + init(); + int initialSize = elementsList.size(); + for (int i = 0; i < initialSize; i++) { + elementsList.add(elementsList.get(i)); + testList.add(elementsList.get(i)); + } + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(initialSize); + Long value = elementsList.get(index); + assertEquals(index, testList.indexOf(value)); + } + assertReference(); + } + + @Test + public void lastIndexOf() throws Exception { + init(); + int initialSize = elementsList.size(); + for (int i = 0; i < initialSize; i++) { + elementsList.add(elementsList.get(i)); + testList.add(elementsList.get(i)); + } + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(initialSize); + Long value = elementsList.get(index); + assertEquals(index + initialSize, testList.lastIndexOf(value)); + } + assertReference(); + } + + @Test + public void indexes() throws Exception { + init(); + int initialSize = elementsList.size(); + for (int i = 0; i < initialSize; i++) { + elementsList.add(elementsList.get(i)); + testList.add(elementsList.get(i)); + } + for (int i = 0; i < initialSize; i++) { + elementsList.add(elementsList.get(i)); + testList.add(i, elementsList.get(i)); + } + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(initialSize); + Long value = elementsList.get(index); + assertArrayEquals(new int[] {index, index + initialSize, index + initialSize * 2}, testList.indexes(value)); + assertEquals(3, testList.count(value)); + } + assertReference(); + } + + @Test + public void indexesWithRemove() throws Exception { + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size() + 1); + testList.add(index, addRandom(index)); + + index = random.nextInt(elementsList.size() + 1); + Long existingValue = getRandomExisting(); + testList.add(index, existingValue); + elementsList.add(index, existingValue); + + index = random.nextInt(elementsList.size()); + existingValue = getRandomExisting(); + testList.set(index, existingValue); + elementsList.set(index, existingValue); + + index = random.nextInt(elementsList.size()); + testList.remove(index); + elementsList.remove(index); + } + + assertEquals(elementsList, testList); + + Map> indexes = new HashMap<>(); + for (int i = 0; i < elementsList.size(); i++) { + indexes.computeIfAbsent(elementsList.get(i), k -> new ArrayList()).add(i); + } + + assertEquals(indexes.keySet(), testList.nodeMap.keySet()); + + for (Entry> entry : indexes.entrySet()) { + assertArrayEquals(entry.getValue().stream().mapToInt(i->i).toArray(), + testList.indexes(entry.getKey())); + assertEquals(entry.getValue().size(), testList.count(entry.getKey())); + } + + assertArrayEquals(elementsList.toArray(), testList.toArray()); + assertArrayEquals(elementsList.toArray(new Long[0]), testList.toArray(new Long[0])); + } + + @Test + public void addRandomExisting() throws Exception { + for (int i = 0; i < iterations; i++) { + testList.add(addRandom()); + Long randomExisting = getRandomExisting(); + testList.add(randomExisting); + elementsList.add(randomExisting); + assertReference(); + } + + assertArrayEquals(elementsList.toArray(), testList.toArray()); + assertArrayEquals(elementsList.toArray(new Long[0]), testList.toArray(new Long[0])); + } + + @Test + public void addSameExisting() throws Exception { + for (int i = 0; i < iterations; i++) { + testList.add(addRandom()); + testList.add(elementsList.get(0)); + elementsList.add(elementsList.get(0)); + assertReference(); + } + + assertArrayEquals(elementsList.toArray(), testList.toArray()); + assertArrayEquals(elementsList.toArray(new Long[0]), testList.toArray(new Long[0])); + } + + @Test + public void addContainsIndexOfRemove() throws Exception { + init(); + while (!testList.isEmpty()) { + // add + int index = random.nextInt(elementsList.size() + 1); + testList.add(index, addRandom(index)); + + // contains + assertTrue(testList.contains(getRandomExisting())); + assertFalse(testList.contains(getRandomNotExisting())); + + // indexOf + index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + assertEquals(value, testList.get(index)); + assertEquals(index, testList.indexOf(value)); + + // remove + final Long valueToRemove = removeRandomValue(); + assertTrue(testList.remove(valueToRemove)); + final int indexToRemove = removeRandomIndex(); + final Long removedValue = testList.get(indexToRemove); + assertEquals(removedValue, testList.remove(indexToRemove)); + + // check add nulls + try { + testList.add(null); + fail("No exception on adding null"); + } catch (NullPointerException e) {} + try { + testList.add(random.nextInt(elementsList.size() + 1), null); + fail("No exception on adding null"); + } catch (NullPointerException e) {} + // check not existing indexOf + assertEquals(-1, testList.indexOf(random.nextLong())); + assertEquals(-1, testList.lastIndexOf(random.nextLong())); + assertEquals(0, testList.indexes(random.nextLong()).length); + assertEquals(0, testList.count(random.nextLong())); + assertFalse(testList.remove(random.nextLong())); + + assertReference(); + } + } + + @Test + public void clear() { + init(); + testList.clear(); + elementsList.clear(); + elementsSet.clear(); + assertReference(); + } + + @Test + public void constructorWithCollection() { + init(); + testList = new IndexedTreeList<>(elementsList); + assertReference(); + } + + @Test + public void constructorWithTreeMap() { + init(); + testList = new IndexedTreeList<>(elementsList, new TreeMap()); + assertReference(); + } + + private void init() { + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size() + 1); + testList.add(index, addRandom(index)); // can be optimized + } + assertReference(); + } + + private Long addRandom() { + while (true) { + Long value = random.nextLong(); + if (elementsSet.add(value)) { + elementsList.add(value); + return value; + } + } + } + + private Long addRandom(int index) { + while (true) { + Long value = random.nextLong(); + if (elementsSet.add(value)) { + elementsList.add(index, value); + return value; + } + } + } + + private Long getRandomExisting() { + if (elementsList.isEmpty()) { + return null; + } + return elementsList.get(random.nextInt(elementsList.size())); + } + + private Long getRandomNotExisting() { + while (true) { + Long value = random.nextLong(); + if (!elementsSet.contains(value)) { + return value; + } + } + } + + private Long getRandomRemoved() { + if (removedList.isEmpty()) { + return null; + } + return removedList.get(random.nextInt(removedList.size())); + } + + private Long removeRandomValue() { + if (elementsList.isEmpty()) { + return null; + } + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + elementsSet.remove(value); + elementsList.remove(index); + removedList.add(value); + return value; + } + + private int removeRandomIndex() { + if (elementsList.isEmpty()) { + return -1; + } + int index = random.nextInt(elementsList.size()); + removeByIndex(index); + return index; + } + + private void removeByIndex(final int index) { + Long value = elementsList.get(index); + elementsSet.remove(value); + elementsList.remove(index); + removedList.add(value); + } + + private void assertReference() { + assertEquals(elementsList, testList); + assertEquals(elementsSet, testList.uniqueValues()); + testList.assertConsistent(); + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedTreeListSetRandomizedTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListSetRandomizedTest.java new file mode 100644 index 0000000000..f65086d5e1 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListSetRandomizedTest.java @@ -0,0 +1,393 @@ +/* + * 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.list; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.*; + +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class IndexedTreeListSetRandomizedTest { + + private Random random; + private Set elementsSet; + private List elementsList; + private List removedList; + + private IndexedTreeListSet testListSet; + + private int seed; + private int iterations; + + public IndexedTreeListSetRandomizedTest(int seed, int iterations) { + this.seed = seed; + this.iterations = iterations; + } + + @Before + public void setUp() throws Exception { + random = new Random(seed); + elementsSet = new HashSet<>(); + elementsList = new ArrayList<>(); + removedList = new ArrayList<>(); + testListSet = new IndexedTreeListSet<>(); + } + + @Parameterized.Parameters(name = "{0} {1}") + public static Collection parameters() { + return Arrays.asList(new Object[][] { + {9999, 1}, + {9999, 2}, + {9999, 3}, + {9999, 4}, + {9999, 5}, + {9999, 10}, + {9999, 100}, + {9999, 1000}, +// {9999, 10000}, + }); + } + +// @Parameterized.Parameters(name = "{0} {1}") +// public static Collection parameters() { +// ArrayList params = new ArrayList(); +// Random r = new Random(); +// for (int i = 0; i < 1000; i++) { +// params.add(new Object[] {r.nextInt(), r.nextInt(1 << (r.nextInt(12))) + 1}); +// } +// return params; +// } + + @Test + public void addToTail() throws Exception { + for (int i = 0; i < iterations; i++) { + testListSet.add(addRandom()); + assertReference(); + } + } + + @Test + public void addToHead() throws Exception { + for (int i = 0; i < iterations; i++) { + testListSet.add(0, addRandom(0)); + assertReference(); + } + } + + @Test + public void addToMiddle() throws Exception { + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size() + 1); + testListSet.add(index, addRandom(index)); + assertReference(); + } + } + + @Test + public void setNewValue() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size()); + final Long value = elementsList.get(index); + elementsSet.remove(value); + final long newValue = random.nextLong(); + elementsSet.add(newValue); + elementsList.set(index, newValue); + final Long oldValue = testListSet.set(index, newValue); + assertEquals(value, oldValue); + assertReference(); + } + } + + @Test + public void removeElementOnSetExistingValue() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int indexFrom = random.nextInt(elementsList.size()); + int indexTo = random.nextInt(elementsList.size()); + final Long value = elementsList.get(indexFrom); + final Long oldValue = testListSet.set(indexTo, value); + assertEquals(elementsList.get(indexTo), oldValue); + if (indexFrom != indexTo) { + elementsSet.remove(elementsList.get(indexTo)); + elementsList.set(indexTo, elementsList.get(indexFrom)); + elementsList.remove(indexFrom); + } + assertReference(); + } + } + + @Test + public void removeByIndex() throws Exception { + init(); + while (!testListSet.isEmpty()) { + int index = removeRandomIndex(); + testListSet.remove(index); + assertReference(); + } + } + + @Test + public void removeByValue() throws Exception { + init(); + while (!testListSet.isEmpty()) { + Long value = removeRandomValue(); + assertTrue(testListSet.contains(value)); + testListSet.remove(value); + assertFalse(testListSet.contains(value)); + assertReference(); + } + } + + @Test + public void removeFirstLast() throws Exception { + init(); + while (!testListSet.isEmpty()) { + removeByIndex(0); + testListSet.remove(0); + if (testListSet.size() > 0) { + removeByIndex(testListSet.size() - 1); + testListSet.remove(testListSet.size() - 1); + } + assertReference(); + } + } + + @Test + public void contains() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + assertTrue(testListSet.contains(getRandomExisting())); + assertFalse(testListSet.contains(getRandomNotExisting())); + } + assertReference(); + } + + @Test + public void get() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + assertEquals(value, testListSet.get(index)); + } + assertReference(); + } + + @Test + public void indexOf() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + assertEquals(index, testListSet.indexOf(value)); + } + assertReference(); + } + + @Test + public void lastIndexOf() throws Exception { + init(); + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + assertEquals(index, testListSet.lastIndexOf(value)); + } + assertReference(); + } + + @Test + public void addExisting() throws Exception { + for (int i = 0; i < iterations; i++) { + testListSet.add(addRandom()); + testListSet.add(getRandomExisting()); + testListSet.add(random.nextInt(elementsList.size() + 1), getRandomExisting()); + assertReference(); + } + } + + @Test + public void addContainsIndexOfRemove() throws Exception { + init(); + while (!testListSet.isEmpty()) { + // add + int index = random.nextInt(elementsList.size() + 1); + testListSet.add(index, addRandom(index)); + + // contains + assertTrue(testListSet.contains(getRandomExisting())); + assertFalse(testListSet.contains(getRandomNotExisting())); + + // indexOf + index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + assertEquals(value, testListSet.get(index)); + assertEquals(index, testListSet.indexOf(value)); + + // remove + final Long valueToRemove = removeRandomValue(); + assertTrue(testListSet.remove(valueToRemove)); + final int indexToRemove = removeRandomIndex(); + final Long removedValue = testListSet.get(indexToRemove); + assertEquals(removedValue, testListSet.remove(indexToRemove)); + + // check add nulls + try { + testListSet.add(null); + fail("No exception on adding null"); + } catch (NullPointerException e) {} + try { + testListSet.add(random.nextInt(elementsList.size() + 1), null); + fail("No exception on adding null"); + } catch (NullPointerException e) {} + // check not existing indexOf + assertEquals(-1, testListSet.indexOf(random.nextLong())); + assertEquals(-1, testListSet.lastIndexOf(random.nextLong())); + assertFalse(testListSet.remove(random.nextLong())); + + + assertReference(); + } + } + + @Test + public void toArray() { + init(); + + for (int i = 0; i < iterations; i++) { + testListSet.add(addRandom()); + testListSet.remove(removeRandomValue()); + assertReference(); + } + + assertArrayEquals(elementsList.toArray(), testListSet.toArray()); + assertArrayEquals(elementsList.toArray(new Long[0]), testListSet.toArray(new Long[0])); + } + + @Test + public void clear() { + init(); + testListSet.clear(); + elementsList.clear(); + elementsSet.clear(); + assertReference(); + } + + @Test + public void constructorWithColection() { + init(); + testListSet = new IndexedTreeListSet<>(elementsList); + assertReference(); + } + + @Test + public void constructorWithTreeMap() { + init(); + testListSet = new IndexedTreeListSet<>(elementsList, new TreeMap()); + assertReference(); + } + + private void init() { + for (int i = 0; i < iterations; i++) { + int index = random.nextInt(elementsList.size() + 1); + testListSet.add(index, addRandom(index)); // can be optimized + } + assertReference(); + } + + private Long addRandom() { + while (true) { + Long value = random.nextLong(); + if (elementsSet.add(value)) { + elementsList.add(value); + return value; + } + } + } + + private Long addRandom(int index) { + while (true) { + Long value = random.nextLong(); + if (elementsSet.add(value)) { + elementsList.add(index, value); + return value; + } + } + } + + private Long getRandomExisting() { + if (elementsList.isEmpty()) { + return null; + } + return elementsList.get(random.nextInt(elementsList.size())); + } + + private Long getRandomNotExisting() { + while (true) { + Long value = random.nextLong(); + if (!elementsSet.contains(value)) { + return value; + } + } + } + + private Long getRandomRemoved() { + if (removedList.isEmpty()) { + return null; + } + return removedList.get(random.nextInt(removedList.size())); + } + + private Long removeRandomValue() { + if (elementsList.isEmpty()) { + return null; + } + int index = random.nextInt(elementsList.size()); + Long value = elementsList.get(index); + elementsSet.remove(value); + elementsList.remove(index); + removedList.add(value); + return value; + } + + private int removeRandomIndex() { + if (elementsList.isEmpty()) { + return -1; + } + int index = random.nextInt(elementsList.size()); + removeByIndex(index); + return index; + } + + private void removeByIndex(final int index) { + Long value = elementsList.get(index); + elementsSet.remove(value); + elementsList.remove(index); + removedList.add(value); + } + + private void assertReference() { + assertEquals(elementsSet, testListSet); + assertEquals(elementsList, testListSet); + testListSet.assertConsistent(); + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedTreeListSetTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListSetTest.java new file mode 100644 index 0000000000..b72c23cd39 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListSetTest.java @@ -0,0 +1,674 @@ +/* + * 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.list; + +import java.util.*; + +/** + * JUnit tests. + * + * @since 3.0 + */ +public class IndexedTreeListSetTest extends AbstractListTest { + + boolean extraVerify = true; + + public IndexedTreeListSetTest(final String testName) { + super(testName); + } + + //----------------------------------------------------------------------- + @Override + public String getCompatibilityVersion() { + return "4"; + } + + @Override + @SuppressWarnings("unchecked") + public E[] getFullNonNullElements() { + // override to avoid duplicate "One" + return (E[]) new Object[] { + new String(""), + new String("One"), + Integer.valueOf(2), + "Three", + Integer.valueOf(4), + new Double(5), + new Float(6), + "Seven", + "Eight", + new String("Nine"), + Integer.valueOf(10), + new Short((short)11), + new Long(12), + "Thirteen", + "14", + "15", + new Byte((byte)16) + }; + } + + //----------------------------------------------------------------------- + @Override + public List makeObject() { + IndexedTreeListSet list = new IndexedTreeListSet<>(); + list.supportSetInIterator = false; + return list; + } + + @Override + public boolean isNullSupported() { + return false; + } + + @SuppressWarnings("unchecked") + public void testAdd() { + final IndexedTreeListSet lset = new IndexedTreeListSet<>(new ArrayList()); + + // Duplicate element + final E obj = (E) Integer.valueOf(1); + lset.add(obj); + lset.add(obj); + assertEquals("Duplicate element was added.", 1, lset.size()); + + // Unique element + lset.add((E) Integer.valueOf(2)); + assertEquals("Unique element was not added.", 2, lset.size()); + } + + @SuppressWarnings("unchecked") + public void testAddAll() { + final IndexedTreeListSet lset = new IndexedTreeListSet<>(new ArrayList()); + + lset.addAll( + Arrays.asList((E[]) new Integer[] { Integer.valueOf(1), Integer.valueOf(1)})); + + assertEquals("Duplicate element was added.", 1, lset.size()); + } + + @Override + public void testCollectionAddAll() { + // override for set behaviour + resetEmpty(); + E[] elements = getFullElements(); + boolean r = getCollection().addAll(Arrays.asList(elements)); + getConfirmed().addAll(Arrays.asList(elements)); + verify(); + assertTrue("Empty collection should change after addAll", r); + for (final E element : elements) { + assertTrue("Collection should contain added element", + getCollection().contains(element)); + } + + resetFull(); + final int size = getCollection().size(); + elements = getOtherElements(); + r = getCollection().addAll(Arrays.asList(elements)); + getConfirmed().addAll(Arrays.asList(elements)); + verify(); + assertTrue("Full collection should change after addAll", r); + for (int i = 0; i < elements.length; i++) { + assertTrue("Full collection should contain added element " + i, + getCollection().contains(elements[i])); + } + assertEquals("Size should increase after addAll", + size + elements.length, getCollection().size()); + } + + @Override + public void testCollectionIteratorRemove() { + try { + extraVerify = false; + super.testCollectionIteratorRemove(); + } finally { + extraVerify = true; + } + } + public void testCollections304() { + final List list = new LinkedList<>(); + final IndexedTreeListSet decoratedList = new IndexedTreeListSet(list); + final String s1 = "Apple"; + final String s2 = "Lemon"; + final String s3 = "Orange"; + final String s4 = "Strawberry"; + + decoratedList.add(s1); + decoratedList.add(s2); + decoratedList.add(s3); + assertEquals(3, decoratedList.size()); + + decoratedList.set(1, s4); + assertEquals(3, decoratedList.size()); + + decoratedList.add(1, s4); + assertEquals(3, decoratedList.size()); + + decoratedList.add(1, s2); + assertEquals(4, decoratedList.size()); + } + + @SuppressWarnings("unchecked") + public void testCollections307() { + List list = new ArrayList<>(); + List uniqueList = new IndexedTreeListSet(list); + + final String hello = "Hello"; + final String world = "World"; + uniqueList.add((E) hello); + uniqueList.add((E) world); + + List subList = list.subList(0, 0); + List subUniqueList = uniqueList.subList(0, 0); + + assertFalse(subList.contains(world)); // passes + assertFalse(subUniqueList.contains(world)); // fails + + List worldList = new ArrayList<>(); + worldList.add((E) world); + assertFalse(subList.contains("World")); // passes + assertFalse(subUniqueList.contains("World")); // fails + + // repeat the test with a different class than HashSet; + // which means subclassing IndexedTreeListSet below + list = new ArrayList<>(); + uniqueList = new IndexedTreeListSet(list, new java.util.TreeMap()); + + uniqueList.add((E) hello); + uniqueList.add((E) world); + + subList = list.subList(0, 0); + subUniqueList = uniqueList.subList(0, 0); + + assertFalse(subList.contains(world)); // passes + assertFalse(subUniqueList.contains(world)); // fails + + worldList = new ArrayList<>(); + worldList.add((E) world); + assertFalse(subList.contains("World")); // passes + assertFalse(subUniqueList.contains("World")); // fails + } + + public void testCollections701() { + final IndexedTreeListSet uniqueList = new IndexedTreeListSet<>(new ArrayList<>()); + final Integer obj1 = Integer.valueOf(1); + final Integer obj2 = Integer.valueOf(2); + uniqueList.add(obj1); + uniqueList.add(obj2); + assertEquals(2, uniqueList.size()); + uniqueList.add(uniqueList); + assertEquals(3, uniqueList.size()); + final List list = new LinkedList<>(); + final IndexedTreeListSet decoratedList = new IndexedTreeListSet(list); + final String s1 = "Apple"; + final String s2 = "Lemon"; + final String s3 = "Orange"; + final String s4 = "Strawberry"; + decoratedList.add(s1); + decoratedList.add(s2); + decoratedList.add(s3); + assertEquals(3, decoratedList.size()); + decoratedList.set(1, s4); + assertEquals(3, decoratedList.size()); + decoratedList.add(decoratedList); + assertEquals(4, decoratedList.size()); + } + + //----------------------------------------------------------------------- + public void testFactory() { + final Integer[] array = new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(1) }; + final ArrayList list = new ArrayList<>(Arrays.asList(array)); + final IndexedTreeListSet lset = new IndexedTreeListSet(list); + + assertEquals("Duplicate element was added.", 2, lset.size()); + assertEquals(Integer.valueOf(1), lset.get(0)); + assertEquals(Integer.valueOf(2), lset.get(1)); + assertEquals(Integer.valueOf(1), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(1)); + } + + public void testIntCollectionAddAll() { + // make a IndexedTreeListSet with one element + final List list = new IndexedTreeListSet<>(new ArrayList()); + final Integer existingElement = Integer.valueOf(1); + list.add(existingElement); + + // add two new unique elements at index 0 + final Integer firstNewElement = Integer.valueOf(2); + final Integer secondNewElement = Integer.valueOf(3); + Collection collection = Arrays.asList(firstNewElement, secondNewElement); + list.addAll(0, collection); + assertEquals("Unique elements should be added.", 3, list.size()); + assertEquals("First new element should be at index 0", firstNewElement, list.get(0)); + assertEquals("Second new element should be at index 1", secondNewElement, list.get(1)); + assertEquals("Existing element should shift to index 2", existingElement, list.get(2)); + + // add a duplicate element and a unique element at index 0 + final Integer thirdNewElement = Integer.valueOf(4); + collection = Arrays.asList(existingElement, thirdNewElement); + list.addAll(0, collection); + assertEquals("Duplicate element should not be added, unique element should be added.", + 4, list.size()); + assertEquals("Third new element should be at index 0", thirdNewElement, list.get(0)); + } + + @SuppressWarnings("unchecked") + public void testIndexOf() { + final List l = makeObject(); + l.add((E) "0"); + l.add((E) "1"); + l.add((E) "2"); + l.add((E) "3"); + l.add((E) "4"); + l.add((E) "5"); + l.add((E) "6"); + assertEquals(0, l.indexOf("0")); + assertEquals(1, l.indexOf("1")); + assertEquals(2, l.indexOf("2")); + assertEquals(3, l.indexOf("3")); + assertEquals(4, l.indexOf("4")); + assertEquals(5, l.indexOf("5")); + assertEquals(6, l.indexOf("6")); + assertEquals(7, l.size()); + + l.set(2, (E) "0"); // Previous "0" at index 0 was removed + assertEquals(1, l.indexOf("0")); + assertEquals(-1, l.indexOf("2")); + assertEquals(0, l.indexOf("1")); + assertEquals(6, l.size()); + + l.set(2, (E) "7"); + assertEquals(-1, l.indexOf("3")); + assertEquals(2, l.indexOf("7")); + assertEquals(6, l.size()); + + l.set(2, (E) "5"); // Previous "5" at index 4 was removed + assertEquals(2, l.indexOf("5")); + assertEquals(4, l.indexOf("6")); + assertEquals(5, l.size()); + + l.remove(4); + assertEquals(2, l.indexOf("5")); + + l.remove(1); + assertEquals(1, l.indexOf("5")); + } + + @SuppressWarnings("unchecked") + public void testListIterator() { + final IndexedTreeListSet lset = new IndexedTreeListSet<>(new ArrayList()); + + final E obj1 = (E) Integer.valueOf(1); + final E obj2 = (E) Integer.valueOf(2); + lset.add(obj1); + lset.add(obj2); + + // Attempts to add a duplicate object + for (final ListIterator it = lset.listIterator(); it.hasNext();) { + it.next(); + + if (!it.hasNext()) { + it.add(obj1); + break; + } + } + + assertEquals("Duplicate element was added", 2, lset.size()); + } + + @Override + public void testListIteratorAdd() { + // override to cope with Set behaviour + resetEmpty(); + final List list1 = getCollection(); + final List list2 = getConfirmed(); + + final E[] elements = getOtherElements(); // changed here + ListIterator iter1 = list1.listIterator(); + ListIterator iter2 = list2.listIterator(); + + for (final E element : elements) { + iter1.add(element); + iter2.add(element); + super.verify(); // changed here + } + + resetFull(); + iter1 = getCollection().listIterator(); + iter2 = getConfirmed().listIterator(); + for (final E element : elements) { + iter1.next(); + iter2.next(); + iter1.add(element); + iter2.add(element); + super.verify(); // changed here + } + } + + //----------------------------------------------------------------------- + @Override + public void testListIteratorSet() { + // override to block + resetFull(); + final ListIterator it = getCollection().listIterator(); + it.next(); + try { + it.set(null); + fail(); + } catch (final UnsupportedOperationException ex) {} + } + + @Override + @SuppressWarnings("unchecked") + public void testListSetByIndex() { + // override for set behaviour + resetFull(); + final int size = getCollection().size(); + getCollection().set(0, (E) new Long(1000)); + assertEquals(size, getCollection().size()); + + getCollection().set(2, (E) new Long(1000)); + assertEquals(size - 1, getCollection().size()); + assertEquals(new Long(1000), getCollection().get(1)); // set into 2, but shifted down to 1 + } + + @SuppressWarnings("unchecked") + public void testRetainAll() { + final List list = new ArrayList<>(10); + final IndexedTreeListSet uniqueList = new IndexedTreeListSet(list); + for (int i = 0; i < 10; ++i) { + uniqueList.add((E)Integer.valueOf(i)); + } + + final Collection retained = new ArrayList<>(5); + for (int i = 0; i < 5; ++i) { + retained.add((E)Integer.valueOf(i * 2)); + } + + assertTrue(uniqueList.retainAll(retained)); + assertEquals(5, uniqueList.size()); + assertTrue(uniqueList.contains(Integer.valueOf(0))); + assertTrue(uniqueList.contains(Integer.valueOf(2))); + assertTrue(uniqueList.contains(Integer.valueOf(4))); + assertTrue(uniqueList.contains(Integer.valueOf(6))); + assertTrue(uniqueList.contains(Integer.valueOf(8))); + } + + @SuppressWarnings("unchecked") + public void testRetainAllWithInitialList() { + // initialized with empty list + final List list = new ArrayList<>(10); + for (int i = 0; i < 5; ++i) { + list.add((E)Integer.valueOf(i)); + } + final IndexedTreeListSet uniqueList = new IndexedTreeListSet(list); + for (int i = 5; i < 10; ++i) { + uniqueList.add((E)Integer.valueOf(i)); + } + + final Collection retained = new ArrayList<>(5); + for (int i = 0; i < 5; ++i) { + retained.add((E)Integer.valueOf(i * 2)); + } + + assertTrue(uniqueList.retainAll(retained)); + assertEquals(5, uniqueList.size()); + assertTrue(uniqueList.contains(Integer.valueOf(0))); + assertTrue(uniqueList.contains(Integer.valueOf(2))); + assertTrue(uniqueList.contains(Integer.valueOf(4))); + assertTrue(uniqueList.contains(Integer.valueOf(6))); + assertTrue(uniqueList.contains(Integer.valueOf(8))); + } + + @SuppressWarnings("unchecked") + public void testSet() { + final IndexedTreeListSet lset = new IndexedTreeListSet<>(new ArrayList()); + + // Duplicate element + final E obj1 = (E) Integer.valueOf(1); + final E obj2 = (E) Integer.valueOf(2); + final E obj3 = (E) Integer.valueOf(3); + + lset.add(obj1); + lset.add(obj2); + lset.set(0, obj1); + assertEquals(2, lset.size()); + assertSame(obj1, lset.get(0)); + assertSame(obj2, lset.get(1)); + + lset.clear(); + lset.add(obj1); + lset.add(obj2); + lset.set(0, obj2); + assertEquals(1, lset.size()); + assertSame(obj2, lset.get(0)); + + lset.clear(); + lset.add(obj1); + lset.add(obj2); + lset.set(0, obj3); + assertEquals(2, lset.size()); + assertSame(obj3, lset.get(0)); + assertSame(obj2, lset.get(1)); + + lset.clear(); + lset.add(obj1); + lset.add(obj2); + lset.set(1, obj1); + assertEquals(1, lset.size()); + assertSame(obj1, lset.get(0)); + } + + public void testSetCollections444() { + final IndexedTreeListSet lset = new IndexedTreeListSet<>(new ArrayList()); + + // Duplicate element + final Integer obj1 = Integer.valueOf(1); + final Integer obj2 = Integer.valueOf(2); + + lset.add(obj1); + lset.add(obj2); + lset.set(0, obj1); + assertEquals(2, lset.size()); + assertSame(obj1, lset.get(0)); + assertSame(obj2, lset.get(1)); + + assertTrue(lset.contains(obj1)); + assertTrue(lset.contains(obj2)); + } + + public void testSubListIsUnmodifiable() { + resetFull(); + final List subList = getCollection().subList(1, 3); + try { + subList.remove(0); + fail("subList should be unmodifiable"); + } catch (final UnsupportedOperationException e) { + // expected + } + } + + + public void testBug35258() { + final Object objectToRemove = Integer.valueOf(3); + + final List TreeListSet = new IndexedTreeListSet<>(); + TreeListSet.add(Integer.valueOf(0)); + TreeListSet.add(Integer.valueOf(1)); + TreeListSet.add(Integer.valueOf(2)); + TreeListSet.add(Integer.valueOf(3)); + TreeListSet.add(Integer.valueOf(4)); + + // this cause inconsistence of ListIterator() + TreeListSet.remove(objectToRemove); + + final ListIterator li = TreeListSet.listIterator(); + assertEquals(Integer.valueOf(0), li.next()); + assertEquals(Integer.valueOf(0), li.previous()); + assertEquals(Integer.valueOf(0), li.next()); + assertEquals(Integer.valueOf(1), li.next()); + // this caused error in bug 35258 + assertEquals(Integer.valueOf(1), li.previous()); + assertEquals(Integer.valueOf(1), li.next()); + assertEquals(Integer.valueOf(2), li.next()); + assertEquals(Integer.valueOf(2), li.previous()); + assertEquals(Integer.valueOf(2), li.next()); + assertEquals(Integer.valueOf(4), li.next()); + assertEquals(Integer.valueOf(4), li.previous()); + assertEquals(Integer.valueOf(4), li.next()); + assertEquals(false, li.hasNext()); + } + + public void testBugCollections447() { + final List TreeListSet = new IndexedTreeListSet<>(); + TreeListSet.add("A"); + TreeListSet.add("B"); + TreeListSet.add("C"); + TreeListSet.add("D"); + + final ListIterator li = TreeListSet.listIterator(); + assertEquals("A", li.next()); + assertEquals("B", li.next()); + + assertEquals("B", li.previous()); + + li.remove(); // Deletes "B" + + // previous() after remove() should move to + // the element before the one just removed + assertEquals("A", li.previous()); + } + + @SuppressWarnings("boxing") // OK in test code + public void testIterationOrder() { + // COLLECTIONS-433: + // ensure that the iteration order of elements is correct + // when initializing the TreeListSet with another collection + + for (int size = 1; size < 1000; size++) { + final List other = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + other.add(i); + } + final IndexedTreeListSet l = new IndexedTreeListSet<>(other); + final ListIterator it = l.listIterator(); + int i = 0; + while (it.hasNext()) { + final Integer val = it.next(); + assertEquals(i++, val.intValue()); + } + + while (it.hasPrevious()) { + final Integer val = it.previous(); + assertEquals(--i, val.intValue()); + } + } + } + + @SuppressWarnings("boxing") // OK in test code + public void testIterationOrderAfterAddAll() { + // COLLECTIONS-433: + // ensure that the iteration order of elements is correct + // when calling addAll on the TreeListSet + + // to simulate different cases in addAll, do different runs where + // the number of elements already in the list and being added by addAll differ + + final int size = 1000; + for (int i = 0; i < 100; i++) { + final List other = new ArrayList<>(size); + for (int j = i; j < size; j++) { + other.add(j); + } + final IndexedTreeListSet l = new IndexedTreeListSet<>(); + for (int j = 0; j < i; j++) { + l.add(j); + } + + l.addAll(other); + + final ListIterator it = l.listIterator(); + int cnt = 0; + while (it.hasNext()) { + final Integer val = it.next(); + assertEquals(cnt++, val.intValue()); + } + + while (it.hasPrevious()) { + final Integer val = it.previous(); + assertEquals(--cnt, val.intValue()); + } + } + } + + @SuppressWarnings("unchecked") + public void testUniqueListDoubleInsert() { + final List l = new IndexedTreeListSet(new LinkedList()); + l.add((E) new Object()); + l.add((E) new Object()); + + // duplicate is removed + l.set(0, l.get(1)); + assertEquals(1, l.size()); + + // duplicate should be removed again + l.add(1, l.get(0)); + assertEquals(1, l.size()); + } + + @SuppressWarnings("unchecked") + public void testUniqueListReInsert() { + final List l = new IndexedTreeListSet(new LinkedList()); + l.add((E) new Object()); + l.add((E) new Object()); + + final E a = l.get(0); + + // duplicate is removed + l.set(0, l.get(1)); + assertEquals(1, l.size()); + + // old object is added back in + l.add(1, a); + assertEquals(2, l.size()); + } + + @Override + @SuppressWarnings("unchecked") + public void verify() { + super.verify(); + + if (extraVerify) { + final int size = getCollection().size(); + getCollection().add((E) new Long(1000)); + assertEquals(size + 1, getCollection().size()); + + getCollection().add((E) new Long(1000)); + assertEquals(size + 1, getCollection().size()); + assertEquals(new Long(1000), getCollection().get(size)); + + getCollection().remove(size); + } + } + +// public void testCreate() throws Exception { +// resetEmpty(); +// writeExternalFormToDisk((java.io.Serializable) getCollection(), "src/test/resources/data/test/IndexedTreeListSet.emptyCollection.version4.obj"); +// resetFull(); +// writeExternalFormToDisk((java.io.Serializable) getCollection(), "src/test/resources/data/test/IndexedTreeListSet.fullCollection.version4.obj"); +// } + +} diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedTreeListTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListTest.java new file mode 100644 index 0000000000..4a622e2e13 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedTreeListTest.java @@ -0,0 +1,328 @@ +/* + * 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.list; + +import junit.framework.Test; +import org.apache.commons.collections4.BulkTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +/** + * JUnit tests + * + * @since 3.1 + */ +public class IndexedTreeListTest extends AbstractListTest { + + public IndexedTreeListTest(final String name) { + super(name); + } + +// public static void main(String[] args) { +// junit.textui.TestRunner.run(suite()); +// System.out.println(" add; toArray; iterator; insert; get; indexOf; remove"); +// System.out.print(" IndexedTreeList = "); +// benchmark(new IndexedTreeList()); +// System.out.print("\n ArrayList = "); +// benchmark(new java.util.ArrayList()); +// System.out.print("\n LinkedList = "); +// benchmark(new java.util.LinkedList()); +// System.out.print("\n NodeCachingLinkedList = "); +// benchmark(new NodeCachingLinkedList()); +// } + + public static Test suite() { + return BulkTest.makeSuite(IndexedTreeListTest.class); + } + + public static void benchmark(final List l) { + long start = System.currentTimeMillis(); + for (int i = 0; i < 100000; i++) { + l.add(Integer.valueOf(i)); + } + System.out.print(System.currentTimeMillis() - start + ";"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 200; i++) { + l.toArray(); + } + System.out.print(System.currentTimeMillis() - start + ";"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 100; i++) { + final java.util.Iterator it = l.iterator(); + while (it.hasNext()) { + it.next(); + } + } + System.out.print(System.currentTimeMillis() - start + ";"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + final int j = (int) (Math.random() * 100000); + l.add(j, Integer.valueOf(-j)); + } + System.out.print(System.currentTimeMillis() - start + ";"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 50000; i++) { + final int j = (int) (Math.random() * 110000); + l.get(j); + } + System.out.print(System.currentTimeMillis() - start + ";"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 200; i++) { + final int j = (int) (Math.random() * 100000); + l.indexOf(Integer.valueOf(j)); + } + System.out.print(System.currentTimeMillis() - start + ";"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + final int j = (int) (Math.random() * 100000); + l.remove(j); + } + System.out.print(System.currentTimeMillis() - start + ";"); + } + + //----------------------------------------------------------------------- + @Override + public IndexedTreeList makeObject() { + return new IndexedTreeList<>(); + } + + @Override + public boolean isNullSupported() { + return false; + } + + //----------------------------------------------------------------------- + @SuppressWarnings("unchecked") + public void testAddMultiple() { + final List l = makeObject(); + l.add((E) "hugo"); + l.add((E) "erna"); + l.add((E) "daniel"); + l.add((E) "andres"); + l.add(0, (E) "harald"); + assertEquals("harald", l.get(0)); + assertEquals("hugo", l.get(1)); + assertEquals("erna", l.get(2)); + assertEquals("daniel", l.get(3)); + assertEquals("andres", l.get(4)); + } + + @SuppressWarnings("unchecked") + public void testRemove() { + final List l = makeObject(); + l.add((E) "hugo"); + l.add((E) "erna"); + l.add((E) "daniel"); + l.add((E) "andres"); + l.add(0, (E) "harald"); + int i = 0; + assertEquals("harald", l.get(i++)); + assertEquals("hugo", l.get(i++)); + assertEquals("erna", l.get(i++)); + assertEquals("daniel", l.get(i++)); + assertEquals("andres", l.get(i++)); + + l.remove(0); + i = 0; + assertEquals("hugo", l.get(i++)); + assertEquals("erna", l.get(i++)); + assertEquals("daniel", l.get(i++)); + assertEquals("andres", l.get(i++)); + + i = 0; + l.remove(1); + assertEquals("hugo", l.get(i++)); + assertEquals("daniel", l.get(i++)); + assertEquals("andres", l.get(i++)); + + i = 0; + l.remove(2); + assertEquals("hugo", l.get(i++)); + assertEquals("daniel", l.get(i++)); + } + + @SuppressWarnings("unchecked") + public void testInsertBefore() { + final List l = makeObject(); + l.add((E) "erna"); + l.add(0, (E) "hugo"); + assertEquals("hugo", l.get(0)); + assertEquals("erna", l.get(1)); + } + + @SuppressWarnings("unchecked") + public void testIndexOf() { + final List l = makeObject(); + l.add((E) "0"); + l.add((E) "1"); + l.add((E) "2"); + l.add((E) "3"); + l.add((E) "4"); + l.add((E) "5"); + l.add((E) "6"); + assertEquals(0, l.indexOf("0")); + assertEquals(1, l.indexOf("1")); + assertEquals(2, l.indexOf("2")); + assertEquals(3, l.indexOf("3")); + assertEquals(4, l.indexOf("4")); + assertEquals(5, l.indexOf("5")); + assertEquals(6, l.indexOf("6")); + + l.set(1, (E) "0"); + assertEquals(0, l.indexOf("0")); + + l.set(3, (E) "3"); + assertEquals(3, l.indexOf("3")); + l.set(2, (E) "3"); + assertEquals(2, l.indexOf("3")); + l.set(1, (E) "3"); + assertEquals(1, l.indexOf("3")); + l.set(0, (E) "3"); + assertEquals(0, l.indexOf("3")); + } + +// public void testCheck() { +// List l = makeEmptyList(); +// l.add("A1"); +// l.add("A2"); +// l.add("A3"); +// l.add("A4"); +// l.add("A5"); +// l.add("A6"); +// } + + public void testBug35258() { + final Object objectToRemove = Integer.valueOf(3); + + final List IndexedTreeList = new IndexedTreeList<>(); + IndexedTreeList.add(Integer.valueOf(0)); + IndexedTreeList.add(Integer.valueOf(1)); + IndexedTreeList.add(Integer.valueOf(2)); + IndexedTreeList.add(Integer.valueOf(3)); + IndexedTreeList.add(Integer.valueOf(4)); + + // this cause inconsistence of ListIterator() + IndexedTreeList.remove(objectToRemove); + + final ListIterator li = IndexedTreeList.listIterator(); + assertEquals(Integer.valueOf(0), li.next()); + assertEquals(Integer.valueOf(0), li.previous()); + assertEquals(Integer.valueOf(0), li.next()); + assertEquals(Integer.valueOf(1), li.next()); + // this caused error in bug 35258 + assertEquals(Integer.valueOf(1), li.previous()); + assertEquals(Integer.valueOf(1), li.next()); + assertEquals(Integer.valueOf(2), li.next()); + assertEquals(Integer.valueOf(2), li.previous()); + assertEquals(Integer.valueOf(2), li.next()); + assertEquals(Integer.valueOf(4), li.next()); + assertEquals(Integer.valueOf(4), li.previous()); + assertEquals(Integer.valueOf(4), li.next()); + assertEquals(false, li.hasNext()); + } + + public void testBugCollections447() { + final List IndexedTreeList = new IndexedTreeList<>(); + IndexedTreeList.add("A"); + IndexedTreeList.add("B"); + IndexedTreeList.add("C"); + IndexedTreeList.add("D"); + + final ListIterator li = IndexedTreeList.listIterator(); + assertEquals("A", li.next()); + assertEquals("B", li.next()); + + assertEquals("B", li.previous()); + + li.remove(); // Deletes "B" + + // previous() after remove() should move to + // the element before the one just removed + assertEquals("A", li.previous()); + } + + @SuppressWarnings("boxing") // OK in test code + public void testIterationOrder() { + // COLLECTIONS-433: + // ensure that the iteration order of elements is correct + // when initializing the IndexedTreeList with another collection + + for (int size = 1; size < 1000; size++) { + final List other = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + other.add(i); + } + final IndexedTreeList l = new IndexedTreeList<>(other); + final ListIterator it = l.listIterator(); + int i = 0; + while (it.hasNext()) { + final Integer val = it.next(); + assertEquals(i++, val.intValue()); + } + + while (it.hasPrevious()) { + final Integer val = it.previous(); + assertEquals(--i, val.intValue()); + } + } + } + + @SuppressWarnings("boxing") // OK in test code + public void testIterationOrderAfterAddAll() { + // COLLECTIONS-433: + // ensure that the iteration order of elements is correct + // when calling addAll on the IndexedTreeList + + // to simulate different cases in addAll, do different runs where + // the number of elements already in the list and being added by addAll differ + + final int size = 1000; + for (int i = 0; i < 100; i++) { + final List other = new ArrayList<>(size); + for (int j = i; j < size; j++) { + other.add(j); + } + final IndexedTreeList l = new IndexedTreeList<>(); + for (int j = 0; j < i; j++) { + l.add(j); + } + + l.addAll(other); + + final ListIterator it = l.listIterator(); + int cnt = 0; + while (it.hasNext()) { + final Integer val = it.next(); + assertEquals(cnt++, val.intValue()); + } + + while (it.hasPrevious()) { + final Integer val = it.previous(); + assertEquals(--cnt, val.intValue()); + } + } + } + +}