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 extends E> 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 extends E> 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 extends E> 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 extends E> 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 extends E> 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