diff --git a/src/main/java/org/apache/commons/collections4/list/IndexedLinkedList.java b/src/main/java/org/apache/commons/collections4/list/IndexedLinkedList.java new file mode 100644 index 0000000000..60e84e4157 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/IndexedLinkedList.java @@ -0,0 +1,3458 @@ +/* + * 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.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + *

+ * This class implements the indexed, heuristic doubly-linked list data + * structure that runs all the single-element operations in expected + * \(\mathcal{O}(\sqrt{n})\) time. Under the hood, the actual elements are + * stored in a doubly-linked list. However, we also maintain a list of so-called + * "fingers" stored in a random access array. Each finger {@code F} + * contains two data fields: + *

+ * + *

+ * + * For the list of size \(n\), we maintain + * \(\bigg \lceil \sqrt{n} \bigg \rceil + 1\) fingers. The rightmost finger in + * the finger list is a special end-of-list sentinel. It always has + * {@code F.node = null} and {@code F.index = } \(n\). The fingers are sorted by + * their indices. That arrangement allows simpler and faster code in the method + * that accesses a finger via element index; see + * {@link FingerList#getFingerIndexImpl(int)}. Since number of fingers is + * \(\sqrt{n}\), and assuming that the fingers are evenly distributed, each + * finger "covers" \(n / \sqrt{n} = \sqrt{n}\) elements. In order to access an + * element in the actual list, we first consult the finger list for the index + * {@code i} of the finger {@code fingerArray[i]} that is closest to the index + * of the target element. This runs in + * + * \[ + * \mathcal{O}(\log \sqrt{n}) = \mathcal{O}(\log n^{1/2}) = \mathcal{O}(\frac{1}{2} \log n) = \mathcal{O}(\log n). + * \] + * + * The rest is to "rewind" the closest finger to point to the target + * element (which requires \(\mathcal{O}(\sqrt{n})\) on evenly distributed + * finger lists). + */ +public class IndexedLinkedList implements Deque, + List, + Cloneable, + java.io.Serializable { + /** + * This inner class implements the finger list data structure for managing + * list fingers. + * + * @param the list node item type. + */ + class FingerList { + + // This is also the minimum capacity. + private static final int INITIAL_CAPACITY = 8; + + // The actual list storage array: + Finger[] fingerArray = new Finger[INITIAL_CAPACITY]; + + // The number of fingers stored in the list. This field does not count + // the end-of-list sentinel finger 'F' for which 'F.index = size'. + private int size; + + // Constructs the empty finger list consisting only of end-of-list + // sentinel finger. + private FingerList() { + fingerArray[0] = new Finger<>(null, 0); + } + + @Override + public String toString() { + return "[FingerList, size = " + size + "]"; + } + + // Appends the input finger to the tail of the finger list: + void appendFinger(Finger finger) { + size++; + enlargeFingerArrayIfNeeded(size + 1); + fingerArray[size] = fingerArray[size - 1]; + fingerArray[size - 1] = finger; + fingerArray[size].index = IndexedLinkedList.this.size; + } + + // Not 'private' since is used in the unit tests. + void clear() { + Arrays.fill(fingerArray, 0, size, null); + fingerArray = new Finger[INITIAL_CAPACITY]; + fingerArray[0] = new Finger<>(null, 0); + size = 0; + } + + Finger get(int index) { + return fingerArray[index]; + } + + // Returns the index of the finger that is closest to the + // 'elementIndex'th list element. + int getFingerIndex(int elementIndex) { + return normalize(getFingerIndexImpl(elementIndex), elementIndex); + } + + int size() { + return size; + } + + private void adjustOnRemoveFirst() { + int lastPrefixIndex = Integer.MAX_VALUE; + + for (int i = 0; i < size; ++i) { + Finger finger = fingerArray[i]; + + if (finger.index != i) { + lastPrefixIndex = i; + break; + } else { + finger.node = finger.node.next; + } + } + + shiftFingerIndicesToLeftOnceUntil(lastPrefixIndex, size - 1); + } + + // We can save some space while keeping the finger array operations + // amortized O(1). The 'nextSize' defines the requested finger array + // size not counting the end-of-finger-list sentinel finger: + private void contractFingerArrayIfNeeded(int nextSize) { + // Can we contract at least once? + if ((nextSize + 1) * 4 < fingerArray.length + && fingerArray.length > 2 * INITIAL_CAPACITY) { + int nextCapacity = fingerArray.length / 4; + + // Good, we can. But can we keep on splitting in half the + // capacity any further? + while (nextCapacity >= 2 * (nextSize + 1) + && nextCapacity > INITIAL_CAPACITY) { + // Yes, we can do it as well. + nextCapacity /= 2; + } + + fingerArray = Arrays.copyOf(fingerArray, nextCapacity); + } + } + + // Makes sure that the next finger fits in this finger stack: + private void enlargeFingerArrayIfNeeded(int requestedSize) { + // If the finger array is full, double the capacity: + if (requestedSize > fingerArray.length) { + int nextCapacity = 2 * fingerArray.length; + + while (nextCapacity < size + 1) { + // If 'requestedSize' is too large, we may need to keep on + // doubling the next capacity until it's large enought to + // accommodate 'requestedSiz + nextCapacity *= 2; + } + + fingerArray = Arrays.copyOf(fingerArray, nextCapacity); + } + } + + // Returns the finger index 'i', such that 'fingerArray[i].index' is no + // less than 'i', and is closest to 'i'. This algorithm is translated + // from https://en.cppreference.com/w/cpp/algorithm/lower_bound + private int getFingerIndexImpl(int elementIndex) { + int count = size + 1; // + 1 for the end sentinel. + int it; + int idx = 0; + + while (count > 0) { + it = idx; + int step = count / 2; + it += step; + + if (fingerArray[it].index < elementIndex) { + idx = ++it; + count -= step + 1; + } else { + count = step; + } + } + + return idx; + } + + // Inserts the input finger into the finger list such that the entire + // finger list is sorted by indices: + private void insertFingerAndShiftOnceToRight(Finger finger) { + enlargeFingerArrayIfNeeded(size + 2); + int beforeFingerIndex = getFingerIndex(finger.index); + System.arraycopy( + fingerArray, + beforeFingerIndex, + fingerArray, + beforeFingerIndex + 1, + size + 1 - beforeFingerIndex); + + // Shift fingerArray[beforeFingerIndex ... size] one position to the + // right (towards larger index values: + shiftFingerIndicesToRightOnce(beforeFingerIndex + 1); + + fingerArray[beforeFingerIndex] = finger; + fingerArray[++size].index = IndexedLinkedList.this.size; + } + + // Make sure we can insert 'roomSize' fingers starting from + // 'fingerIndex', shifting all the fingers starting from 'fingerIndex' + // 'numberOfNodes' to the right: + private void makeRoomAtIndex(int fingerIndex, int roomSize, int numberOfNodes) { + shiftFingerIndicesToRight(fingerIndex, numberOfNodes); + size += roomSize; + enlargeFingerArrayIfNeeded(size + 1); // +1 for the end of list + // sentinel. + System.arraycopy(fingerArray, + fingerIndex, + fingerArray, + fingerIndex + roomSize, + size - roomSize - fingerIndex + 1); + } + + // Moves 'numberOfFingers' fingers to the prefix ending in 'fromIndex': + private void moveFingersToPrefix(int fromIndex, int numberOfFingers) { + if (numberOfFingers == 0) { + // Here, nothing to move: + return; + } + + int fromFingerIndex = getFingerIndex(fromIndex); + + if (fromFingerIndex == 0) { + // Here, the prefix is empty: + moveFingersToPrefixOnEmptyPrefix( + fromIndex, + numberOfFingers); + + return; + } + + int i; + int targetIndex = -1; + Finger targetFinger = null; + + // Find the rightmost finger index after which we can put + // 'numberOfFingers' fingers: + for (i = fromFingerIndex - 1; i >= 0; --i) { + Finger finger = fingerArray[i]; + + if (finger.index + numberOfFingers - 1 + i < fromIndex) { + targetFinger = finger; + targetIndex = i; + break; + } + } + + // Pack the rest of the prefix fingers: + for (int j = targetIndex + 1; j < numberOfFingers; ++j) { + Finger predecessorFinger = fingerArray[j - 1]; + Finger currentFinger = fingerArray[j]; + currentFinger.index = predecessorFinger.index + 1; + currentFinger.node = predecessorFinger.node.next; + } + } + + // Move 'numberOfFingers' fingers to the empty prefix: + private void moveFingersToPrefixOnEmptyPrefix(int fromIndex, + int numberOfFingers) { + Finger firstFinger = fingerArray[0]; + int toMove = firstFinger.index - fromIndex + numberOfFingers; + + for (int i = 0; i < toMove; ++i) { + firstFinger.node = firstFinger.node.prev; + } + + firstFinger.index -= toMove; + + for (int i = 1; i < numberOfFingers; ++i) { + Finger previousFinger = fingerArray[i - 1]; + Finger currentFinger = fingerArray[i]; + currentFinger.node = previousFinger.node.next; + currentFinger.index = previousFinger.index + 1; + } + } + + // Moves 'numberOfFingers' fingers to the suffix starting in + // 'toIndex': + private void moveFingersToSuffix(int toIndex, int numberOfFingers) { + if (numberOfFingers == 0) { + // Here, nothing to move: + return; + } + + int toFingerIndex = getFingerIndexImpl(toIndex); + + if (toFingerIndex == fingerList.size) { + // Here, the suffix is empty: + moveFingersToSuffixOnEmptySuffix(toIndex, numberOfFingers); + return; + } + + int i; + Finger targetFinger = null; + + // Find the leftmost finger index before which we can put + // 'numberOfFingers' fingers: + for (i = toFingerIndex; i < size; ++i) { + Finger finger = fingerArray[i]; + + if (finger.index - numberOfFingers + 1 >= toIndex) { + targetFinger = finger; + break; + } + } + + if (targetFinger == null) { + // Here, all the 'numberOfFingers' do not fit. Make some room: + Finger f = fingerArray[size - 1]; + int toMove = toIndex + numberOfFingers - 1 - f.index; + + for (int j = 0; j < toMove; ++j) { + f.node = f.node.next; + } + + f.index += toMove; + i = size - 1; + } + + int stopIndex = numberOfFingers - (size - i); + + // Pack the rest of the suffix fingers: + for (int j = i - 1, k = 0; k < stopIndex; ++k, --j) { + Finger predecessorFinger = fingerArray[j]; + Finger currentFinger = fingerArray[j + 1]; + predecessorFinger.index = currentFinger.index - 1; + predecessorFinger.node = currentFinger.node.prev; + } + } + + // Move 'numberOfFingers' fingers to the empty suffix: + private void moveFingersToSuffixOnEmptySuffix(int toIndex, + int numberOfFingers) { + int toMove = toIndex + - fingerArray[size - 1].index + + numberOfFingers - 1; + + Finger finger = fingerArray[size - 1]; + + for (int i = 0; i < toMove; ++i) { + finger.node = finger.node.next; + } + + finger.index += toMove; + + for (int i = 1; i < numberOfFingers; ++i) { + Finger predecessorFinger = fingerArray[size - i - 1]; + Finger currentFinger = fingerArray[size - i]; + predecessorFinger.index = currentFinger.index - 1; + predecessorFinger.node = currentFinger.node.prev; + } + } + + // Returns the 'i'th node of this linked list. The closest finger is + // updated to point to the returned node: + private Node node(int index) { + Finger finger = fingerArray[getFingerIndex(index)]; + int steps = finger.index - index; + + if (steps > 0) { + finger.rewindLeft(steps); + } else { + finger.rewindRight(-steps); + } + + return finger.node; + } + + // Makes sure that the returned finger index 'i' points to the closest + // finger in the finger array: + private int normalize(int fingerIndex, int elementIndex) { + if (fingerIndex == 0) { + // Since we cannot point to '-1'th finger, return 0: + return 0; + } + + if (fingerIndex == size) { + // Don't go outside of 'size - 1*: + return size - 1; + } + + Finger finger1 = fingerArray[fingerIndex - 1]; + Finger finger2 = fingerArray[fingerIndex]; + + int distance1 = elementIndex - finger1.index; + int distance2 = finger2.index - elementIndex; + + // Return the closest finger index: + return distance1 < distance2 ? fingerIndex - 1 : fingerIndex; + } + + // Removes the last finger residing right before the end-of-finger-list + // sentinel finger: + private void removeFinger() { + contractFingerArrayIfNeeded(--size); + fingerArray[size] = fingerArray[size + 1]; + fingerArray[size + 1] = null; + fingerArray[size].index = IndexedLinkedList.this.size; + } + + // Removes the finger range [startFingerIndex, endFingerIndex). + private void removeRange(int prefixSize, + int suffixSize, + int nodesToRemove) { + int fingersToRemove = size - suffixSize - prefixSize; + + shiftFingerIndicesToLeft(size - suffixSize, nodesToRemove); + + System.arraycopy(fingerArray, + size - suffixSize, + fingerArray, + prefixSize, + suffixSize + 1); + + size -= fingersToRemove; + contractFingerArrayIfNeeded(size); + + Arrays.fill(fingerArray, + size + 1, + Math.min(fingerArray.length, + size + 1 + fingersToRemove), + null); + } + + private void setFinger(int index, Finger finger) { + fingerArray[index] = finger; + } + + // Moves all the fingers in range [startFingerIndex, size] + // 'shiftLength' positions to the left (towards smaller indices): + private void shiftFingerIndicesToLeft(int startFingerIndex, + int shiftLength) { + for (int i = startFingerIndex; i <= size; ++i) { + fingerArray[i].index -= shiftLength; + } + } + + // Moves all the fingers in range [startFingerIndex, endFingerIndex] one + // position to the left (towards smaller indices): + private void shiftFingerIndicesToLeftOnceUntil(int startFingerIndex, + int endFingerIndex) { + for (int i = startFingerIndex; i <= endFingerIndex; ++i) { + fingerArray[i].index--; + } + } + + // Moves all the fingers in range [startFingerIndex, size] + // 'shiftLength' positions to the right (towards larger indices): + private void shiftFingerIndicesToRight(int startIndex, + int shiftLength) { + for (int i = startIndex; i <= size; ++i) { + fingerArray[i].index += shiftLength; + } + } + + // Moves all the fingers in range [startFingerIndex, size] one + // position to the right (towards larger indices): + private void shiftFingerIndicesToRightOnce(int startIndex) { + shiftFingerIndicesToRight(startIndex, 1); + } + } + + static final class Node { + + E item; + Node prev; + Node next; + + Node(E item) { + this.item = item; + } + + @Override + public String toString() { + return "[Node; item = " + item + "]"; + } + } + + static final class Finger { + + Node node; + int index; // Index at which 'node' is located. + int updateIndex; + + Finger(Node node, int index) { + this.node = node; + this.index = index; + } + + @Override + public String toString() { + return "[Finger; index = " + index + + ", item = " + ((node == null) ? "null" : node.item) + + "]"; + } + + // Moves this finger 'steps' position to the left + void rewindLeft(int steps) { + for (int i = 0; i < steps; i++) { + node = node.prev; + } + + index -= steps; + } + + // Moves this finger 'steps' position to the right + void rewindRight(int steps) { + for (int i = 0; i < steps; i++) { + node = node.next; + } + + index += steps; + } + } + + @java.io.Serial + private static final long serialVersionUID = 54170828611556733L; + + /** + * The cached number of elements in this list. + */ + private int size; + + /** + * The modification counter. Used to detect state changes. + */ + private transient int modCount; + transient Node first; + transient Node last; + + // Without 'private' since it is accessed in unit tests. + transient FingerList fingerList = new FingerList<>(); + + /** + * Constructs an empty list. + */ + public IndexedLinkedList() { + + } + + /** + * Constructs a new list and copies the data in {@code c} to it. Runs in + * \(\mathcal{O}(m + \sqrt{m})\) time, where \(m = |c|\). + * + * @param c the collection to copy. + */ + public IndexedLinkedList(Collection c) { + this(); + addAll(c); + } + + /** + * Appends the specified element to the end of this list. Runs in amortized + * constant time. + * + *

This method is equivalent to {@link #addLast}. + * + * @param e element to be appended to this list. + * @return {@code true} (as specified by {@link Collection#add}). + */ + @Override + public boolean add(E e) { + linkLast(e); + return true; + } + + /** + * Inserts the specified element at the specified position in this list. + * The affected finger indices will be incremented by one. A finger + * {@code F} is affected, if {@code F.index >= index}. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param index index at which the specified element is to be inserted. + * @param element element to be inserted. + * @throws IndexOutOfBoundsException {@inheritDoc} + */ + @Override + public void add(int index, E element) { + checkPositionIndex(index); + + if (index == size) { + linkLast(element); + } else { + linkBefore(element, node(index), index); + } + } + + /** + * Appends all of the elements in the specified collection to the end of + * this list, in the order they are returned by the specified collection's + * iterator. The behavior of this operation is undefined if the specified + * collection is modified while the operation is in progress. (Note that + * this will occur if the specified collection is this list, and it's + * nonempty.) Runs in \(\mathcal{O}(m + \sqrt{m + n})\), where \(m = |c|\) + * and \(n\) is the size of this list. + * + * @param c collection containing elements to be added to this list. + * @return {@code true} if this list changed as a result of the call. + * @throws NullPointerException if the specified collection is null. + */ + @Override + public boolean addAll(Collection c) { + return addAll(size, c); + } + + /** + * Inserts all of the elements in the specified collection into this list, + * starting at the specified position. For each finger {@code F} with + * {@code F.index >= index} will increment {@code F.index} by 1. Runs in + * \(\mathcal{O}(m + \sqrt{m + n})\), where \(m = |c|\) and \(n\) is the + * size of this list. + * + * @param index index at which to insert the first element from the + * specified collection. + * @param c collection containing elements to be added to this list. + * @return {@code true} if this list changed as a result of the call. + * @throws IndexOutOfBoundsException {@inheritDoc} + * @throws NullPointerException if the specified collection is null + */ + @Override + public boolean addAll(int index, Collection c) { + checkPositionIndex(index); + + if (c.isEmpty()) { + return false; + } + + if (size == 0) { + setAll(c); + } else if (index == 0) { + prependAll(c); + } else if (index == size) { + appendAll(c); + } else { + insertAll(c, node(index), index); + } + + return true; + } + + /** + * Adds the element {@code e} before the head of this list. Runs in Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param e the element to add. + */ + @Override + public void addFirst(E e) { + linkFirst(e); + } + + /** + * Adds the element {@code e} after the tail of this list. Runs in constant + * time. + * + * @param e the element to add. + */ + public void addLast(E e) { + linkLast(e); + } + + /** + * Checks the data structure invariant. Throws + * {@link java.lang.IllegalStateException} on invalid invariant. The + * invariant is valid if: + *

    + *
  1. All the fingers in the finger list are sorted by indices.
  2. + *
  3. There is no duplicate indices.
  4. + *
  5. The index of the leftmost finger is no less than zero.
  6. + *
  7. There must be an end-of-list sentinel finger {@code F}, such that + * {@code F.index = } size of linked list and {@code F.node} is + * {@code null}. + *
  8. + *
  9. Each finger {@code F} points to the {@code i}th linked list node, + * where {@code i = F.index}.
  10. + *
+ * Runs always in linear time. + */ + public void checkInvarant() { + for (int i = 0; i < fingerList.size() - 1; ++i) { + Finger left = fingerList.get(i); + Finger right = fingerList.get(i + 1); + + if (left.index >= right.index) { + throw new IllegalStateException( + "FingerList failed: fingerList[" + + i + + "].index = " + + left.index + + " >= " + + right.index + + " = fingerList[" + + (i + 1) + + "]"); + } + } + + Finger sentinelFinger = fingerList.get(fingerList.size()); + + if (sentinelFinger.index != this.size || sentinelFinger.node != null) { + throw new IllegalStateException( + "Invalid end-of-list sentinel: " + sentinelFinger); + } + + Finger finger = fingerList.get(0); + Node node = first; + int fingerCount = 0; + int tentativeSize = 0; + + while (node != null) { + tentativeSize++; + + if (finger.node == node) { + finger = fingerList.get(++fingerCount); + } + + node = node.next; + } + + if (size != tentativeSize) { + throw new IllegalStateException( + "Number of nodes mismatch: size = " + + size + + ", tentativeSize = " + + tentativeSize); + } + + if (fingerList.size() != fingerCount) { + throw new IllegalStateException( + "Number of fingers mismatch: fingerList.size() = " + + fingerList.size() + + ", fingerCount = " + + fingerCount); + } + } + + /** + * Completely clears this list. + */ + @Override + public void clear() { + fingerList.clear(); + size = 0; + + // Help GC: + for (Node node = first; node != null;) { + node.prev = null; + node.item = null; + Node next = node.next; + node.next = null; + node = next; + } + + first = last = null; + modCount++; + } + + /** + * Returns the clone list with same content as this list. + * + * @return the clone list. + */ + @Override + public Object clone() { + return new IndexedLinkedList<>(this); + } + + /** + * Returns {@code true} only if {@code o} is present in this list. Runs in + * worst-case linear time. + * + * @param o the query object. + */ + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + /** + * Returns {@code true} only if this list contains all the elements + * mentioned in the {@code c}. Runs in Runs in \(\mathcal{O}(mn)\) time, + * where \(m = |c|\). + * + * @param c the query object collection. + * @return {@code true} only if this list contains all the elements in + * {@code c}, or {@code false} otherwise. + */ + @Override + public boolean containsAll(Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + + return true; + } + + /** + * Returns the descending iterator. + * + * @return the descending iterator pointing to the tail of this list. + */ + @Override + public Iterator descendingIterator() { + return new DescendingIterator(); + } + + /** + * Returns the first element of this list. Runs in constant time. + * + * @return the first element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E element() { + return getFirst(); + } + + /** + * Returns {@code true} only if {@code o} is an instance of + * {@link java.util.List} and has the sane contents as this list. Runs in + * worst-case linear time. + * + * @return {@code true} only if {@code o} is a list with the same contents + * as this list. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof List)) { + return false; + } + + int expectedModCount = modCount; + + List otherList = (List) o; + boolean equal = equalsRange(otherList, 0, size); + + checkForComodification(expectedModCount); + return equal; + } + + /** + * Returns {@code index}th element. Runs in worst-case Runs in worst-case + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return {@code index}th element. + * @throws IndexOutOfBoundsException if the index is out of range + * {@code 0, 1, ..., size - 1}, or if this list is empty. + */ + @Override + public E get(int index) { + checkElementIndex(index); + return node(index).item; + } + + /** + * Returns the first element of this list. Runs in constant time. + * + * @return the first element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E getFirst() { + if (first == null) { + throw new NoSuchElementException( + "Getting the head element from an empty list."); + } + + return first.item; + } + + /** + * Returns the last element of this list. Runs in constant time. + * + * @return the last element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E getLast() { + if (last == null) { + throw new NoSuchElementException( + "Getting the tail element from an empty list."); + } + + return last.item; + } + + /** + * Returns the hash code of this list. Runs in linear time. + * + * @return the hash code of this list. + */ + @Override + public int hashCode() { + int expectedModCount = modCount; + int hash = hashCodeRange(0, size); + checkForComodification(expectedModCount); + return hash; + } + + /** + * Returns the index of the leftmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. Runs in worst-case linear time. + * + * @return the index of the leftmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. + * + * @see IndexedLinkedList#lastIndexOf(java.lang.Object) + */ + @Override + public int indexOf(Object obj) { + return indexOfRange(obj, 0, size); + } + + /** + * Returns {@code true} only if this list is empty. + * + * @return {@code true} only if this list is empty. + */ + @Override + public boolean isEmpty() { + return size == 0; + } + + /** + * Returns the iterator over this list. + * + * @return the iterator over this list. + */ + @Override + public Iterator iterator() { + return new BasicIterator(); + } + + /** + * Returns the index of the rightmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. Runs in worst-case linear time. + * + * @return the index of the rightmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. + * + * @see IndexedLinkedList#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(Object obj) { + return lastIndexOfRange(obj, 0, size); + } + + /** + * Returns the list iterator pointing to the head element of this list. + * + * @return the list iterator. + * @see java.util.ListIterator + */ + @Override + public ListIterator listIterator() { + return new EnhancedIterator(0); + } + + /** + * Returns the list iterator pointing between {@code list[index - 1]} and + * {@code list[index]}. + * + * @param index the gap index. The value of zero will point before the head + * element. + * + * @return the list iterator pointing to the {@code index}th gap. + */ + @Override + public ListIterator listIterator(int index) { + return new EnhancedIterator(index); + } + + /** + * Adds {@code e} after the tail element of this list. Runs in constant + * time. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean offer(E e) { + return add(e); + } + + /** + * Adds {@code e} before the head element of this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean offerFirst(E e) { + addFirst(e); + return true; + } + + /** + * Adds {@code e} after the tail element of this list. Runs in constant + * time. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean offerLast(E e) { + addLast(e); + return true; + } + + /** + * Moves all the fingers such that they are evenly distributed. Runs in + * linear time. + */ + public void optimize() { + distributeAllFingers(); + } + + /** + * Takes a look at the first element in this list. + * + * @return the head element or {@code null} if this list is empty. + */ + @Override + public E peek() { + return first == null ? null : first.item; + } + + /** + * Takes a look at the first element in this list. + * + * @return the head element or {@code null} if this list is empty. + */ + @Override + public E peekFirst() { + return first == null ? null : first.item; + } + + /** + * Takes a look at the last element in this list. + * + * @return the tail element or {@code null} if this list is empty. + */ + @Override + public E peekLast() { + return last == null ? null : last.item; + } + + /** + * If this list is empty, does nothing else but return {@code null}. + * Otherwise, removes the first element and returns it. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element (which was removed due to the call to this + * method), or {@code null} if the list is empty. + */ + @Override + public E poll() { + return first == null ? null : removeFirst(); + } + + /** + * If this list is empty, does nothing else but return {@code null}. + * Otherwise, removes the first element and returns it. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element (which was removed due to the call to this + * method), or {@code null} if the list is empty. + */ + @Override + public E pollFirst() { + return first == null ? null : removeFirst(); + } + + /** + * If this list is empty, does nothing else but return {@code null}. + * Otherwise, removes the last element and returns it. Runs in constant + * time. + * + * @return the last element (which was removed due to the call to this + * method), or {@code null} if the list is empty. + */ + @Override + public E pollLast() { + return last == null ? null : removeLast(); + } + + /** + * Removes the first element and returns it. + * Runs in \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element. + * @throws NoSuchElementException if the list is empty. + */ + @Override + public E pop() { + return removeFirst(); + } + + /** + * Adds {@code e} before the head of this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + */ + @Override + public void push(E e) { + addFirst(e); + } + + /** + * Removes and returns the first element. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the head element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E remove() { + return removeFirst(); + } + + /** + * Removes the leftmost occurrence of {@code o} in this list. Runs in worst- + * case Runs in \(\mathcal{O}(n + \sqrt{n})\) time. \(\mathcal{O}(n)\) for + * iterating the list and \(\mathcal{O}(\sqrt{n})\) time for fixing the + * fingers. + * + * @return {@code true} only if {@code o} was located in this list and, + * thus, removed. + */ + @Override + public boolean remove(Object o) { + int index = 0; + + for (Node x = first; x != null; x = x.next, index++) { + if (Objects.equals(o, x.item)) { + removeObjectImpl(x, index); + return true; + } + } + + return false; + } + + /** + * Removes the element residing at the given index. Runs in worst-case + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param index the index of the element to remove. + * @return the removed element. (The one that resided at the index + * {@code index}.) + */ + @Override + public E remove(int index) { + checkElementIndex(index); + + int closestFingerIndex = fingerList.getFingerIndex(index); + Finger closestFinger = fingerList.get(closestFingerIndex); + + E returnValue; + Node nodeToRemove; + + if (closestFinger.index == index) { + nodeToRemove = closestFinger.node; + moveFingerOutOfRemovalLocation(closestFinger, + closestFingerIndex); + } else { + // Keep the fingers at their original position. + // Find the target node: + int steps = closestFinger.index - index; + + nodeToRemove = + traverseLinkedListBackwards( + closestFinger, + steps); + + for (int i = closestFingerIndex + 1; + i <= fingerList.size(); + i++) { + fingerList.get(i).index--; + } + + if (steps > 0) { + fingerList.get(closestFingerIndex).index--; + } + } + + returnValue = nodeToRemove.item; + unlink(nodeToRemove); + decreaseSize(); + + if (mustRemoveFinger()) { + removeFinger(); + } + + return returnValue; + } + + /** + * Removes from this list all the elements mentioned in {@code c}. Runs in + * \(\mathcal{O}(n\sqrt{n} + fn)\) time, where \(\mathcal{O}(f)\) is the + * time of checking for element inclusion in {@code c}. + * + * @param c the collection holding all the elements to remove. + * @return {@code true} only if at least one element in {@code c} was + * located and removed from this list. + */ + @Override + public boolean removeAll(Collection c) { + return batchRemove(c, true, 0, size); + } + + /** + * Removes the first element from this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element. + */ + @Override + public E removeFirst() { + if (size == 0) { + throw new NoSuchElementException( + "removeFirst from an empty LinkedList"); + } + + E returnValue = first.item; + decreaseSize(); + + first = first.next; + + if (first == null) { + last = null; + } else { + first.prev = null; + } + + fingerList.adjustOnRemoveFirst(); + + if (mustRemoveFinger()) { + removeFinger(); + } + + fingerList.get(fingerList.size()).index = size; + return returnValue; + } + + /** + * Removes the leftmost occurrence of {@code o}. Runs in worst-case + * \(\mathcal{O}(n)\) time. + * + * @return {@code true} only if {@code o} was present in the list and was + * successfully removed. + */ + @Override + public boolean removeFirstOccurrence(Object o) { + int index = 0; + + for (Node x = first; x != null; x = x.next, index++) { + if (Objects.equals(o, x.item)) { + removeObjectImpl(x, index); + return true; + } + } + + return false; + } + + /** + * Removes from this list all the elements that satisfy the given input + * predicate. Runs in \(\mathcal{O}(n\sqrt{n})\) time. + * + * @param filter the filtering predicate. + * @return {@code true} only if at least one element was removed. + */ + @Override + public boolean removeIf(Predicate filter) { + return removeIf(filter, 0, size); + } + + /** + * Removes and returns the last element of this list. Runs in constant time. + * + * @return the removed head element. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E removeLast() { + if (size == 0) { + throw new NoSuchElementException("removeLast on empty LinkedList"); + } + + E returnValue = last.item; + decreaseSize(); + + last = last.prev; + + if (last == null) { + first = null; + } else { + last.next = null; + } + + if (mustRemoveFinger()) { + removeFinger(); + } + + return returnValue; + } + + /** + * Removes the rightmost occurrence of {@code o}. Runs in + * \(\mathcal{O}(n)\) time. + * + * @param o the object to remove. + * @return {@code true} only if an element was actually removed. + */ + @Override + public boolean removeLastOccurrence(Object o) { + int index = size - 1; + + for (Node x = last; x != null; x = x.prev, index--) { + if (Objects.equals(o, x.item)) { + removeObjectImpl(x, index); + return true; + } + } + + return false; + } + + /** + * Replaces all the elements in this list by applying the given input + * operator to each of the elements. Runs in linear time. + * + * @param operator the operator mapping one element to another. + */ + @Override + public void replaceAll(UnaryOperator operator) { + replaceAllRange(operator, 0, size); + modCount++; + } + + /** + * Remove all the elements that do not appear in + * {@code c}. Runs in worst-case \(\mathcal{O}(nf + n\sqrt{n})\) time, where + * the inclusion check is run in \(\mathcal{O}(f)\) time. + * + * @param c the collection of elements to retain. + * @return {@code true} only if at least one element was removed. + */ + @Override + public boolean retainAll(Collection c) { + return batchRemove(c, false, 0, size); + } + + /** + * Sets the element at index {@code index} to {@code element} and returns + * the old element. Runs in worst-case \(\mathcal{O}(\sqrt{n})\) time. + * + * @param index the target index. + * @param element the element to set. + * @return the previous element at the given index. + */ + @Override + public E set(int index, E element) { + checkElementIndex(index); + Node node = node(index); + E oldElement = node.item; + node.item = element; + return oldElement; + } + + /** + * Returns the number of elements in this list. + * + * @return the size of this list. + */ + @Override + public int size() { + return size; + } + + /** + * Sorts stably this list into non-descending order. Runs in + * \(\mathcal{O}(n \log n)\). + * + * @param c the element comparator. + */ + @Override + public void sort(Comparator c) { + if (size == 0) { + return; + } + + Object[] array = toArray(); + Arrays.sort((E[]) array, c); + + Node node = first; + + // Rearrange the items over the linked list nodes: + for (int i = 0; i < array.length; ++i, node = node.next) { + E item = (E) array[i]; + node.item = item; + } + + distributeAllFingers(); + modCount++; + } + + /** + * Returns the spliterator over this list. + */ + @Override + public Spliterator spliterator() { + return new LinkedListSpliterator<>(this, first, size, 0, modCount); + } + + /** + * Returns a sublist view + * {@code list[fromIndex, fromIndex + 1, ..., toIndex - 1}. + * + * @param fromIndex the smallest index, inclusive. + * @param toIndex the largest index, exclusive. + * @return the sublist view. + */ + public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + return new EnhancedSubList(this, fromIndex, toIndex); + } + + /** + * Returns the {@link Object} array containing all the elements in this + * list, in the same order as they appear in the list. + * + * @return the list contents in an {@link Object} array. + */ + @Override + public Object[] toArray() { + Object[] arr = new Object[size]; + int index = 0; + + for (Node node = first; node != null; node = node.next) { + arr[index++] = node.item; + } + + return arr; + } + + /** + * Generates the array containing all the elements in this list. + * + * @param the array component type. + * @param generator the generator function. + * @return the list contents in an array with component type of {@code T}. + */ + @Override + public T[] toArray(IntFunction generator) { + return toArray(generator.apply(size)); + } + + /** + * If {@code a} is sufficiently large, returns the same array holding all + * the contents of this list. Also, if {@code a} is larger than the input + * array, sets {@code a[s] = null}, where {@code s} is the size of this + * list. However, if {@code a} is smaller than this list, allocates a new + * array of the same length, populates it with the list contents and returns + * it. + * + * @param the element type. + * @param a the input array. + * @return an array holding the contents of this list. + */ + @SuppressWarnings("unchecked") + @Override + public T[] toArray(T[] a) { + if (a.length < size) { + a = (T[]) Array.newInstance(a.getClass().getComponentType(), size); + } + + int index = 0; + + for (Node node = first; node != null; node = node.next) { + a[index++] = (T) node.item; + } + + if (a.length > size) { + a[size] = null; + } + + return a; + } + + /** + * Returns the string representation of this list, listing all the elements. + * + * @return the string representation of this list. + */ + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("["); + + boolean firstIteration = true; + + for (E element : this) { + if (firstIteration) { + firstIteration = false; + } else { + stringBuilder.append(", "); + } + + stringBuilder.append(element); + } + + return stringBuilder.append("]").toString(); + } + + int getFingerListSize() { + return fingerList.size(); + } + + // Computes the recommended number of fingers for 'size' elements. + private static int getRecommendedNumberOfFingers(int size) { + return (int) Math.ceil(Math.sqrt(size)); + } + + private static void subListRangeCheck(int fromIndex, + int toIndex, + int size) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); + } + + if (toIndex > size){ + throw new IndexOutOfBoundsException( + "toIndex(" + toIndex + ") > size(" + size + ")"); + } + + if (fromIndex > toIndex) + throw new IllegalArgumentException( + "fromIndex(" + fromIndex + ") > toIndex(" + + toIndex + ")"); + } + + // Adds fingers after appending a collection to this list. + private void addFingersAfterAppendAll( + Node first, + int firstIndex, + int collectionSize) { + int numberOfNewFingers = + getRecommendedNumberOfFingers() - fingerList.size(); + + if (numberOfNewFingers == 0) { + fingerList.get(fingerList.size()).index += collectionSize; + return; + } + + int distanceBetweenFingers = collectionSize / numberOfNewFingers; + int nodesToSkip = distanceBetweenFingers / 2; + int index = firstIndex + nodesToSkip; + Node node = first; + + for (int i = 0; i < nodesToSkip; i++) { + node = node.next; + } + + int fingerIndex = fingerList.size(); + + fingerList.makeRoomAtIndex(fingerIndex, + numberOfNewFingers, + collectionSize); + + fingerList.setFinger(fingerIndex++, new Finger<>(node, index)); + + for (int i = 1; i < numberOfNewFingers; i++) { + index += distanceBetweenFingers; + + for (int j = 0; j < distanceBetweenFingers; j++) { + node = node.next; + } + + fingerList.setFinger(fingerIndex++, new Finger<>(node, index)); + } + } + + // Adds fingers after inserting a collection in this list. + private void addFingersAfterInsertAll(Node headNodeOfInsertedRange, + int indexOfInsertedRangeHead, + int collectionSize) { + int numberOfNewFingers = + getRecommendedNumberOfFingers() - fingerList.size(); + + if (numberOfNewFingers == 0) { + int fingerIndex = + fingerList.getFingerIndexImpl(indexOfInsertedRangeHead); + + fingerList.shiftFingerIndicesToRight(fingerIndex, collectionSize); + return; + } + + int distanceBetweenFingers = collectionSize / numberOfNewFingers; + int startOffset = distanceBetweenFingers / 2; + int index = indexOfInsertedRangeHead + startOffset; + Node node = headNodeOfInsertedRange; + + for (int i = 0; i < startOffset; i++) { + node = node.next; + } + + int startFingerIndex = + fingerList.getFingerIndexImpl(indexOfInsertedRangeHead); + + fingerList.makeRoomAtIndex(startFingerIndex, + numberOfNewFingers, + collectionSize); + + fingerList.setFinger(startFingerIndex, new Finger<>(node, index)); + + for (int i = 1; i < numberOfNewFingers; i++) { + index += distanceBetweenFingers; + + for (int j = 0; j < distanceBetweenFingers; j++) { + node = node.next; + } + + fingerList.setFinger(startFingerIndex + i, + new Finger<>(node, index)); + } + } + + // Adds fingers after prepending a collection to this list. + private void addFingersAfterPrependAll(Node first, int collectionSize) { + int numberOfNewFingers = + getRecommendedNumberOfFingers() - fingerList.size(); + + if (numberOfNewFingers == 0) { + fingerList.shiftFingerIndicesToRight(0, collectionSize); + return; + } + + fingerList.makeRoomAtIndex(0, numberOfNewFingers, collectionSize); + + int distance = collectionSize / numberOfNewFingers; + int startIndex = distance / 2; + int index = startIndex; + Node node = first; + + for (int i = 0; i < startIndex; i++) { + node = node.next; + } + + int fingerIndex = 0; + + fingerList.setFinger(fingerIndex++, new Finger<>(node, index)); + + for (int i = 1; i < numberOfNewFingers; i++) { + index += distance; + + for (int j = 0; j < distance; j++) { + node = node.next; + } + + fingerList.setFinger(fingerIndex++, new Finger<>(node, index)); + } + } + + // Adds fingers after setting a collection as a list. + private void addFingersAfterSetAll(int collectionSize) { + int numberOfNewFingers = getRecommendedNumberOfFingers(); + int distance = size / numberOfNewFingers; + int startIndex = distance / 2; + int index = startIndex; + fingerList.makeRoomAtIndex(0, + numberOfNewFingers, + collectionSize); + + Node node = first; + + for (int i = 0; i < startIndex; i++) { + node = node.next; + } + + fingerList.setFinger(0, new Finger<>(node, startIndex)); + + for (int i = 1; i < numberOfNewFingers; i++) { + index += distance; + + for (int j = 0; j < distance; j++) { + node = node.next; + } + + fingerList.setFinger(i, new Finger<>(node, index)); + } + } + + // Appends the input collection to the tail of this list. + private void appendAll(Collection c) { + Node prev = last; + Node oldLast = last; + + for (E item : c) { + Node newNode = new Node<>(item); + newNode.item = item; + newNode.prev = prev; + prev.next = newNode; + prev = newNode; + } + + last = prev; + int sz = c.size(); + size += sz; + modCount++; + addFingersAfterAppendAll(oldLast.next, size - sz, sz); + } + + // Adds the finger to the tail of the finger list and before the end-of- + // finger-list sentinel. + private void appendFinger(Node node, int index) { + Finger finger = new Finger<>(node, index); + fingerList.appendFinger(finger); + } + + /** + * This class implements a basic iterator over this list. + */ + public final class BasicIterator implements Iterator { + + private Node lastReturned; + private Node next = first; + private int nextIndex; + int expectedModCount = IndexedLinkedList.this.modCount; + + /** + * Constructs the basic iterator pointing to the first element. + */ + BasicIterator() { + + } + + @Override + public boolean hasNext() { + return nextIndex < size; + } + + @Override + public E next() { + checkForComodification(); + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + lastReturned = next; + next = next.next; + nextIndex++; + return lastReturned.item; + } + + @Override + public void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + + checkForComodification(); + + int removalIndex = nextIndex - 1; + removeObjectImpl(lastReturned, removalIndex); + nextIndex--; + lastReturned = null; + expectedModCount++; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + while (modCount == expectedModCount && nextIndex < size) { + action.accept(next.item); + next = next.next; + nextIndex++; + } + + checkForComodification(); + } + + private void checkForComodification() { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + // Implements the batch remove. If 'complement' is true, this operation + // removes all the elements appearing in 'c'. Otherwise, it will retain all + // the elements present in 'c': + private boolean batchRemove(Collection c, + boolean complement, + int from, + int end) { + Objects.requireNonNull(c); + + if (c.isEmpty()) { + return false; + } + + boolean modified = false; + + int numberOfNodesToIterate = end - from; + int i = 0; + int nodeIndex = from; + + for (Node node = node(from); i < numberOfNodesToIterate; ++i) { + Node nextNode = node.next; + + if (c.contains(node.item) == complement) { + modified = true; + removeObjectImpl(node, nodeIndex); + } else { + nodeIndex++; + } + + node = nextNode; + } + + return modified; + } + + // Checks the element index. In the case of non-empty list, valid indices + // are '{ 0, 1, ..., size - 1 }'. + private void checkElementIndex(int index) { + if (!isElementIndex(index)) { + throw new IndexOutOfBoundsException(getOutOfBoundsMessage(index)); + } + } + + private void checkForComodification(int expectedModCount) { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + // Checks that the input index is a valid position index for add operation + // or iterator position. In other words, checks that {@code index} is in the + // set '{ 0, 1, ..., size}'. + private void checkPositionIndex(int index) { + if (!isPositionIndex(index)) { + throw new IndexOutOfBoundsException(getOutOfBoundsMessage(index)); + } + } + + private void decreaseSize() { + size--; + modCount++; + } + + // Distributes the fingers over the element list [fromIndex, toIndex): + private void distributeFingers(int fromIndex, int toIndex) { + int rangeLength = toIndex - fromIndex; + + if (rangeLength == 0) { + return; + } + + int fingerPrefixLength = fingerList.getFingerIndexImpl(fromIndex); + int fingerSuffixLength = fingerList.size() + - fingerList.getFingerIndexImpl(toIndex); + + int numberOfRangeFingers = fingerList.size() + - fingerPrefixLength + - fingerSuffixLength; + + int numberOfFingersPerFinger = rangeLength / numberOfRangeFingers; + int startOffset = numberOfFingersPerFinger / 2; + int index = fromIndex + startOffset; + + Node node = node(fromIndex); + + for (int i = 0; i < startOffset; ++i) { + node = node.next; + } + + for (int i = 0; i < numberOfRangeFingers - 1; ++i) { + Finger finger = fingerList.get(i); + finger.node = node; + finger.index = index; + + for (int j = 0; j < numberOfFingersPerFinger; ++j) { + node = node.next; + } + + index += numberOfFingersPerFinger; + } + + // Since we cannot advance node to the right, we need to deal with the + // last (non-sentinel) finger manually: + Finger lastFinger = fingerList.get(fingerList.size() - 1); + lastFinger.node = node; + lastFinger.index = index; + } + + // Distributes evenly all the figners over this list: + private void distributeAllFingers() { + distributeFingers(0, size); + } + + // Implements the descending list iterator over this list. + private final class DescendingIterator implements Iterator { + + private Node lastReturned; + private Node nextToIterate = last; + private int nextIndex = IndexedLinkedList.this.size - 1; + int expectedModCount = IndexedLinkedList.this.modCount; + + @Override + public boolean hasNext() { + return nextIndex > -1; + } + + @Override + public E next() { + checkForComodification(); + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + lastReturned = nextToIterate; + nextToIterate = nextToIterate.prev; + nextIndex--; + return lastReturned.item; + } + + @Override + public void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + + checkForComodification(); + + removeObjectImpl(lastReturned, nextIndex + 1); + lastReturned = null; + expectedModCount++; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + while (modCount == expectedModCount && hasNext()) { + action.accept(nextToIterate.item); + nextToIterate = nextToIterate.prev; + nextIndex--; + } + + checkForComodification(); + } + + private void checkForComodification() { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + // Implements the enhanced list iterator over this list. + final class EnhancedIterator implements ListIterator { + + private Node lastReturned; + private Node next; + private int nextIndex; + + // Package-private for the sake of unit testing: + int expectedModCount = modCount; + + EnhancedIterator(int index) { + next = (index == size) ? null : node(index); + nextIndex = index; + } + + @Override + public boolean hasNext() { + return nextIndex < size; + } + + @Override + public E next() { + checkForComdification(); + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + lastReturned = next; + next = next.next; + nextIndex++; + return lastReturned.item; + } + + @Override + public boolean hasPrevious() { + return nextIndex > 0; + } + + @Override + public E previous() { + checkForComdification(); + + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + + lastReturned = next = (next == null) ? last : next.prev; + nextIndex--; + return lastReturned.item; + } + + @Override + public int nextIndex() { + return nextIndex; + } + + @Override + public int previousIndex() { + return nextIndex - 1; + } + + @Override + public void remove() { + checkForComdification(); + + if (lastReturned == null) { + throw new IllegalStateException(); + } + + Node lastNext = lastReturned.next; + int removalIndex = nextIndex - 1; + removeObjectImpl(lastReturned, removalIndex); + + if (next == lastReturned) { + next = lastNext; + } else { + nextIndex = removalIndex; + } + + lastReturned = null; + expectedModCount++; + } + + @Override + public void set(E e) { + if (lastReturned == null) { + throw new IllegalStateException(); + } + + checkForComdification(); + lastReturned.item = e; + } + + @Override + public void add(E e) { + checkForComdification(); + + lastReturned = null; + + if (next == null) { + linkLast(e); + } else { + linkBefore(e, next, nextIndex); + } + + nextIndex++; + expectedModCount++; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + while (modCount == expectedModCount && nextIndex < size) { + action.accept(next.item); + next = next.next; + nextIndex++; + } + + checkForComdification(); + } + + private void checkForComdification() { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + private boolean equalsRange(List other, int from, int to) { + Iterator otherIterator = other.iterator(); + + for (Node node = node(from); from < to; from++, node = node.next) { + if (!otherIterator.hasNext() || + !Objects.equals(node.item, otherIterator.next())) { + return false; + } + } + + return true; + } + + // Constructs an IndexOutOfBoundsException detail message. + private String getOutOfBoundsMessage(int index) { + return "Index: " + index + ", Size: " + size; + } + + // Computes the recommended number of fingers. + private int getRecommendedNumberOfFingers() { + return (int) Math.ceil(Math.sqrt(size)); + } + + // Computes the hash code for the range [from, to): + private int hashCodeRange(int from, int to) { + int hashCode = 1; + + Node node = node(from); + + while (from++ < to) { + // Same arithmetics as in ArrayList. + hashCode = + 31 * hashCode + + (node.item == null ? 0 : node.item.hashCode()); + + node = node.next; + } + + return hashCode; + } + + // Increases the size of the list and its modification count. + private void increaseSize() { + ++size; + ++modCount; + } + + private int indexOfRange(Object o, int start, int end) { + int index = start; + + if (o == null) { + for (Node node = node(start); + index < end; + index++, node = node.next) { + if (node.item == null) { + return index; + } + } + } else { + for (Node node = node(start); + index < end; + index++, node = node.next) { + if (o.equals(node.item)) { + return index; + } + } + } + + return -1; + } + + // Inserts the input collection right before the node 'succ'. + private void insertAll(Collection c, + Node succ, + int succIndex) { + + Node pred = succ.prev; + Node prev = pred; + + for (E item : c) { + Node newNode = new Node<>(item); + newNode.prev = prev; + prev.next = newNode; + prev = newNode; + } + + prev.next = succ; + succ.prev = prev; + + int sz = c.size(); + modCount++; + size += sz; + + // Add fingers: + addFingersAfterInsertAll(pred.next, + succIndex, + sz); + } + + // Tells if the argument is the index of an existing element. + private boolean isElementIndex(int index) { + return index >= 0 && index < size; + } + + // Tells if the argument is the index of a valid position for an iterator or + // an add operation. + private boolean isPositionIndex(int index) { + return index >= 0 && index <= size; + } + + // Returns the last appearance index of 'obj'. + private int lastIndexOfRange(Object o, int start, int end) { + int index = end - 1; + + if (o == null) { + for (Node node = node(index); + index >= start; + index--, node = node.prev) { + if (node.item == null) { + return index; + } + } + } else { + for (Node node = node(index); + index >= start; + index--, node = node.prev) { + if (o.equals(node.item)) { + return index; + } + } + } + + return -1; + } + + // Links the input element right before the node 'succ'. + private void linkBefore(E e, Node succ, int index) { + Node pred = succ.prev; + Node newNode = new Node<>(e); + newNode.next = succ; + succ.prev = newNode; + + if (pred == null) { + first = newNode; + } else { + pred.next = newNode; + newNode.prev = pred; + } + + size++; + modCount++; + + if (mustAddFinger()) { + fingerList.insertFingerAndShiftOnceToRight( + new Finger<>(newNode, index)); + } else { + int fingerIndex = fingerList.getFingerIndex(index); + fingerList.shiftFingerIndicesToRightOnce(fingerIndex); + } + } + + // Prepends the input element to the head of this list. + private void linkFirst(E e) { + Node f = first; + Node newNode = new Node<>(e); + newNode.next = f; + first = newNode; + + if (f == null) { + last = newNode; + } else { + f.prev = newNode; + } + + increaseSize(); + + if (mustAddFinger()) { + fingerList.insertFingerAndShiftOnceToRight( + new Finger<>(newNode, 0)); + } else { + fingerList.shiftFingerIndicesToRightOnce(0); + } + } + + // Appends the input element to the tail of this list. + private void linkLast(E e) { + Node l = last; + Node newNode = new Node<>(e); + newNode.prev = l; + last = newNode; + + if (l == null) { + first = newNode; + } else { + l.next = newNode; + } + + increaseSize(); + + if (mustAddFinger()) { + appendFinger(newNode, size - 1); + } else { + fingerList.get(fingerList.size()).index++; + } + } + + // Sets a finger that does not point to the element to remove. We need this + // in order to make sure that after removal, all the fingers point to valid + // nodes. + void moveFingerOutOfRemovalLocation(Finger finger, int fingerIndex) { + if (fingerList.size() == size()) { + // Here, fingerList.size() is 1 or 2 and the size of the list is the + // same: + if (fingerList.size() == 1) { + // The only finger will be removed in 'remove(int)'. Return: + return; + } + + // Once here, 'fingerList.size() == 2'! + switch (fingerIndex) { + case 0: + // Shift 2nd and the sentinal fingers one position to the + // left: + fingerList.setFinger(0, fingerList.get(1)); + fingerList.get(0).index = 0; + fingerList.setFinger(1, fingerList.get(2)); + fingerList.get(1).index = 1; + fingerList.setFinger(2, null); + fingerList.size = 1; + break; + + case 1: + // Just remove the (last) finger: + fingerList.removeFinger(); + fingerList.get(1).index = 1; + break; + } + + return; + } + + // Try push the fingers to the right: + for (int f = fingerIndex; f < fingerList.size(); ++f) { + Finger fingerLeft = fingerList.get(f); + Finger fingerRight = fingerList.get(f + 1); + + if (fingerLeft.index + 1 < fingerRight.index) { + for (int i = f; i >= fingerIndex; --i) { + Finger fngr = fingerList.get(i); + fngr.node = fngr.node.next; + } + + for (int j = f + 1; j <= fingerList.size(); ++j) { + fingerList.get(j).index--; + } + + return; + } + } + + // Could not push the fingers to the right. Push to the left. Since the + // number of fingers here is smaller than the list size, there must be + // a spot to move to some fingers: + for (int f = fingerIndex; f > 0; --f) { + Finger fingerLeft = fingerList.get(f - 1); + Finger fingerRight = fingerList.get(f); + + if (fingerLeft.index + 1 < fingerRight.index) { + for (int i = fingerIndex; i > 0; --i) { + Finger fngr = fingerList.get(i); + fngr.node = fngr.node.prev; + fngr.index--; + } + + for (int i = fingerIndex + 1; i <= fingerList.size(); ++i) { + fingerList.get(i).index--; + } + + return; + } + } + + // Once here, the only free spots are at the very beginning of the + // finger list: + for (int i = 0; i < fingerList.size(); ++i) { + Finger fngr = fingerList.get(i); + fngr.index--; + fngr.node = fngr.node.prev; + } + + // The end-of-finger-list node has no Finger.node defined. Take it + // outside of the above loop and decrement its index manually:cd + fingerList.get(fingerList.size()).index--; + } + + // Returns true only if this list requires more fingers. + private boolean mustAddFinger() { + // Here, fingerStack.size() == getRecommendedFingerCount(), or, + // fingerStack.size() == getRecommendedFingerCount() - 1 + return fingerList.size() != getRecommendedNumberOfFingers(); + } + + // Returns true only if this list requires less fingers. + private boolean mustRemoveFinger() { + // Here, fingerStack.size() == getRecommendedFingerCount(), or, + // fingerStack.size() == getRecommendedFingerCount() + 1 + return fingerList.size() != getRecommendedNumberOfFingers(); + } + + // Returns the node at index 'elementIndex'. + private Node node(int elementIndex) { + return fingerList.node(elementIndex); + } + + // Prepends the input collection to the head of this list. + private void prependAll(Collection c) { + Iterator iterator = c.iterator(); + Node oldFirst = first; + first = new Node<>(iterator.next()); + + Node prevNode = first; + + for (int i = 1, sz = c.size(); i < sz; i++) { + Node newNode = new Node<>(iterator.next()); + newNode.prev = prevNode; + prevNode.next = newNode; + prevNode = newNode; + } + + prevNode.next = oldFirst; + oldFirst.prev = prevNode; + + int sz = c.size(); + modCount++; + size += sz; + + // Now, add the missing fingers: + addFingersAfterPrependAll(first, sz); + } + + /** + * Reconstitutes this {@code LinkedList} instance from a stream (that is, + * deserializes it). + * + * @param s the object input stream. + * + * @serialData first, the size of the list is read. Then all the node items + * are read and stored in the deserialization order, that is the + * same order as in serialization. + * + * @throws java.io.IOException if I/O fails. + * @throws ClassNotFoundException if the class is not found. + */ + @java.io.Serial + private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in any hidden serialization magic + s.defaultReadObject(); + + int size = s.readInt(); + this.size = size; + this.fingerList = new FingerList<>(); + + switch (size) { + case 0: + return; + + case 1: + Node newNode = new Node<>((E) s.readObject()); + first = last = newNode; + fingerList.appendFinger(new Finger<>(newNode, 0)); + return; + } + + Node rightmostNode = new Node<>((E) s.readObject()); + first = rightmostNode; + + int numberOfRequestedFingers = getRecommendedNumberOfFingers(size); + int distance = size / numberOfRequestedFingers; + int startOffset = distance / 2; + + // Read in all elements in the proper order. + for (int i = 1; i < size; i++) { + Node node = new Node<>((E) s.readObject()); + + if ((i - startOffset) % distance == 0) { + fingerList.appendFinger(new Finger<>(node, i)); + } + + rightmostNode.next = node; + node.prev = rightmostNode; + rightmostNode = node; + } + + last = rightmostNode; + } + + private void removeFinger() { + fingerList.removeFinger(); + } + + // Removes all the items that satisfy the given predicate. + private boolean removeIf(Predicate filter, + int fromIndex, + int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + + boolean modified = false; + int numberOfNodesToIterate = toIndex - fromIndex; + int i = 0; + int nodeIndex = fromIndex; + + for (Node node = node(fromIndex); i < numberOfNodesToIterate; ++i) { + Node nextNode = node.next; + + if (filter.test(node.item)) { + modified = true; + removeObjectImpl(node, nodeIndex); + } else { + nodeIndex++; + } + + node = nextNode; + } + + return modified; + } + + // Implements the node removal. + private void removeObjectImpl(Node node, int index) { + int closestFingerIndex = fingerList.getFingerIndex(index); + Finger closestFinger = fingerList.get(closestFingerIndex); + + if (closestFinger.index == index) { + // Make sure no finger is pointing to 'node': + moveFingerOutOfRemovalLocation(closestFinger, closestFingerIndex); + } else { + for (int i = closestFingerIndex + 1; + i <= fingerList.size(); + i++) { + fingerList.get(i).index--; + } + + int steps = closestFinger.index - index; + + if (steps > 0) { + fingerList.get(closestFingerIndex).index--; + } + } + + unlink(node); + decreaseSize(); + + if (mustRemoveFinger()) { + removeFinger(); + } + } + + // Removes the finger range 'fingereList[fromIndex], ..., + // fingerList[toIndex - 1]'. + private void removeRange(int fromIndex, int toIndex) { + int removalSize = toIndex - fromIndex; + + if (removalSize == 0) { + return; + } + + if (removalSize == size) { + clear(); + return; + } + + Node firstNodeToRemove = node(fromIndex); + + int nextFingerCount = getRecommendedNumberOfFingers(size - removalSize); + int prefixSize = fromIndex; + int suffixSize = size - toIndex; + int prefixFingersSize = fingerList.getFingerIndexImpl(fromIndex); + int suffixFingersSize = fingerList.size - + fingerList.getFingerIndexImpl(toIndex); + + int prefixFreeSpotCount = prefixSize - prefixFingersSize; + int suffixFreeSpotCount = suffixSize - suffixFingersSize; + + if (prefixFreeSpotCount == 0) { + if (suffixFreeSpotCount == 0) { + removeRangeNoPrefixNoSuffix(firstNodeToRemove, + fromIndex, + removalSize); + } else { + int numberOfFingersToMove = nextFingerCount - prefixFingersSize; + + // Once here, prefixFreeSpotCount = 0 and + // suffixFreeSpotCount > 0. In other words, we are moving to + // suffix. + fingerList.moveFingersToSuffix(toIndex, numberOfFingersToMove); + fingerList.removeRange(0, suffixFingersSize, removalSize); + removeRangeNodes(firstNodeToRemove, removalSize); + } + } else { + if (suffixFreeSpotCount == 0) { + int numberOfFingersToMove = + Math.min( + nextFingerCount - prefixFingersSize, + prefixFreeSpotCount); + + // Once here, suffixFreeSpotCount = 0 and + // prefixFreeSpotCount > 0. In other words, we are moving a to + // prefix: + fingerList.moveFingersToPrefix( + fromIndex, + numberOfFingersToMove); + + fingerList.removeRange(numberOfFingersToMove, 0, removalSize); + removeRangeNodes(firstNodeToRemove, removalSize); + } else { + int prefixSuffixFreeSpotCount = prefixFreeSpotCount + + suffixFreeSpotCount; + + float prefixLoadFactor = ((float)(prefixFreeSpotCount)) / + ((float)(prefixSuffixFreeSpotCount)); + + int numberOfFingersOnLeft = + (int)(prefixLoadFactor * nextFingerCount); + + int numberOfFingersOnRight = + nextFingerCount - numberOfFingersOnLeft; + + fingerList.moveFingersToPrefix(fromIndex, + numberOfFingersOnLeft); + + fingerList.moveFingersToSuffix(toIndex, numberOfFingersOnRight); + + fingerList.removeRange(numberOfFingersOnLeft, + numberOfFingersOnRight, + removalSize); + + removeRangeNodes(firstNodeToRemove, removalSize); + } + } + + modCount++; + size -= removalSize; + } + + // Removes the unnecessary fingers when the prefix and suffix are empty. + void removeRangeNoPrefixNoSuffix(Node node, + int fromIndex, + int removalSize) { + int nextListSize = IndexedLinkedList.this.size - removalSize; + int nextFingerListSize = + getRecommendedNumberOfFingers(nextListSize); + + int fingersToRemove = fingerList.size() - nextFingerListSize; + int firstFingerIndex = fingerList.getFingerIndexImpl(fromIndex); + int fingerCount = 0; + + Finger finger1 = fingerList.get(firstFingerIndex); + Finger finger2 = fingerList.get(firstFingerIndex + 1); + Node prefixLastNode = node.prev; + Node nextNode = node; + + for (int i = 0; i < removalSize - 1; ++i) { + Finger f = fingerList.get(firstFingerIndex + fingerCount); + + if (finger1 == f) { + if (fingersToRemove != 0) { + fingersToRemove--; + fingerCount++; + finger1 = finger2; + finger2 = fingerList.get(firstFingerIndex + fingerCount); + } + } + + nextNode = node.next; + node.next = null; + node.prev = null; + node.item = null; + node = nextNode; + } + + Node suffixFirstNode = nextNode.next; + nextNode.next = null; + nextNode.prev = null; + nextNode.item = null; + + if (fingersToRemove != 0) { + // Count the last finger: + fingerCount++; + } + + if (prefixLastNode != null) { + prefixLastNode.next = null; + last = prefixLastNode; + } else { + suffixFirstNode.prev = null; + first = suffixFirstNode; + } + + fingerList.removeRange( + firstFingerIndex, + fingerList.size() - firstFingerIndex - fingerCount, + removalSize); + } + + // Unlinks the 'numberOfNodesToRemove' consecutive nodes starting from + // 'node'. + private void removeRangeNodes(Node node, int numberOfNodesToRemove) { + Node prefixLastNode = node.prev; + Node nextNode = node; + + for (int i = 0; i < numberOfNodesToRemove - 1; ++i) { + nextNode = node.next; + node.next = null; + node.prev = null; + node.item = null; + node = nextNode; + } + + Node suffixFirstNode = nextNode.next; + nextNode.next = null; + nextNode.prev = null; + nextNode.item = null; + + if (prefixLastNode != null) { + if (suffixFirstNode == null) { + prefixLastNode.next = null; + last = prefixLastNode; + } else { + prefixLastNode.next = suffixFirstNode; + suffixFirstNode.prev = prefixLastNode; + } + } else { + suffixFirstNode.prev = null; + first = suffixFirstNode; + } + } + + // Replaces all the elements from range [i, end): + private void replaceAllRange(UnaryOperator operator, int i, int end) { + Objects.requireNonNull(operator); + int expectedModCount = modCount; + Node node = node(i); + + while (modCount == expectedModCount && i < end) { + node.item = operator.apply(node.item); + node = node.next; + i++; + } + + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + // Sets the input collection as a list. + private void setAll(Collection c) { + Iterator iterator = c.iterator(); + + first = new Node<>(iterator.next()); + Node prevNode = first; + + for (int i = 1, sz = c.size(); i < sz; i++) { + Node newNode = new Node<>(iterator.next()); + prevNode.next = newNode; + newNode.prev = prevNode; + prevNode = newNode; + } + + last = prevNode; + size = c.size(); + modCount++; + + addFingersAfterSetAll(c.size()); + } + + // If steps > 0, rewind to the left. Otherwise, rewind to the right. + private Node traverseLinkedListBackwards(Finger finger, int steps) { + Node node = finger.node; + + if (steps > 0) { + for (int i = 0; i < steps; i++) { + node = node.prev; + } + } else { + steps = -steps; + + for (int i = 0; i < steps; i++) { + node = node.next; + } + } + + return node; + } + + // Unlinks the input node from the actual doubly-linked list. + private void unlink(Node x) { + Node next = x.next; + Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + } + + /** + * Saves the state of this {@code LinkedList} instance to a stream (that is, + * serializes it). + * + * @param s the object output stream. + * + * @serialData The size of the list (the number of elements it + * contains) is emitted (int), followed by all of its + * elements (each an Object) in the proper order. + * + * @throws java.io.IOException if the I/O fails. + */ + @java.io.Serial + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden serialization magic + s.defaultWriteObject(); + + // Write out size + s.writeInt(size); + + // Write out all elements in the proper order. + for (Node x = first; x != null; x = x.next) { + s.writeObject(x.item); + } + } + + static final class LinkedListSpliterator implements Spliterator { + + static final long MINIMUM_BATCH_SIZE = 1 << 10; // 1024 items + + private final IndexedLinkedList list; + private Node node; + private long lengthOfSpliterator; + private long numberOfProcessedElements; + private long offsetOfSpliterator; + private final int expectedModCount; + + private LinkedListSpliterator(IndexedLinkedList list, + Node node, + long lengthOfSpliterator, + long offsetOfSpliterator, + int expectedModCount) { + this.list = list; + this.node = node; + this.lengthOfSpliterator = lengthOfSpliterator; + this.offsetOfSpliterator = offsetOfSpliterator; + this.expectedModCount = expectedModCount; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + + if (numberOfProcessedElements == lengthOfSpliterator) { + return false; + } + + numberOfProcessedElements++; + E item = node.item; + action.accept(item); + node = node.next; + + if (list.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + + return true; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + for (long i = numberOfProcessedElements; + i < lengthOfSpliterator; + i++) { + E item = node.item; + action.accept(item); + node = node.next; + } + + numberOfProcessedElements = lengthOfSpliterator; + + if (list.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + @Override + public Spliterator trySplit() { + long sizeLeft = estimateSize(); + + if (sizeLeft == 0) { + return null; + } + + long thisSpliteratorNewLength = sizeLeft / 2L; + + if (thisSpliteratorNewLength < MINIMUM_BATCH_SIZE) { + return null; + } + + long newSpliteratorLength = sizeLeft - thisSpliteratorNewLength; + long newSpliteratorOffset = this.offsetOfSpliterator; + + this.offsetOfSpliterator += newSpliteratorLength; + this.lengthOfSpliterator -= newSpliteratorLength; + + Node newSpliteratorNode = this.node; + + this.node = list.node((int) this.offsetOfSpliterator); + + return new LinkedListSpliterator<>(list, + newSpliteratorNode, + newSpliteratorLength, // length + newSpliteratorOffset, // offset + expectedModCount); + } + + @Override + public long estimateSize() { + return (long)(lengthOfSpliterator - numberOfProcessedElements); + } + + @Override + public long getExactSizeIfKnown() { + return estimateSize(); + } + + @Override + public int characteristics() { + return Spliterator.ORDERED | + Spliterator.SUBSIZED | + Spliterator.SIZED; + } + + @Override + public boolean hasCharacteristics(int characteristics) { + switch (characteristics) { + case Spliterator.ORDERED: + case Spliterator.SIZED: + case Spliterator.SUBSIZED: + return true; + + default: + return false; + } + } + } + + class EnhancedSubList implements List, Cloneable { + + private final IndexedLinkedList root; + private final EnhancedSubList parent; + private final int offset; + private int size; + private int modCount; + + public EnhancedSubList(IndexedLinkedList root, + int fromIndex, + int toIndex) { + + this.root = root; + this.parent = null; + this.offset = fromIndex; + this.size = toIndex - fromIndex; + this.modCount = root.modCount; + } + + private EnhancedSubList(EnhancedSubList parent, + int fromIndex, + int toIndex) { + + this.root = parent.root; + this.parent = parent; + this.offset = parent.offset + fromIndex; + this.size = toIndex - fromIndex; + this.modCount = root.modCount; + } + + @Override + public boolean add(E e) { + checkInsertionIndex(size); + checkForComodification(); + root.add(offset + size, e); + updateSizeAndModCount(1); + return true; + } + + @Override + public void add(int index, E element) { + checkInsertionIndex(index); + checkForComodification(); + root.add(offset + index, element); + updateSizeAndModCount(1); + } + + @Override + public boolean addAll(Collection c) { + return addAll(this.size, c); + } + + @Override + public boolean addAll(int index, Collection collection) { + checkInsertionIndex(index); + int collectionSize = collection.size(); + + if (collectionSize == 0) { + return false; + } + + checkForComodification(); + root.addAll(offset + index, collection); + updateSizeAndModCount(collectionSize); + return true; + } + + @Override + public void clear() { + checkForComodification(); + root.removeRange(offset, offset + size); + updateSizeAndModCount(-size); + } + + @Override + public Object clone() { + List list = new IndexedLinkedList<>(); + + for (E element : this) { + list.add(element); + } + + return list; + } + + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + @Override + public boolean containsAll(Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + + return true; + } + + @Override + public boolean equals(Object o) { + return root.equalsRange((List) o, offset, offset + size); + } + + @Override + public void forEach(Consumer action) { + Objects.requireNonNull(action); + int expectedModCount = modCount; + int iterated = 0; + + for (Node node = node(offset); + modCount == expectedModCount && iterated < size; + node = node.next, ++iterated) { + + action.accept(node.item); + } + + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + @Override + public E get(int index) { + Objects.checkIndex(index, size); + checkForComodification(); + return root.get(offset + index); + } + + @Override + public int hashCode() { + return root.hashCodeRange(offset, offset + size); + } + + @Override + public int indexOf(Object o) { + int index = root.indexOfRange(o, offset, offset + size); + checkForComodification(); + return index >= 0 ? index - offset : -1; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public Iterator iterator() { + return listIterator(); + } + + @Override + public int lastIndexOf(Object o) { + int index = root.lastIndexOfRange(o, offset, offset + size); + checkForComodification(); + return index >= 0 ? index - offset : -1; + } + + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + @Override + public ListIterator listIterator(int index) { + checkForComodification(); + checkInsertionIndex(index); + + return new ListIterator() { + private final ListIterator i = + root.listIterator(offset + index); + + public boolean hasNext() { + return nextIndex() < size; + } + + public E next() { + if (hasNext()) { + return i.next(); + } + + throw new NoSuchElementException(); + } + + public boolean hasPrevious() { + return previousIndex() >= 0; + } + + public E previous() { + if (hasPrevious()) { + return i.previous(); + } + + throw new NoSuchElementException(); + } + + public int nextIndex() { + return i.nextIndex() - offset; + } + + public int previousIndex() { + return i.previousIndex() - offset; + } + + public void remove() { + i.remove(); + updateSizeAndModCount(-1); + } + + public void set(E e) { + i.set(e); + } + + public void add(E e) { + i.add(e); + updateSizeAndModCount(1); + } + }; + } + + @Override + public boolean remove(Object o) { + ListIterator iterator = listIterator(); + + if (o == null) { + while (iterator.hasNext()) { + if (iterator.next() == null) { + iterator.remove(); + return true; + } + } + } else { + while (iterator.hasNext()) { + if (o.equals(iterator.next())) { + iterator.remove(); + return true; + } + } + } + + return false; + } + + @Override + public E remove(int index) { + Objects.checkIndex(index, size); + checkForComodification(); + E result = root.remove(offset + index); + updateSizeAndModCount(-1); + return result; + } + + @Override + public boolean removeAll(Collection c) { + return batchRemove(c, true); + } + + @Override + public boolean removeIf(Predicate filter) { + checkForComodification(); + int oldSize = root.size; + boolean modified = root.removeIf(filter, offset, offset + size); + + if (modified) { + updateSizeAndModCount(root.size - oldSize); + } + + return modified; + } + + @Override + public void replaceAll(UnaryOperator operator) { + root.replaceAllRange(operator, offset, offset + size); + } + + @Override + public boolean retainAll(Collection c) { + return batchRemove(c, false); + } + + @Override + public E set(int index, E element) { + Objects.checkIndex(index, size); + checkForComodification(); + return root.set(offset + index, element); + } + + @Override + public int size() { + checkForComodification(); + return size; + } + + @Override + @SuppressWarnings("unchecked") + public void sort(Comparator c) { + if (size == 0) { + return; + } + + int expectedModCount = modCount; + Object[] array = toArray(); + Node node = node(offset); + + Arrays.sort((E[]) array, c); + + // Rearrange the items over the linked list nodes: + for (int i = 0; i < array.length; ++i, node = node.next) { + E item = (E) array[i]; + node.item = item; + } + + if (expectedModCount != modCount) { + throw new ConcurrentModificationException(); + } + + distributeFingers(offset, offset + size); + modCount++; + } + + @Override + public Spliterator spliterator() { + return new LinkedListSpliterator(root, + node(offset), + size, + offset, + modCount); + } + + @Override + public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + return new EnhancedSubList(this, fromIndex, toIndex); + } + + @Override + public Object[] toArray() { + checkForComodification(); + int expectedModCount = root.modCount; + Object[] array = new Object[this.size]; + Node node = node(offset); + + for (int i = 0; + i < array.length && expectedModCount == root.modCount; + i++, node = node.next) { + array[i] = node.item; + } + + if (expectedModCount != root.modCount) { + throw new ConcurrentModificationException(); + } + + return array; + } + + @Override + public T[] toArray(IntFunction generator) { + return toArray(generator.apply(size)); + } + + @Override + public T[] toArray(T[] a) { + checkForComodification(); + + int expectedModCount = root.modCount; + + if (a.length < size) { + a = (T[]) Array.newInstance( + a.getClass().getComponentType(), + size); + } + + int index = 0; + + for (Node node = node(offset); + expectedModCount == root.modCount && index < size; + ++index, node = node.next) { + + a[index] = (T) node.item; + } + + if (a.length > size) { + a[size] = null; + } + + return a; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("["); + + boolean firstIteration = true; + + for (E element : this) { + if (firstIteration) { + firstIteration = false; + } else { + stringBuilder.append(", "); + } + + stringBuilder.append(element); + } + + return stringBuilder.append("]").toString(); + } + + private boolean batchRemove(Collection c, boolean complement) { + checkForComodification(); + int oldSize = root.size; + boolean modified = root.batchRemove(c, + complement, + offset, + offset + size); + + if (modified) { + updateSizeAndModCount(root.size - oldSize); + } + + return modified; + } + + private void checkForComodification() { + if (root.modCount != this.modCount) + throw new ConcurrentModificationException(); + } + + private void checkInsertionIndex(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException("Negative index: " + index); + } + + if (index > this.size) { + throw new IndexOutOfBoundsException( + "index(" + index + ") > size(" + size + ")"); + } + } + + private void updateSizeAndModCount(int sizeDelta) { + EnhancedSubList subList = this; + + do { + subList.size += sizeDelta; + subList.modCount = root.modCount; + subList = subList.parent; + } while (subList != null); + } + } +} diff --git a/src/test/java/org/apache/commons/collections4/list/FingerListTest.java b/src/test/java/org/apache/commons/collections4/list/FingerListTest.java new file mode 100644 index 0000000000..741eb4e68c --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/FingerListTest.java @@ -0,0 +1,136 @@ +/* + * 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 com.github.coderodde.util.IndexedLinkedList.Finger; +import com.github.coderodde.util.IndexedLinkedList.Node; +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Test; + +public class FingerListTest { + + private final IndexedLinkedList list = new IndexedLinkedList<>(); + private final IndexedLinkedList.FingerList fl = + list.fingerList; + + @Before + public void setUp() { + fl.clear(); + } + + @Test + public void appendGetFinger() { + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(0)), 0)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(1)), 1)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(3)), 3)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(6)), 6)); + fl.fingerArray[4].index = 8; + fl.fingerArray[4].node = new Node<>(Integer.valueOf(1000)); + + Finger finger = fl.get(fl.getFingerIndex(0)); + assertEquals(0, finger.index); + assertEquals(Integer.valueOf(0), finger.node.item); + + finger = fl.get(fl.getFingerIndex(1)); + assertEquals(1, finger.index); + assertEquals(Integer.valueOf(1), finger.node.item); + + finger = fl.get(fl.getFingerIndex(2)); + assertEquals(3, finger.index); + assertEquals(Integer.valueOf(3), finger.node.item); + + finger = fl.get(fl.getFingerIndex(3)); + assertEquals(3, finger.index); + assertEquals(Integer.valueOf(3), finger.node.item); + + finger = fl.get(fl.getFingerIndex(4)); + assertEquals(3, finger.index); + assertEquals(Integer.valueOf(3), finger.node.item); + + finger = fl.get(fl.getFingerIndex(5)); + assertEquals(6, finger.index); + assertEquals(Integer.valueOf(6), finger.node.item); + + finger = fl.get(fl.getFingerIndex(6)); + assertEquals(6, finger.index); + assertEquals(Integer.valueOf(6), finger.node.item); + } + +// @Test + public void insertFingerAtFront() { + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(0)), 0)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(1)), 1)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(3)), 3)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(6)), 6)); + + Finger insertionFinger = new Finger<>(new Node<>(null), 0); + +// fl.insertFinger(insertionFinger); + + Finger finger = fl.get(fl.getFingerIndex(0)); + assertEquals(insertionFinger.index, finger.index); + + assertEquals(5, fl.size()); + } + +// @Test + public void insertFingerAtTail() { + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(2)), 2)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(4)), 4)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(5)), 5)); + + // Add end of finger list sentinel: + fl.fingerArray[3] = + new Finger<>(new Node(Integer.valueOf(100)), 10); + + Finger insertionFinger = new Finger<>(new Node<>(null), 6); + +// fl.insertFinger(insertionFinger); + + Finger finger = fl.get(fl.getFingerIndex(6)); + assertEquals(insertionFinger.index, finger.index); + + assertEquals(4, fl.size()); + } + +// @Test + public void insertFingerInBetween1() { + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(2)), 2)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(4)), 4)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(5)), 5)); + + Finger insertionFinger = new Finger<>(new Node<>(null), 4); + +// fl.insertFinger(insertionFinger); + + assertEquals(insertionFinger, fl.get(1)); + } + +// @Test + public void insertFingerInBetween2() { + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(2)), 2)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(4)), 4)); + fl.appendFinger(new Finger<>(new Node<>(Integer.valueOf(5)), 5)); + + Finger insertionFinger = new Finger<>(new Node<>(null), 3); + +// fl.insertFinger(insertionFinger); + + assertEquals(insertionFinger, fl.get(1)); + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedLinkedListTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedLinkedListTest.java new file mode 100644 index 0000000000..8f99256dc7 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedLinkedListTest.java @@ -0,0 +1,3061 @@ +/* + * 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 com.github.coderodde.util.IndexedLinkedList.BasicIterator; +import com.github.coderodde.util.IndexedLinkedList.EnhancedIterator; +import com.github.coderodde.util.IndexedLinkedList.Finger; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Before; +import org.junit.Test; + +public class IndexedLinkedListTest { + + private final IndexedLinkedList list = new IndexedLinkedList<>(); + + @Before + public void setUp() { + list.clear(); + } + + @Test + public void addFirstLarge() { + List l = getIntegerList(1000); + + for (int i = 0; i < l.size(); i++) { + list.addFirst(l.get(i)); + } + + Collections.reverse(l); + assertTrue(listsEqual(list, l)); + } + + @Test + public void addAllAtIndexLarge() { + Random random = new Random(1003L); + List referenceList = new ArrayList<>(); + + for (int i = 0; i < 100; ++i) { + int index = random.nextInt(list.size() + 1); + List coll = getIntegerList(random.nextInt(100)); + list.addAll(index, coll); + referenceList.addAll(index, coll); + } + + assertTrue(listsEqual(list, referenceList)); + } + + @Test + public void constructAdd() { + List l = new IndexedLinkedList<>(Arrays.asList("a", "b", "c")); + + assertEquals(3, l.size()); + + assertEquals("a", l.get(0)); + assertEquals("b", l.get(1)); + assertEquals("c", l.get(2)); + } + + @Test + public void contains() { + assertFalse(list.contains(Integer.valueOf(1))); + assertFalse(list.contains(Integer.valueOf(2))); + assertFalse(list.contains(Integer.valueOf(3))); + + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + + list.addAll(Arrays.asList(1, 2, 3)); + + assertEquals(3, list.size()); + assertFalse(list.isEmpty()); + + assertTrue(list.contains(Integer.valueOf(1))); + assertTrue(list.contains(Integer.valueOf(2))); + assertTrue(list.contains(Integer.valueOf(3))); + } + + @Test + public void descendingIterator() { + list.addAll(Arrays.asList(1, 2, 3)); + Iterator iterator = list.descendingIterator(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(3), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(1), iterator.next()); + + assertFalse(iterator.hasNext()); + } + + @Test + public void descendingIteratorRemove1() { + list.addAll(Arrays.asList(1, 2, 3)); + Iterator iterator = list.descendingIterator(); + + iterator.next(); + iterator.remove(); + + assertEquals(2, list.size()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(1), iterator.next()); + + assertFalse(iterator.hasNext()); + } + + @Test + public void descendingIteratorForEachRemaining() { + list.addAll(Arrays.asList(1, 2, 3, -1, -2, -3)); + Iterator iterator = list.descendingIterator(); + + assertEquals(Integer.valueOf(-3), iterator.next()); + assertEquals(Integer.valueOf(-2), iterator.next()); + assertEquals(Integer.valueOf(-1), iterator.next()); + + class LocalConsumer implements Consumer { + + final List list = new ArrayList<>(); + + @Override + public void accept(Integer i) { + list.add(i); + } + } + + LocalConsumer consumer = new LocalConsumer(); + iterator.forEachRemaining(consumer); + + assertEquals(Integer.valueOf(3), consumer.list.get(0)); + assertEquals(Integer.valueOf(2), consumer.list.get(1)); + assertEquals(Integer.valueOf(1), consumer.list.get(2)); + } + + @Test + public void subListClearOnEmptyPrefix() { + list.addAll(getIntegerList(100)); + list.get(10); + list.subList(5, 100).clear(); + list.checkInvarant(); + } + + @Test + public void removeFirstUntilEmpty() { + list.addAll(getIntegerList(10)); + + while (!list.isEmpty()) { + list.removeFirst(); + list.checkInvarant(); + } + + list.checkInvarant(); + } + + @Test + public void moveFingerOutOfRemovalLocation() { + list.addAll(getIntegerList(16)); + list.fingerList.fingerArray[0] = + new Finger<>(list.last.prev.prev.prev, 12); + + list.fingerList.fingerArray[1] = new Finger<>(list.last.prev.prev, 13); + list.fingerList.fingerArray[2] = new Finger<>(list.last.prev, 14); + list.fingerList.fingerArray[3] = new Finger<>(list.last, 15); + + list.remove(12); + + Finger finger = list.fingerList.fingerArray[0]; + + assertEquals(Integer.valueOf(11), finger.node.item); + assertEquals(11, finger.index); + } + + @Test + public void removeIf() { + list.addAll(getIntegerList(10)); + list.removeIf((i) -> { + return i % 2 == 1; + }); + + list.checkInvarant(); + assertEquals(Arrays.asList(0, 2, 4, 6, 8), list); + } + + @Test + public void descendingIteratorRemove2() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5)); + + Iterator iter = list.descendingIterator(); + + iter.next(); + iter.remove(); + + assertEquals(Integer.valueOf(4), iter.next()); + iter.remove(); + + assertEquals(3, list.size()); + + assertEquals(Integer.valueOf(3), iter.next()); + iter.remove(); + + assertEquals(2, list.size()); + + assertEquals(Integer.valueOf(2), iter.next()); + assertEquals(Integer.valueOf(1), iter.next()); + + iter.remove(); + + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(2), list.get(0)); + } + + @Test(expected = NoSuchElementException.class) + public void elementThrowsOnEmptyList() { + list.element(); + } + + @Test + public void element() { + list.add(1); + list.add(2); + + assertEquals(Integer.valueOf(1), list.element()); + + list.remove(); + + assertEquals(Integer.valueOf(2), list.element()); + } + + @Test + public void listEquals() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + List otherList = Arrays.asList(1, 2, 3, 4); + + assertTrue(list.equals(otherList)); + + list.remove(Integer.valueOf(3)); + + assertFalse(list.equals(otherList)); + + assertFalse(list.equals(null)); + assertTrue(list.equals(list)); + + Set set = new HashSet<>(list); + + assertFalse(list.equals(set)); + + list.clear(); + list.addAll(Arrays.asList(0, 1, 2, 3)); + otherList = Arrays.asList(0, 1, 4, 3); + + assertFalse(list.equals(otherList)); + } + + class DummyList extends ArrayList { + private final class DummyIterator implements Iterator { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return Integer.valueOf(0); + } + } + + public Iterator iterator() { + return new DummyIterator(); + } + + public int size() { + return 2; + } + } + + @Test + public void sublistClear1() { + list.addAll(getIntegerList(100)); + List sublist = list.subList(49, 51); + assertEquals(2, sublist.size()); + sublist.clear(); + assertEquals(98, list.size()); + assertEquals(0, sublist.size()); + } + + @Test + public void sublistClear2() { + int fromIndex = 10; + int toIndex = 990; + + List referenceList = getIntegerList(1000); + list.addAll(referenceList); + referenceList.subList(fromIndex, toIndex).clear(); + list.subList(fromIndex, toIndex).clear(); + list.checkInvarant(); + assertEquals(referenceList, list); + } + + private void checkSubList(int size, int fromIndex, int toIndex) { + List referenceList = getIntegerList(size); + list.addAll(referenceList); + referenceList.subList(fromIndex, toIndex).clear(); + list.subList(fromIndex, toIndex).clear(); + list.checkInvarant(); + assertEquals(referenceList, list); + } + + @Test + public void sublistClear3() { + int size = 1_000_000; + int fromIndex = 10; + int toIndex = 999_990; + checkSubList(size, fromIndex, toIndex); + } + + @Test + public void sublistClear4() { + int size = 1_000; + int fromIndex = 10; + int toIndex = 500; + checkSubList(size, fromIndex, toIndex); + } + + @Test + public void sublistClear5() { + int size = 100; + int fromIndex = 10; + int toIndex = 90; + checkSubList(size, fromIndex, toIndex); + } + + @Test + public void sublistClearLeftOfSmall() { + list.add(1); + list.add(2); + + list.subList(0, 1).clear(); + + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(2), list.get(0)); + list.checkInvarant(); + } + + @Test + public void sublistClearRightOfSmall() { + list.add(1); + list.add(2); + + list.subList(1, 2).clear(); + list.checkInvarant(); + + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(1), list.get(0)); + } + + @Test + public void sublistClearRightOfSmall2() { + List referenceList = new ArrayList<>(getIntegerList(20)); + list.addAll(referenceList); + + list.subList(0, 5).clear(); + list.checkInvarant(); + referenceList.subList(0, 5).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void debugClear1() { + list.addAll(getIntegerList(12)); + list.subList(4, 9).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(0, 1, 2, 3, 9, 10, 11), list); + } + + @Test + public void debugClear2() { + list.addAll(getIntegerList(10)); + list.subList(0, 4).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(4, 5, 6, 7, 8, 9), list); + } + + @Test + public void subListClear2Fingers3Nodes_1() { + list.addAll(Arrays.asList(1, 2, 3)); + list.subList(0, 1).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(2, 3), list); + } + + @Test + public void sublistClear6() { + list.addAll(getIntegerList(1000)); + list.subList(70, 1000).clear(); + } + + @Test + public void bruteForceSublistClearOnSmallLists() { + Random random = new Random(26L); + + for (int i = 0; i < 200; ++i) { + int size = 1 + random.nextInt(15); + List referenceList = new ArrayList<>(getIntegerList(size)); + list.clear(); + list.addAll(referenceList); + + int fromIndex = random.nextInt(size); + int toIndex = Math.min(size, fromIndex + random.nextInt(size)); + + list.subList(fromIndex, toIndex).clear(); + referenceList.subList(fromIndex, toIndex).clear(); + + list.checkInvarant(); + assertEquals(referenceList, list); + } + } + + @Test + public void optmize() { + list.addAll(getIntegerList(100)); + Random random = new Random(100L); + + for (int i = 0; i < 50; ++i) { + list.get(random.nextInt(list.size())); + } + + list.checkInvarant(); + list.optimize(); + list.checkInvarant(); + } + + @Test + public void removeFromRange() { + list.addAll(getIntegerList(10)); + List referenceList = new ArrayList<>(list); + + List subList1 = list.subList(1, 9); + List subList2 = referenceList.subList(1, 9); + + assertEquals(subList2, subList1); + + // Remove from ArrayList: + subList2.remove(Integer.valueOf(0)); + + // Remove from IndexedLinkedList: + subList1.remove(Integer.valueOf(0)); + + assertEquals(subList2, subList1); + + assertEquals(8, subList1.size()); + assertEquals(10, list.size()); + + subList1.remove(Integer.valueOf(5)); + subList2.remove(Integer.valueOf(5)); + + assertEquals(subList2, subList1); + + assertEquals(7, subList1.size()); + assertEquals(9, list.size()); + + assertEquals(referenceList, list); + } + + @Test + public void sort() { + Random random = new Random(1L); + + for (int i = 0; i < 100; ++i) { + list.add(random.nextInt(70)); + } + + List referenceList = new ArrayList<>(list); + + Comparator comp = (i1, i2) -> { + return Integer.compare(i1, i2); + }; + + list.checkInvarant(); + list.sort(comp); + list.checkInvarant(); + + referenceList.sort(comp); + assertEquals(referenceList, list); + } + + @Test + public void sortSubLists() { + Random random = new Random(12L); + + for (int i = 0; i < 10; ++i) { + list.clear(); + list.addAll(getIntegerList(500)); + Collections.shuffle(list, random); + List referenceList = new ArrayList<>(list); + + int f = random.nextInt(list.size() + 1); + int t = random.nextInt(list.size() - 1); + + int fromIndex = Math.min(f, t); + int toIndex = Math.max(f, t); + + Comparator cmp = Integer::compare; + + list.subList(fromIndex, toIndex).sort(cmp); + referenceList.subList(fromIndex, toIndex).sort(cmp); + + assertEquals(referenceList, list); + } + } + + @Test + public void sortSubListOfSubList() { + list.addAll(Arrays.asList(4, 1, 0, 2, 6, 8, 4, 1, 3)); + List referenceList = new ArrayList<>(list); + Comparator cmp = Integer::compare; + list.subList(1, 7).subList(1, 4).sort(cmp); + referenceList.subList(1, 7).subList(1, 4).sort(cmp); + assertEquals(referenceList, list); + } + + @Test + public void subListAdd() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).add(Integer.valueOf(1000)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 2, 1, 4, 1000, 5), list); + } + + @Test + public void subListAddInt() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).add(1, Integer.valueOf(1000)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 2, 1000, 1, 4, 5), list); + } + + @Test + public void subListAddAll() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).addAll(Arrays.asList(10, 11)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 2, 1, 4, 10, 11, 5), list); + } + + @Test + public void subListAddAllInt() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).addAll(0, Arrays.asList(10, 11)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 10, 11, 2, 1, 4, 5), list); + } + + @Test + public void subListContains() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5, 8, 7)); + + assertTrue(list.subList(1, 5).contains(2)); + assertTrue(list.subList(1, 5).contains(1)); + assertTrue(list.subList(1, 5).contains(4)); + assertTrue(list.subList(1, 5).contains(5)); + + assertFalse(list.subList(1, 5).contains(3)); + assertFalse(list.subList(1, 5).contains(8)); + assertFalse(list.subList(1, 5).contains(7)); + } + + @Test + public void subListContainsAll() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5, 8, 7)); + + assertTrue(list.subList(1, 5).containsAll(Arrays.asList(2, 4, 1, 5))); + + assertFalse(list.subList(1, 5) + .containsAll(Arrays.asList(2, 4, 1, 5, 3))); + + assertFalse(list.subList(1, 5) + .containsAll(Arrays.asList(2, 4, 1, 5, 8))); + + assertFalse(list.subList(1, 5) + .containsAll(Arrays.asList(2, 4, 1, 5, 7))); + } + + @Test + public void containsAll() { + list.addAll(Arrays.asList(4, 1, 8, 7, 5, 6)); + + assertFalse(list.containsAll(Arrays.asList(8, 7, 3))); + assertFalse(list.containsAll(Arrays.asList(1, 4, 3))); + assertFalse(list.containsAll(Arrays.asList(-1))); + + list.addAll(Arrays.asList(4, 1, 8, 7, 5, 6)); + + assertTrue(list.containsAll(Arrays.asList(8, 7))); + assertTrue(list.containsAll(Arrays.asList())); + assertTrue(list.containsAll(Arrays.asList(8, 1, 4, 7, 6, 5))); + } + + @Test + public void hashCode2() { + List referenceList = new ArrayList<>(); + + list.add(null); + referenceList.add(null); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(1); + referenceList.add(1); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(5); + referenceList.add(5); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(7); + referenceList.add(7); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(null); + referenceList.add(null); + assertEquals(referenceList.hashCode(), list.hashCode()); + } + + @Test + public void removeAll() { + list.addAll(Arrays.asList(4, 1, 8, 9, 5, 1, -1, 5, 2, 3, 0)); + list.removeAll(Arrays.asList(1, -1, 5)); + // list = <4, 8, 9, 2, 3, 0> + assertEquals(Arrays.asList(4, 8, 9, 2, 3, 0), list); + + list.removeAll(Arrays.asList(-2, 8, 0)); + // list = <4, 9, 2, 3> + assertEquals(Arrays.asList(4, 9, 2, 3), list); + } + + @Test + public void replaceAll() { + list.addAll(Arrays.asList(3, 2, 1)); + list.replaceAll((i) -> { + return i * 3 + 1; + }); + + assertEquals(Arrays.asList(10, 7, 4), list); + } + + @Test + public void retainAll() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); + + list.retainAll(Arrays.asList(2, 3, 5, 7)); + + assertEquals(Arrays.asList(2, 3, 5), list); + + list.retainAll(Arrays.asList(3)); + + assertEquals(Arrays.asList(3), list); + + list.retainAll(Arrays.asList(0)); + + assertTrue(list.isEmpty()); + } + + @Test + public void toArrayGeneric() { + list.addAll(Arrays.asList(3, 1, 2, 5, 4)); + + Integer[] array = new Integer[7]; + + array[5] = Integer.valueOf(10); + array[6] = Integer.valueOf(11); + + Integer[] cloneArray = list.toArray(array); + + assertTrue(cloneArray == array); + assertNull(cloneArray[5]); + assertEquals(Integer.valueOf(11), cloneArray[6]); + + array = new Integer[3]; + + cloneArray = list.toArray(array); + + assertFalse(cloneArray == array); + + assertEquals(5, cloneArray.length); + + for (int i = 0; i < cloneArray.length; ++i) { + assertEquals(list.get(i), cloneArray[i]); + } + } + + @Test + public void toString2() { + list.addAll(Arrays.asList(1, 11, 111)); + assertEquals("[1, 11, 111]", list.toString()); + } + + @Test + public void subListToString() { + list.addAll(Arrays.asList(0, 2, 22, 222, 0)); + assertEquals("[2, 22, 222]", list.subList(1, 4).toString()); + } + + @Test(expected = IllegalStateException.class) + public void listIteratorSetAddThrows() { + list.addAll(getIntegerList(10)); + ListIterator listIterator = list.listIterator(3); + + listIterator.add(100); + listIterator.set(-100); + } + + @Test(expected = IllegalStateException.class) + public void subListIteratorSetAddThrows() { + list.addAll(getIntegerList(10)); + ListIterator listIterator = list.subList(4, 9).listIterator(3); + + listIterator.add(100); + listIterator.set(-100); + } + + @Test(expected = IllegalStateException.class) + public void listIteratorRemoveWithouttNextPreviousThrows() { + list.addAll(getIntegerList(5)); + ListIterator iter = list.listIterator(1); + iter.remove(); + } + + @Test(expected = IllegalStateException.class) + public void subListIteratorRemoveWithouttNextPreviousThrows() { + list.addAll(getIntegerList(8)); + List subList = list.subList(1, 6); + ListIterator iter = subList.listIterator(5); + iter.remove(); + } + + @Test + public void listIteratorSetAdd() { + list.addAll(getIntegerList(5)); + ListIterator listIterator = list.listIterator(2); + // list = <0, 1, 2, 3, 4> + // iter: | + + listIterator.add(100); + // list = <0, 1, 100, 2, 3, 4> + assertEquals(Integer.valueOf(2), listIterator.next()); + listIterator.set(-100); + // list = <0, 1, 100, -100, 3, 4> + + assertEquals(Arrays.asList(0, 1, 100, -100, 3, 4), list); + // list = <0, 1, 100, -100, 3, 4> + + listIterator = list.listIterator(4); + // list = <0, 1, 100, -100, 3, 4> + // iter: | + listIterator.add(1000); + // list = <0, 1, 100, -100, 1000, 3, 4> + assertEquals(Arrays.asList(0, 1, 100, -100, 1000, 3, 4), list); + + assertEquals(Integer.valueOf(1000), listIterator.previous()); + listIterator.set(-1000); + // list = <0, 1, 100, -1000, 1000, 3, 4> + + assertEquals( + Arrays.asList(0, 1, 100, -100, -1000, 3, 4), + list); + } + + @Test + public void subListIteratorSetAdd() { + list.addAll(getIntegerList(8)); + List subList = list.subList(1, 6); + ListIterator listIterator = subList.listIterator(2); + // subList = <1, 2, 3, 4, 5> + // iter: | + + listIterator.add(100); + assertEquals(Arrays.asList(1, 2, 100, 3, 4, 5), subList); + // subList = <1, 2, 100, 3, 4, 5> + + assertEquals(Integer.valueOf(3), listIterator.next()); + listIterator.set(-100); + // subList = <1, 2, 100, -100, 4, 5> + // iter: | + + assertEquals(Arrays.asList(1, 2, 100, -100, 4, 5), subList); + assertEquals(Integer.valueOf(4), listIterator.next()); + assertEquals(Integer.valueOf(5), listIterator.next()); + + listIterator.add(1000); + + // list = <1, 2, 100, -100, 4, 5, 1000> + assertEquals(Arrays.asList(1, 2, 100, -100, 4, 5, 1000), subList); + + assertEquals(Integer.valueOf(1000), listIterator.previous()); + listIterator.set(-1000); + // list = <1, 2, 100, -100, 4, 5, -1000> + + assertEquals(Arrays.asList(1, 2, 100, -100, 4, 5, -1000), subList); + } + + @Test(expected = IllegalStateException.class) + public void listIteratorThrowsOnSetAfterRemove() { + list.addAll(getIntegerList(8)); + ListIterator iterator = list.listIterator(2); + iterator.previous(); + iterator.remove(); + iterator.set(1000); + } + + @Test(expected = IllegalStateException.class) + public void subListIteratorThrowsOnSetAfterRemove() { + list.addAll(getIntegerList(8)); + List subList = list.subList(1, 7); + + ListIterator iterator = subList.listIterator(2); + iterator.previous(); + iterator.remove(); + iterator.set(1000); + } + + @Test + public void debugChainResolve() { + list.addAll(getIntegerList(9_999)); + list.checkInvarant(); + list.subList(5, list.size() - 5).clear(); + list.checkInvarant(); + } + + @Test + public void debugChainResolve2() { + list.addAll(getIntegerList(9_999)); + list.checkInvarant(); + list.subList(5, list.size()).clear(); + list.checkInvarant(); + } + + @Test + public void debugChainResolve3() { + list.addAll(getIntegerList(9_999)); + list.checkInvarant(); + list.subList(0, list.size() - 5).clear(); + list.checkInvarant(); + } + + @Test + public void addFingersAfterAppendAll() { + list.addAll(getIntegerList(9_990)); + assertEquals(100, list.getFingerListSize()); + list.addAll(Arrays.asList(-1, -2)); + assertEquals(100, list.getFingerListSize()); + } + + @Test + public void canUseListAsMapKey() { + List l = new ArrayList<>(Arrays.asList(1, 5, 3)); + list.addAll(l); + + Map, Integer> map = new HashMap<>(); + + map.put(l, 100); + + assertEquals(Integer.valueOf(100), map.get(list)); + + l = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6)); + list.clear(); + list.addAll(l); + + List subList1 = l.subList(1, 4); + List subList2 = list.subList(1, 4); + + map.put(subList1, Integer.valueOf(200)); + assertEquals(Integer.valueOf(200), map.get(subList2)); + } + + @Test + public void spliteratorOverSubList() { + list.addAll(getIntegerList(10)); + List subList = list.subList(1, 9); + + Spliterator spliterator = subList.spliterator(); + + class MyConsumer implements Consumer { + final List data = new ArrayList<>(); + + @Override + public void accept(Integer i) { + data.add(i); + } + } + + MyConsumer myConsumer = new MyConsumer(); + + assertEquals(8L, spliterator.getExactSizeIfKnown()); + assertEquals(8L, spliterator.estimateSize()); + + spliterator.tryAdvance(myConsumer); + assertEquals(1, myConsumer.data.size()); + + spliterator.tryAdvance(myConsumer); + assertEquals(2, myConsumer.data.size()); + + spliterator.tryAdvance(myConsumer); + assertEquals(3, myConsumer.data.size()); + + spliterator.tryAdvance(myConsumer); + assertEquals(4, myConsumer.data.size()); + + assertEquals(Arrays.asList(1, 2, 3, 4), myConsumer.data); + + spliterator.forEachRemaining(myConsumer); + assertEquals(8, myConsumer.data.size()); + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8), myConsumer.data); + } + + @Test + public void subListForEach() { + list.addAll(Arrays.asList(4, 2, 1, 3, 1, 2, 5, 8)); + + class MyConsumer implements Consumer { + + final List data = new ArrayList<>(); + + @Override + public void accept(Integer t) { + data.add(t); + } + } + + MyConsumer myConsumer = new MyConsumer(); + list.subList(1, 6).forEach(myConsumer); + + assertEquals(Integer.valueOf(2), myConsumer.data.get(0)); + assertEquals(Integer.valueOf(1), myConsumer.data.get(1)); + assertEquals(Integer.valueOf(3), myConsumer.data.get(2)); + assertEquals(Integer.valueOf(1), myConsumer.data.get(3)); + assertEquals(Integer.valueOf(2), myConsumer.data.get(4)); + } + + @Test + public void subListGet() { + list.addAll(Arrays.asList(4, 2, 8, 0, 9)); + List subList = list.subList(1, 4); + assertEquals(Integer.valueOf(2), subList.get(0)); + assertEquals(Integer.valueOf(8), subList.get(1)); + assertEquals(Integer.valueOf(0), subList.get(2)); + } + + @Test + public void subListIndexOf() { + list.addAll(Arrays.asList(5, 1, 9, 10, 2, 3, 7, 6)); + List subList = list.subList(2, 6); // <9, 10, 2, 3> + + assertEquals(-1, subList.indexOf(5)); + assertEquals(-1, subList.indexOf(1)); + assertEquals(-1, subList.indexOf(7)); + assertEquals(-1, subList.indexOf(6)); + + assertEquals(0, subList.indexOf(9)); + assertEquals(1, subList.indexOf(10)); + assertEquals(2, subList.indexOf(2)); + assertEquals(3, subList.indexOf(3)); + } + + @Test + public void subListIsEmpty() { + List subList = list.subList(0, 0); + assertTrue(subList.isEmpty()); + subList.add(1); + assertFalse(subList.isEmpty()); + subList.remove(0); + assertTrue(subList.isEmpty()); + } + + @Test + public void subListIterator() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); + List subList = list.subList(1, 5); // <2, 3, 4, 5> + Iterator iterator = subList.iterator(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + + iterator.remove(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(3), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(4), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(5), iterator.next()); + + iterator.remove(); + + assertFalse(iterator.hasNext()); + + assertEquals(Arrays.asList(3, 4), subList); + assertEquals(Arrays.asList(1, 3, 4, 6), list); + } + + @Test + public void subListLastIndex() { + list.addAll(Arrays.asList(1, 2, 3, 2, 3, 2, 1)); + List subList = list.subList(2, 7); // <3, 2, 3, 2, 1> + + assertEquals(4, subList.lastIndexOf(1)); + assertEquals(3, subList.lastIndexOf(2)); + assertEquals(2, subList.lastIndexOf(3)); + assertEquals(-1, subList.lastIndexOf(10)); + } + + @Test + public void subListListIterator() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + List subList = list.subList(2, 8); // <3, 4, 5, 6, 7, 8> + + assertEquals(Arrays.asList(3, 4, 5, 6, 7, 8), subList); + + ListIterator iterator = subList.listIterator(2); + + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasPrevious()); + + assertEquals(Integer.valueOf(5), iterator.next()); + + iterator.remove(); // subList = <3, 4, 6, 7, 8> + + assertEquals(Arrays.asList(1, 2, 3, 4, 6, 7, 8, 9), list); + assertEquals(Arrays.asList(3, 4, 6, 7, 8), subList); + + assertEquals(Integer.valueOf(4), iterator.previous()); + + iterator.remove(); // subList = <3, 6, 7, 8> + + assertEquals(Integer.valueOf(6), iterator.next()); + assertEquals(Integer.valueOf(7), iterator.next()); + assertEquals(Integer.valueOf(8), iterator.next()); + + assertFalse(iterator.hasNext()); + assertTrue(iterator.hasPrevious()); + } + + @Test + public void subListRemoveObject() { + list.addAll(Arrays.asList(3, 1, 2, 4, null, 5, null, 8, 1)); + List subList = list.subList(2, 8); + // subList = <2, 4, null, 5, null, 8> + + subList.remove(null); + + assertEquals(Arrays.asList(2, 4, 5, null, 8), subList); + assertEquals(Arrays.asList(3, 1, 2, 4, 5, null, 8, 1), list); + + subList.remove(Integer.valueOf(5)); + + assertEquals(Arrays.asList(2, 4, null, 8), subList); + assertEquals(Arrays.asList(3, 1, 2, 4, null, 8, 1), list); + + subList.remove(null); + + assertEquals(Arrays.asList(2, 4, 8), subList); + assertEquals(Arrays.asList(3, 1, 2, 4, 8, 1), list); + } + + @Test + public void subListRemoveInt() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); + List subList = list.subList(3, 5); + + assertEquals(Arrays.asList(4, 5), subList); + + subList.remove(1); + + assertEquals(Arrays.asList(4), subList); + assertEquals(Arrays.asList(1, 2, 3, 4, 6, 7), list); + + subList.remove(0); + + assertEquals(Arrays.asList(), subList); + assertEquals(Arrays.asList(1, 2, 3, 6, 7), list); + } + + @Test + public void subListRemoveAll() { + list.addAll(Arrays.asList(4, 1, 2, 9, 8, 7, 5, 2, 8, 10, 11)); + List subList = list.subList(1, 9); + // subList = <1, 2, 9, 8, 7, 5, 2, 8> + + subList.removeAll(Arrays.asList(1)); + // subList = <2, 9, 8, 7, 5, 2, 8> + list.checkInvarant(); + + assertEquals(Arrays.asList(2, 9, 8, 7, 5, 2, 8), subList); + + subList.removeAll(Arrays.asList(2)); + // subList = <9, 8, 7, 5, 8> + list.checkInvarant(); + + assertEquals(Arrays.asList(9, 8, 7, 5, 8), subList); + + subList.removeAll(Arrays.asList(8, 7)); + // subList = <9, 5> + list.checkInvarant(); + + assertEquals(Arrays.asList(9, 5), subList); + + subList.removeAll(Arrays.asList(9, 5)); // subList = <> + list.checkInvarant(); + + assertEquals(Arrays.asList(), subList); + assertTrue(subList.isEmpty()); + assertFalse(list.isEmpty()); + } + + @Test + public void subListRemoveIf() { + list.addAll(Arrays.asList(1, 5, 2, 3, 4, 8, 9, 10, 4)); + List subList = list.subList(2, 7); + // subList = <2, 3, 4, 8, 9> + + subList.removeIf((i) -> { + return i % 2 == 1; // Remove odd integers. + }); + + // subList = <2, 4, 8> + assertEquals(Arrays.asList(2, 4, 8), subList); + assertEquals(Arrays.asList(1, 5, 2, 4, 8, 10, 4), list); + } + + @Test + public void subListReplaceAll() { + list.addAll(Arrays.asList(4, 4, 5, 1, 8, 2, 9, 0, 1, 3)); + List subList = list.subList(2, 8); + // subList = <5, 1, 8, 2, 9, 0> + subList.replaceAll((i) -> { return i * 2; }); + + assertEquals(Arrays.asList(10, 2, 16, 4, 18, 0), subList); + assertEquals(Arrays.asList(4, 4, 10, 2, 16, 4, 18, 0, 1, 3), list); + } + + @Test + public void subListRetainAll() { + list.addAll(Arrays.asList(3, 10, 8, 2, 5, 4, 1, 0, 7, 4)); + List subList = list.subList(2, 8); + // subList = <8, 2, 5, 4, 1, 0> + subList.retainAll(Arrays.asList(4, 2, 5, 11)); + list.checkInvarant(); + + // subList = <2, 5, 4> + assertEquals(Arrays.asList(2, 5, 4), subList); + assertEquals(Arrays.asList(3, 10, 2, 5, 4, 7, 4), list); + + list.clear(); + + list.addAll(Arrays.asList(1, 3, 2, 1, 2, 3, 3, 1, 2, 0, 0)); + subList = list.subList(1, 6); + // subList = <3, 2, 1, 2, 3> + + subList.retainAll(Arrays.asList(3, 4, 5)); + // subList = <3, 3> + assertEquals(Arrays.asList(3, 3), subList); + assertEquals(Arrays.asList(1, 3, 3, 3, 1, 2, 0, 0), list); + } + + @Test + public void subListSet() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5)); + List subList = list.subList(1, 4); + // subList = <2, 3, 4> + subList.set(0, 10); + subList.set(2, 11); + + assertEquals(Arrays.asList(10, 3, 11), subList); + assertEquals(Arrays.asList(1, 10, 3, 11, 5), list); + } + + @Test + public void subListSort() { + Random random = new Random(); + + for (int i = 0; i < 100; ++i) { + int value = random.nextInt(100) % 75; + list.add(value); + } + + Collections.shuffle(list); + List referenceList = new ArrayList<>(list); + + list.subList(10, 80).sort(Integer::compare); + referenceList.subList(10, 80).sort(Integer::compare); + + assertEquals(referenceList, list); + } + + @Test + public void subListToArray() { + list.addAll(getIntegerList(15)); + List subList = list.subList(5, 10); + + Object[] array = subList.toArray(); + + for (int i = 0; i < subList.size(); ++i) { + Integer listInteger = list.get(i + 5); + Integer arrayInteger = (Integer) array[i]; + assertEquals(listInteger, arrayInteger); + } + } + + @Test + public void subListToArrayGenerator() { + list.addAll(getIntegerList(20)); + List subList = list.subList(10, 16); + Integer[] array = subList.toArray(Integer[]::new); + + assertEquals(Integer.valueOf(10), array[0]); + assertEquals(Integer.valueOf(11), array[1]); + assertEquals(Integer.valueOf(12), array[2]); + assertEquals(Integer.valueOf(13), array[3]); + assertEquals(Integer.valueOf(14), array[4]); + assertEquals(Integer.valueOf(15), array[5]); + } + + @Test + public void listToArrayGenerator() { + list.addAll(Arrays.asList(4, 1, 3, 2, 5)); + Integer[] array = list.toArray(Integer[]::new); + + assertEquals(5, array.length); + assertEquals(Integer.valueOf(4), array[0]); + assertEquals(Integer.valueOf(1), array[1]); + assertEquals(Integer.valueOf(3), array[2]); + assertEquals(Integer.valueOf(2), array[3]); + assertEquals(Integer.valueOf(5), array[4]); + } + + @Test + public void subListToArrayGeneric() { + list.addAll(getIntegerList(15)); + List subList = list.subList(5, 10); + + Integer[] array = new Integer[5]; + Integer[] resultArray = subList.toArray(array); + + assertTrue(array == resultArray); + + array = new Integer[7]; + resultArray = subList.toArray(array); + + assertTrue(array == resultArray); + + assertNull(resultArray[5]); + + array = new Integer[3]; + resultArray = subList.toArray(array); + + assertFalse(array == resultArray); + assertEquals(5, resultArray.length); + } + + @Test + public void clone2() { + list.addAll(Arrays.asList(4, 1, 3, 2)); + assertEquals(Arrays.asList(4, 1, 3, 2), list.clone()); + } + + @Test + public void subListClone() { + list.addAll(Arrays.asList(4, 1, 8, 9, 5, 6, 7, 0, 1)); + List subList1 = list.subList(1, list.size() - 1); + + IndexedLinkedList.EnhancedSubList subList2 = + (IndexedLinkedList.EnhancedSubList) + subList1.subList(1, subList1.size() - 1); + + List clone = (List) subList2.clone(); + assertEquals(Arrays.asList(8, 9, 5, 6, 7), clone); + } + + @Test + public void debugContractFingerArrayIfNeeded() { + list.addAll(getIntegerList(15_877)); // 128 finger spots occupied. + assertEquals(127, list.getFingerListSize()); + list.subList(49, list.size()).clear(); // 8 finger spots occupied. + assertEquals(7, list.getFingerListSize()); + } + + @Test + public void bruteForceSublistClearOnLargeLists() { + Random random = new Random(26L); + + for (int i = 0; i < 30; ++i) { + int size = 1 + random.nextInt(5_000); + List referenceList = new ArrayList<>(getIntegerList(size)); + list.clear(); + list.addAll(referenceList); + + int f = random.nextInt(size); + int t = random.nextInt(size); + int fromIndex = Math.min(f, t); + int toIndex = Math.max(f, t); + + list.subList(fromIndex, toIndex).clear(); + referenceList.subList(fromIndex, toIndex).clear(); + + list.checkInvarant(); + assertEquals(referenceList, list); + } + } + +// @Test(expected = IllegalStateException.class) + public void listEqualsThrowsOnBadIterator() { + DummyList dummyList = new DummyList(); + list.addAll(Arrays.asList(0, 0)); + list.equals(dummyList); + } + + @Test + public void offer() { + assertTrue(list.equals(Arrays.asList())); + + list.offer(1); + + assertTrue(list.equals(Arrays.asList(1))); + + list.offer(2); + + assertTrue(list.equals(Arrays.asList(1, 2))); + } + + @Test + public void offerFirst() { + assertTrue(list.equals(Arrays.asList())); + + list.offerFirst(1); + + assertTrue(list.equals(Arrays.asList(1))); + + list.offerFirst(2); + + assertTrue(list.equals(Arrays.asList(2, 1))); + } + + @Test + public void offerLast() { + assertTrue(list.equals(Arrays.asList())); + + list.offerLast(1); + + assertTrue(list.equals(Arrays.asList(1))); + + list.offerLast(2); + + assertTrue(list.equals(Arrays.asList(1, 2))); + } + + @Test + public void peek() { + assertNull(list.peek()); + + list.addLast(0); + + assertEquals(Integer.valueOf(0), list.peek()); + + list.addLast(1); + + assertEquals(Integer.valueOf(0), list.peek()); + + list.addFirst(Integer.valueOf(-1)); + + assertEquals(Integer.valueOf(-1), list.peek()); + } + + @Test + public void peekFirst() { + assertNull(list.peek()); + + list.addLast(0); + + assertEquals(Integer.valueOf(0), list.peekFirst()); + + list.addFirst(1); + + assertEquals(Integer.valueOf(1), list.peekFirst()); + + list.addFirst(Integer.valueOf(-1)); + + assertEquals(Integer.valueOf(-1), list.peekFirst()); + } + + @Test + public void peekLast() { + assertNull(list.peek()); + + list.addLast(0); + + assertEquals(Integer.valueOf(0), list.peekLast()); + + list.addLast(1); + + assertEquals(Integer.valueOf(1), list.peekLast()); + + list.addLast(2); + + assertEquals(Integer.valueOf(2), list.peekLast()); + } + + @Test + public void poll() { + assertNull(list.poll()); + + list.addAll(Arrays.asList(1, 2, 3)); + + assertEquals(Integer.valueOf(1), list.poll()); + assertEquals(Integer.valueOf(2), list.poll()); + assertEquals(Integer.valueOf(3), list.poll()); + } + + @Test + public void pollFirst() { + assertNull(list.pollFirst()); + + list.addAll(Arrays.asList(1, 2, 3)); + + assertEquals(Integer.valueOf(1), list.pollFirst()); + assertEquals(Integer.valueOf(2), list.pollFirst()); + assertEquals(Integer.valueOf(3), list.pollFirst()); + } + + @Test + public void pollLast() { + assertNull(list.pollLast()); + + list.addAll(Arrays.asList(1, 2, 3)); + + assertEquals(Integer.valueOf(3), list.pollLast()); + assertEquals(Integer.valueOf(2), list.pollLast()); + assertEquals(Integer.valueOf(1), list.pollLast()); + } + + @Test(expected = NoSuchElementException.class) + public void removeFirstThrowsOnEmptyList() { + list.removeFirst(); + } + + @Test + public void pop() { + list.addAll(Arrays.asList(1, 2, 3)); + + assertEquals(Integer.valueOf(1), list.pop()); + assertEquals(Integer.valueOf(2), list.pop()); + assertEquals(Integer.valueOf(3), list.pop()); + } + + @Test + public void push() { + list.push(1); + list.push(2); + list.push(3); + + assertTrue(list.equals(Arrays.asList(3, 2, 1))); + } + + class BadList extends IndexedLinkedList { + + class BadListIterator implements Iterator { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return Integer.valueOf(3); + } + } + + @Override + public Iterator iterator() { + return new BadListIterator(); + }; + + public int size() { + return 2; + } + } + + // TODO: deal with the equals(Object)! +// @Test(expected = IllegalStateException.class) +// public void badThisIterator() { +// List arrayList = Arrays.asList(3, 3); +// BadList badList = new BadList(); +// badList.addAll(Arrays.asList(3, 3)); +// badList.equals(arrayList); +// } + + @Test + public void removeFirstOccurrenceOfNull() { + list.addAll(Arrays.asList(1, 2, null, 4, null, 6)); + + assertTrue(list.removeFirstOccurrence(null)); + + // Remove the last null value: + list.set(3, 10); + + assertFalse(list.removeFirstOccurrence(null)); + } + + @Test + public void removeLastOccurrenceOfNull() { + list.addAll(Arrays.asList(1, 2, null, 4, null, 6)); + + assertTrue(list.removeLastOccurrence(null)); + + // Remove the last null value: + list.set(2, 10); + + assertFalse(list.removeLastOccurrence(null)); + } + + @Test + public void appendAll() { + list.addAll(Arrays.asList(0, 1, 2)); + + List arrayList = new ArrayList<>(); + + for (int i = 3; i < 20_000; i++) { + arrayList.add(i); + } + + list.addAll(arrayList); + + for (int i = 0; i < 20_000; i++) { + assertEquals(Integer.valueOf(i), list.get(i)); + } + } + + @Test + public void smallListRemoveFirstFinger() { + list.add(0); + list.add(1); + list.remove(0); + } + + @Test + public void smallListRemoveSecondFinger() { + list.add(0); + list.add(1); + list.remove(1); + } + + @Test + public void prependAll() { + List l = new ArrayList<>(); + + for (int i = 0; i < 10_000; i++) { + l.add(i); + } + + list.addAll(l); + + l = new ArrayList<>(); + + for (int i = 10_000; i < 20_000; i++) { + l.add(i); + } + + list.addAll(0, l); + + int index = 0; + + for (int i = 10_000; i < 20_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + + for (int i = 0; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + } + + @Test + public void insertAll() { + for (int i = 0; i < 20_000; i++) { + list.add(i); + } + + List arrayList = new ArrayList<>(10_000); + + for (int i = 20_000; i < 30_000; i++) { + arrayList.add(i); + } + + list.addAll(10_000, arrayList); + + int index = 0; + + for (int i = 0; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + + for (int i = 20_000; i < 30_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + + for (int i = 10_000; i < 20_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + } + + @Test(expected = IndexOutOfBoundsException.class) + public void checkPositionIndexThrowsOnNegativeIndex() { + list.add(-1, Integer.valueOf(0)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void checkPositionIndxThrowsOnTooLargeIndex() { + list.add(Integer.valueOf(0)); + + list.add(2, Integer.valueOf(1)); + } + + @Test(expected = NoSuchElementException.class) + public void removeLastThrowsOnEmptyList() { + list.removeLast(); + } + + @Test(expected = NoSuchElementException.class) + public void getFirstThrowsOnEmptyList() { + list.getFirst(); + } + + @Test + public void getFirst() { + list.addAll(Arrays.asList(10, 20)); + assertEquals(Integer.valueOf(10), list.getFirst()); + + list.removeFirst(); + + assertEquals(Integer.valueOf(20), list.getFirst()); + } + + @Test(expected = NoSuchElementException.class) + public void basicIteratorNextThrowsOnNoNext() { + list.add(1); + + Iterator iter = list.iterator(); + + try { + iter.next(); + } catch (Exception ex) { + fail("Should not get here."); + } + + iter.next(); + } + + @Test(expected = IllegalStateException.class) + public void basicIteratorThrowsOnDoubleRemove() { + list.add(1); + + Iterator iter = list.iterator(); + + try { + iter.next(); + iter.remove(); + } catch (Exception ex) { + fail("Should not get here."); + } + + iter.remove(); + } + + @Test + public void basicIteratorRemove1() { + list.add(1); + list.add(2); + + Iterator iter = list.iterator(); + + iter.next(); + iter.remove(); + iter.next(); + iter.remove(); + + assertEquals(0, list.size()); + assertFalse(iter.hasNext()); + } + + @Test + public void basicIteratorRemove2() { + list.add(1); + list.add(2); + list.add(3); + + Iterator iter = list.iterator(); + + iter.next(); + iter.remove(); + iter.next(); + iter.next(); + } + + @Test(expected = IllegalStateException.class) + public void enhancedIteratorThrowsOnSetAfterRemove() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + + ListIterator iter = list.listIterator(1); + + iter.next(); + iter.remove(); + iter.set(10); + } + + @Test(expected = ConcurrentModificationException.class) + public void basicIteratorForEachRemainingThrowsOnConcurrentModification() { + list.addAll(getIntegerList(1_000_000)); + + BasicIterator iter =(BasicIterator) list.iterator(); + iter.expectedModCount = -1000; + + iter.forEachRemaining((e) -> {}); + } + + @Test(expected = ConcurrentModificationException.class) + public void + enhancedIteratorForEachRemainingThrowsOnConcurrentModification() { + + list.addAll(getIntegerList(1_000_000)); + + EnhancedIterator iter = (EnhancedIterator) list.listIterator(); + iter.expectedModCount = -1; + + iter.forEachRemaining((e) -> {}); + } + + @Test(expected = ConcurrentModificationException.class) + public void spliteratorThrowsOnConcurrentModification() { + list.addAll(getIntegerList(50_000)); + + Spliterator spliterator = list.spliterator(); + list.add(50_000); + + spliterator.tryAdvance((e) -> {}); + } + + @Test + public void spliteratorTrySplitReturnsNullOnEmptyList() { + Spliterator spliterator = list.spliterator(); + + assertNull(spliterator.trySplit()); + } + + @Test + public void spliteratorTrySplitReturnsNullOnTooSmallList() { + list.addAll( + getIntegerList( + (int) + (IndexedLinkedList + .LinkedListSpliterator + .MINIMUM_BATCH_SIZE - 1L))); + + Spliterator spliterator = list.spliterator(); + + assertNull(spliterator.trySplit()); + } + + @Test + public void spliteratorHasCharasteristics() { + Spliterator spliterator = list.spliterator(); + + assertTrue(spliterator.hasCharacteristics(Spliterator.ORDERED)); + assertTrue(spliterator.hasCharacteristics(Spliterator.SIZED)); + assertTrue(spliterator.hasCharacteristics(Spliterator.SUBSIZED)); + + assertFalse(spliterator.hasCharacteristics(Spliterator.CONCURRENT)); + assertFalse(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + assertFalse(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + assertFalse(spliterator.hasCharacteristics(Spliterator.NONNULL)); + assertFalse(spliterator.hasCharacteristics(Spliterator.SORTED)); + } + + @Test + public void enhancedListIteratorForEachRemaining() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + List storageList = new ArrayList<>(3); + ListIterator iter = list.listIterator(1); + + iter.next(); + + iter.forEachRemaining((e) -> { + storageList.add(e); + }); + + storageList.equals(Arrays.asList(2, 3, 4)); + } + + @Test(expected = NullPointerException.class) + public void + spliteratorTryAdvanceThrowsNullPointerExceptionOnNullConsumer() { + list.spliterator().tryAdvance(null); + } + + @Test(expected = NullPointerException.class) + public void + spliteratorForEachRemainingThrowsNullPointerExceptionOnNullConsumer() { + list.spliterator().forEachRemaining(null); + } + + @Test(expected = ConcurrentModificationException.class) + public void + spliteratorThrowsConcurrentModificationExceptionOnConcurrentModification() { + list.addAll(Arrays.asList(1, 2, 3)); + + Spliterator spliterator = list.spliterator(); + + list.add(4); + spliterator.forEachRemaining((e) -> {}); + list.forEach((e) -> {}); + } + + @Test(expected = NoSuchElementException.class) + public void enhancedIteratorNextThrowsOnNoNext() { + list.addAll(getIntegerList(20)); + + ListIterator iter = list.listIterator(19); + + try { + iter.next(); + } catch (Exception ex) { + fail("Should not get here."); + } + + iter.next(); + } + + @Test(expected = NoSuchElementException.class) + public void enhancedIteratorPrevioiusThrowsOnNoPrevious() { + list.addAll(getIntegerList(20)); + + ListIterator iter = list.listIterator(1); + + try { + iter.previous(); + } catch (Exception ex) { + fail("Should not get here."); + } + + iter.previous(); + } + + @Test(expected = IllegalStateException.class) + public void enhancedIteratorThrowsOnDoubleRemove() { + list.add(1); + + ListIterator iter = list.listIterator(); + + try { + iter.next(); + iter.remove(); + } catch (Exception ex) { + fail("Should not get here."); + } + + iter.remove(); + } + + @Test(expected = NoSuchElementException.class) + public void getLastThrowsOnEmptyList() { + list.getLast(); + } + + @Test + public void getLast() { + list.addAll(Arrays.asList(10, 20)); + assertEquals(Integer.valueOf(20), list.getLast()); + + list.removeLast(); + + assertEquals(Integer.valueOf(10), list.getLast()); + } + + @Test + public void indexOfNull() { + list.addAll(Arrays.asList(1, 2, null, 3, null, 4)); + + assertEquals(2, list.indexOf(null)); + + list.set(2, 5); + list.set(4, 10); + + assertEquals(-1, list.indexOf(null)); + } + + @Test + public void lastIndexOfNull() { + list.addAll(Arrays.asList(1, 2, null, 3, null, 4)); + + assertEquals(4, list.lastIndexOf(null)); + + list.set(2, 5); + list.set(4, 10); + + assertEquals(-1, list.lastIndexOf(null)); + } + + @Test + public void add() { + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + + list.add(1); + + assertEquals(1, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(1), list.get(0)); + + list.add(2); + + assertEquals(2, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(1), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(1)); + } + + @Test + public void addFirst() { + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + + list.addFirst(1); + + assertEquals(1, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(1), list.get(0)); + + list.addFirst(2); + + assertEquals(2, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void throwsOnAccessingEmptyList() { + list.get(0); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void throwsOnNegativeIndexInEmptyList() { + list.get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void throwsOnNegativeIndexInNonEmptyList() { + list.addFirst(10); + list.get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void throwsOnTooLargeIndex() { + list.addFirst(10); + list.addLast(20); + list.get(2); + } + + @Test + public void addIndexAndElement() { + list.add(0, 1); + assertEquals(Integer.valueOf(1), list.get(0)); + + list.add(0, 2); + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + + list.add(2, 10); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + assertEquals(Integer.valueOf(10), list.get(2)); + + list.add(2, 100); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + assertEquals(Integer.valueOf(100), list.get(2)); + assertEquals(Integer.valueOf(10), list.get(3)); + } + + @Test + public void addCollectionOneElementToEmptyList() { + List c = new ArrayList<>(); + c.add(100); + + list.addAll(c); + + assertFalse(list.isEmpty()); + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(100), list.get(0)); + } + + @Test + public void addCollectionThreeElementsToEmptyList() { + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + + List c = Arrays.asList(1, 2, 3); + + list.addAll(c); + assertFalse(list.isEmpty()); + assertEquals(3, list.size()); + + for (int i = 0; i < list.size(); i++) { + assertEquals(Integer.valueOf(i + 1), list.get(i)); + } + } + + @Test + public void addCollectionAtIndex() { + list.addAll(0, Arrays.asList(2, 3)); // setAll + list.addAll(0, Arrays.asList(0, 1)); // prependAll + list.addAll(4, Arrays.asList(6, 7)); // appendAll + list.addAll(4, Arrays.asList(4, 5)); // insertAll + + for (int i = 0; i < list.size(); i++) { + assertEquals(Integer.valueOf(i), list.get(i)); + } + } + + @Test + public void removeInt() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + // [0, 1, 2, 3, 4] + assertEquals(Integer.valueOf(0), list.remove(0)); + // [1, 2, 3, 4] + assertEquals(Integer.valueOf(4), list.remove(3)); + // [1, 2, 3] + assertEquals(Integer.valueOf(2), list.remove(1)); + // [1, 3] + assertEquals(Integer.valueOf(1), list.remove(0)); + // [3] + assertEquals(Integer.valueOf(3), list.remove(0)); + // [] + } + + @Test // shadowed + public void basicIteratorUsage() { + for (int i = 0; i < 1000; i++) { + list.add(i); + } + + Iterator iterator = list.iterator(); + + for (int i = 0; i < 1000; i++) { + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(i), iterator.next()); + } + + assertFalse(iterator.hasNext()); + } + + @Test + public void removeFirstLast() { + list.addAll(getIntegerList(5)); + + List referenceList = new ArrayList<>(list); + + list.removeFirst(); + referenceList.remove(0); + + assertTrue(listsEqual(list, referenceList)); + + list.removeFirst(); + referenceList.remove(0); + + assertTrue(listsEqual(list, referenceList)); + + list.removeLast(); + referenceList.remove(referenceList.size() - 1); + + assertTrue(listsEqual(list, referenceList)); + + list.removeLast(); + referenceList.remove(referenceList.size() - 1); + + assertTrue(listsEqual(list, referenceList)); + } + + @Test(expected = ConcurrentModificationException.class) + public void subListThrowsOnConcurrentModification() { + List l = + new IndexedLinkedList( + Arrays.asList(1, 2, 3, 4)); + + List subList1 = l.subList(1, 4); // <2, 3, 4> + List subList2 = subList1.subList(0, 2); // <2, 3> + + subList1.add(1, Integer.valueOf(10)); + subList2.add(1, Integer.valueOf(11)); // Must throw here. + } + + @Test + public void removeFirstLastOccurrence() { + IndexedLinkedList l = new IndexedLinkedList<>(); + + list.addAll(Arrays.asList(1, 2, 3, 1, 2, 3)); + l.addAll(list); + + list.removeFirstOccurrence(2); + l.removeFirstOccurrence(2); + + assertTrue(listsEqual(list, l)); + + list.removeLastOccurrence(3); + l.removeLastOccurrence(3); + + assertTrue(listsEqual(list, l)); + } + + @Test + public void bruteForceAddCollectionAtIndex() { + Random random = new Random(100L); + + list.addAll(getIntegerList()); + + LinkedList referenceList = new LinkedList<>(list); + + for (int op = 0; op < 100; op++) { + int index = random.nextInt(list.size()); + Collection coll = getIntegerList(random.nextInt(40)); + referenceList.addAll(index, coll); + list.addAll(index, coll); + + if (!listsEqual(list, referenceList)) { + fail("Lists not equal!"); + } + } + } + + @Test + public void removeAtIndex() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + // [0, 1, 2, 3, 4] + assertEquals(Integer.valueOf(2), list.remove(2)); + // [0, 1, 3, 4] + assertEquals(Integer.valueOf(0), list.remove(0)); + // [1, 3, 4] + assertEquals(Integer.valueOf(4), list.remove(2)); + // [1, 3] + assertEquals(Integer.valueOf(3), list.remove(1)); + // [1] + assertEquals(Integer.valueOf(1), list.remove(0)); + // [] + } + + @Test + public void removeObject() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + assertFalse(list.remove(Integer.valueOf(10))); + assertFalse(list.remove(null)); + + list.add(3, null); + + assertTrue(list.remove(null)); + + assertTrue(list.remove(Integer.valueOf(4))); + assertTrue(list.remove(Integer.valueOf(0))); + assertTrue(list.remove(Integer.valueOf(2))); + assertFalse(list.remove(Integer.valueOf(2))); + } + + @Test + public void basicIteratorTraversal() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + Iterator iter = list.iterator(); + + for (int i = 0; i < list.size(); i++) { + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(i), iter.next()); + } + + iter = list.iterator(); + + class MyConsumer implements Consumer { + + int total; + + @Override + public void accept(Integer t) { + total += t; + } + } + + MyConsumer myConsumer = new MyConsumer(); + + list.iterator().forEachRemaining(myConsumer); + assertEquals(10, myConsumer.total); + } + + @Test + public void basicIteratorRemoval() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + Iterator iter = list.iterator(); + + iter.next(); + iter.next(); + iter.remove(); + + assertEquals(4, list.size()); + + iter = list.iterator(); + iter.next(); + iter.remove(); + + assertEquals(3, list.size()); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(3), list.get(1)); + assertEquals(Integer.valueOf(4), list.get(2)); + } + + @Test + public void enhancedIteratorTraversal() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + ListIterator iter = list.listIterator(); + + assertFalse(iter.hasPrevious()); + + for (int i = 0; i < list.size(); i++) { + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(i), iter.next()); + } + + assertFalse(iter.hasNext()); + + for (int i = 4; i >= 0; i--) { + assertTrue(iter.hasPrevious()); + assertEquals(Integer.valueOf(i), iter.previous()); + } + + iter = list.listIterator(2); + + assertEquals(Integer.valueOf(2), iter.next()); + assertEquals(Integer.valueOf(2), iter.previous()); + + iter = list.listIterator(3); + + assertEquals(Integer.valueOf(3), iter.next()); + assertEquals(Integer.valueOf(4), iter.next()); + + assertFalse(iter.hasNext()); + assertTrue(iter.hasPrevious()); + } + + @Test + public void removeAt() { + list.addAll(getIntegerList(10)); + List referenceList = new ArrayList<>(list); + Random random = new Random(100L); + + while (!referenceList.isEmpty()) { + int removalIndex = random.nextInt(list.size()); + Integer referenceInteger = referenceList.remove(removalIndex); + Integer listInteger = list.remove(removalIndex); + assertEquals(referenceInteger, listInteger); + assertEquals(referenceList, list); + } + } + + // Used to find a failing removal sequence: + @Test + public void removeAtFindFailing() { + long seed = 101L; + Random random = new Random(seed); + int iteration = 0; + while (true) { + iteration++; + + list.clear(); + list.addAll(getIntegerList(10)); + + List indices = new ArrayList<>(); + + if (iteration == 100) { + return; + } + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + indices.add(index); + + try { + list.remove(index); + } catch (NullPointerException ex) { + // Should not get here. Ever. + fail("Failing indices: " + indices); + return; + }catch (AssertionError ae) { + return; + } + } + } + } + + @Test + public void bugTinyRemoveInt() { + list.addAll(getIntegerList(5)); + + list.remove(4); + list.remove(0); + list.remove(2); + list.remove(0); + list.remove(0); + } + + @Test + public void removeAtIndex1() { + list.addAll(getIntegerList(10)); + // TODO: remove 'getIntegerList()'! + List referenceList = new ArrayList<>(getIntegerList(10)); + int[] indices = { 9, 3, 3, 3, 1, 0 }; + + for (int i = 0; i < indices.length; i++) { + assertEquals(referenceList, list); + + int index = indices[i]; + list.remove(index); + referenceList.remove((int) index); + } + + assertEquals(referenceList, list); + } + + @Test + public void enhancedIteratorAddition() { + list.addAll(Arrays.asList(1, 2, 3)); + ListIterator iter = list.listIterator(); + + iter.add(0); + + while (iter.hasNext()) { + iter.next(); + } + + iter.add(4); + iter = list.listIterator(); + + for (int i = 0; i < list.size(); i++) { + assertEquals(Integer.valueOf(i), iter.next()); + } + + iter = list.listIterator(2); + iter.add(10); + + assertEquals(Integer.valueOf(10), list.get(2)); + } + + @Test + public void findFailingIterator() { + list.addAll(getIntegerList(3850)); + Iterator iterator = list.iterator(); + int counter = 0; + + while (iterator.hasNext()) { + assertEquals(Integer.valueOf(counter), iterator.next()); + + // Remove every 10th element: + if (counter % 10 == 0) { + iterator.remove(); + } + + counter++; + } + } + + @Test + public void bruteForceIteratorRemove() throws Exception { + list.addAll(getIntegerList(1000)); + + int counter = 1; + List arrayList = new ArrayList<>(list); + Iterator iter = list.iterator(); + Iterator arrayListIter = arrayList.iterator(); + int totalIterations = 0; + + while (iter.hasNext()) { + iter.next(); + arrayListIter.next(); + + if (counter % 10 == 0) { + + try { + iter.remove(); + } catch (IllegalStateException ex) { + throw new Exception(ex); + } + + arrayListIter.remove(); + counter = 1; + } else { + counter++; + } + + if (!listsEqual(list, arrayList)) { + throw new IllegalStateException( + "totalIterations = " + totalIterations); + } + + totalIterations++; + } + } + + @Test + public void findFailingRemoveObject() { + LinkedList referenceList = new LinkedList<>(); + + list.addAll(getIntegerList(10)); + referenceList.addAll(list); + + Integer probe = list.get(1); + + list.remove(probe); + referenceList.remove(probe); + + Iterator iterator1 = list.iterator(); + Iterator iterator2 = referenceList.iterator(); + + Random random = new Random(100L); + + while (!list.isEmpty()) { + if (!iterator1.hasNext()) { + + if (iterator2.hasNext()) { + throw new IllegalStateException(); + } + + iterator1 = list.iterator(); + iterator2 = referenceList.iterator(); + continue; + } + + iterator1.next(); + iterator2.next(); + + if (random.nextBoolean()) { + iterator1.remove(); + iterator2.remove(); + assertTrue(listsEqual(list, referenceList)); + } + } + + assertTrue(listsEqual(list, referenceList)); + } + + @Test + public void iteratorAdd() { + list.addAll(getIntegerList(4)); + + ListIterator iterator = list.listIterator(1); + + assertEquals(1, iterator.nextIndex()); + assertEquals(0, iterator.previousIndex()); + + iterator.next(); + + assertEquals(2, iterator.nextIndex()); + assertEquals(1, iterator.previousIndex()); + + iterator.add(Integer.valueOf(100)); + + assertEquals(Integer.valueOf(0), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + assertEquals(Integer.valueOf(100), list.get(2)); + assertEquals(Integer.valueOf(2), list.get(3)); + assertEquals(Integer.valueOf(3), list.get(4)); + } + + @Test + public void bruteForceIteratorTest() { + list.addAll(getIntegerList(100)); + List referenceList = new LinkedList<>(list); + + ListIterator iterator1 = list.listIterator(2); + ListIterator iterator2 = referenceList.listIterator(2); + Random random = new Random(300L); + + while (iterator1.hasNext()) { + if (!iterator2.hasNext()) { + fail("Iterator mismatch on hasNext()."); + } + + iterator1.next(); + iterator2.next(); + + int choice = random.nextInt(10); + + if (choice < 2) { + Integer integer = Integer.valueOf(random.nextInt(100)); + iterator1.add(integer); + iterator2.add(integer); + assertTrue(listsEqual(list, referenceList)); + } else if (choice == 2) { + iterator1.remove(); + iterator2.remove(); + assertTrue(listsEqual(list, referenceList)); + } else if (choice < 6) { + if (iterator1.hasPrevious()) { + iterator1.previous(); + } + + if (iterator2.hasPrevious()) { + iterator2.previous(); + } + } else { + if (iterator1.hasNext()) { + iterator1.next(); + } + + if (iterator2.hasNext()) { + iterator2.next(); + } + } + } + + if (iterator2.hasNext()) { + fail("Java List iterator has more to offer."); + } + } + + @Test + public void indexOf() { + list.add(1); + list.add(2); + list.add(3); + + list.add(3); + list.add(2); + list.add(1); + + assertEquals(0, list.indexOf(1)); + assertEquals(1, list.indexOf(2)); + assertEquals(2, list.indexOf(3)); + + assertEquals(3, list.lastIndexOf(3)); + assertEquals(4, list.lastIndexOf(2)); + assertEquals(5, list.lastIndexOf(1)); + } + + class MyIntegerConsumer implements Consumer { + + List ints = new ArrayList<>(); + + @Override + public void accept(Integer t) { + ints.add(t); + } + } + + @Test + @SuppressWarnings("empty-statement") + public void basicSpliteratorUsage() { + list.addAll(getIntegerList(10_000)); + + Spliterator spliterator1 = list.spliterator(); + Spliterator spliterator2 = spliterator1.trySplit(); + + //// spliterator 2 : spliterator 1 + + assertEquals(5000, spliterator1.getExactSizeIfKnown()); + assertEquals(5000, spliterator2.getExactSizeIfKnown()); + + + assertTrue(spliterator2.tryAdvance( + i -> assertEquals(list.get(0), Integer.valueOf(0)))); + + assertTrue(spliterator2.tryAdvance( + i -> assertEquals(list.get(1), Integer.valueOf(1)))); + + assertTrue(spliterator2.tryAdvance( + i -> assertEquals(list.get(2), Integer.valueOf(2)))); + + + + assertTrue(spliterator1.tryAdvance( + i -> assertEquals(list.get(5000), Integer.valueOf(5000)))); + + assertTrue(spliterator1.tryAdvance( + i -> assertEquals(list.get(5001), Integer.valueOf(5001)))); + + assertTrue(spliterator1.tryAdvance( + i -> assertEquals(list.get(5002), Integer.valueOf(5002)))); + + //// spliterator 3 : spliterator 2 : splitereator 1 + + Spliterator spliterator3 = spliterator2.trySplit(); + + assertEquals(4997, spliterator1.getExactSizeIfKnown()); + + assertTrue(spliterator3.tryAdvance( + i -> assertEquals(list.get(3), Integer.valueOf(3)))); + + assertTrue(spliterator3.tryAdvance( + i -> assertEquals(list.get(4), Integer.valueOf(4)))); + + assertTrue(spliterator3.tryAdvance( + i -> assertEquals(list.get(5), Integer.valueOf(5)))); + + //// + + MyIntegerConsumer consumer = new MyIntegerConsumer(); + + while (spliterator1.tryAdvance(consumer)); + + for (int i = 0; i < consumer.ints.size(); i++) { + Integer actualInteger = consumer.ints.get(i); + Integer expectedInteger = 5003 + i; + assertEquals(expectedInteger, actualInteger); + } + } + + @Test + public void spliteratorForEachRemaining() { + list.addAll(getIntegerList(10_000)); + Spliterator split = list.spliterator(); + MyIntegerConsumer consumer = new MyIntegerConsumer(); + + split.forEachRemaining(consumer); + + for (int i = 0; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), consumer.ints.get(i)); + } + } + + @Test + public void spliteratorForEachRemainingTwoSpliterators() { + list.addAll(getIntegerList(10_000)); + Spliterator splitRight = list.spliterator(); + Spliterator splitLeft = splitRight.trySplit(); + + MyIntegerConsumer consumerRight = new MyIntegerConsumer(); + MyIntegerConsumer consumerLeft = new MyIntegerConsumer(); + + splitRight.forEachRemaining(consumerRight); + splitLeft.forEachRemaining(consumerLeft); + + for (int i = 0; i < 5_000; i++) { + assertEquals(Integer.valueOf(i), consumerLeft.ints.get(i)); + } + + for (int i = 5_000; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), consumerRight.ints.get(i - 5_000)); + } + } + + @Test + public void spliteratorForEachRemainingWithAdvance() { + list.addAll(getIntegerList(10_000)); + Spliterator rightSpliterator = list.spliterator(); + + assertTrue( + rightSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(0), i))); + + Spliterator leftSpliterator = rightSpliterator.trySplit(); + + assertEquals(4_999, rightSpliterator.getExactSizeIfKnown()); + assertEquals(5_000, leftSpliterator.getExactSizeIfKnown()); + + // Check two leftmost elements of the left spliterator: + assertTrue(leftSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(1), i))); + + assertTrue(leftSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(2), i))); + + // Check two leftmost elements of the right splliterator: + assertTrue(rightSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(5_000), i))); + + assertTrue(rightSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(5_001), i))); + } + + @Test + public void spliterator() { + list.addAll(getIntegerList(6_000)); + Spliterator split = list.spliterator(); + + assertEquals(6_000L, split.getExactSizeIfKnown()); + assertEquals(6_000L, split.estimateSize()); + + assertTrue(split.tryAdvance((i) -> assertEquals(list.get((int) i), i))); + assertTrue(split.tryAdvance((i) -> assertEquals(list.get((int) i), i))); + + assertEquals(5998, split.getExactSizeIfKnown()); + + // 5998 elements left / 2 = 2999 per spliterator: + Spliterator leftSpliterator = split.trySplit(); + + assertNotNull(leftSpliterator); + assertEquals(2999, split.getExactSizeIfKnown()); + assertEquals(2999, leftSpliterator.getExactSizeIfKnown()); + + //// leftSpliterator = [1, 2999] + + for (int i = 2; i < 3000; i++) { + Integer integer = list.get(i); + assertTrue( + leftSpliterator.tryAdvance( + (j) -> assertEquals(integer, j))); + } + + //// split = [3001, 5999] + + assertTrue(split.tryAdvance(i -> assertEquals(2999, i))); + assertTrue(split.tryAdvance(i -> assertEquals(3000, i))); + assertTrue(split.tryAdvance(i -> assertEquals(3001, i))); + + while (split.getExactSizeIfKnown() > 0) { + split.tryAdvance(i -> {}); + } + + assertFalse(split.tryAdvance(i -> {})); + } + + @Test + public void bruteforceSpliterator() { + list.addAll(getIntegerList(1_000_000)); + Collections.shuffle(list); + + List newList = + list.parallelStream() + .map(i -> 2 * i) + .collect(Collectors.toList()); + + assertEquals(newList.size(), list.size()); + + for (int i = 0; i < list.size(); i++) { + Integer integer1 = 2 * list.get(i); + Integer integer2 = newList.get(i); + assertEquals(integer1, integer2); + } + } + + private static final String SERIALIZATION_FILE_NAME = "LinkedList.ser"; + + @Test + public void serialization() { + list.add(10); + list.add(13); + list.add(12); + + try { + File file = new File(SERIALIZATION_FILE_NAME); + + FileOutputStream fos = new FileOutputStream(file); + ObjectOutputStream oos = new ObjectOutputStream(fos); + + oos.writeObject(list); + oos.flush(); + oos.close(); + + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + + IndexedLinkedList ll = + (IndexedLinkedList) ois.readObject(); + + ois.close(); + boolean equal = listsEqual(list, ll); + assertTrue(equal); + + if (!file.delete()) { + file.deleteOnExit(); + } + + } catch (IOException | ClassNotFoundException ex) { + fail(ex.getMessage()); + } + } + + @Test + public void bruteforceSerialization() { + for (int i = 0; i < 20; i++) { + list.addAll(getIntegerList(i)); + + try { + File file = new File(SERIALIZATION_FILE_NAME); + + FileOutputStream fos = new FileOutputStream(file); + ObjectOutputStream oos = new ObjectOutputStream(fos); + + oos.writeObject(list); + oos.flush(); + oos.close(); + + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + + IndexedLinkedList ll = + (IndexedLinkedList) ois.readObject(); + + ois.close(); + boolean equal = listsEqual(list, ll); + assertTrue(equal); + + if (!file.delete()) { + file.deleteOnExit(); + } + + } catch (IOException | ClassNotFoundException ex) { + fail(ex.getMessage()); + } + + list.clear(); + } + } + + @Test + public void bugCheckInvariantAfterRemoval() { + for (int i = 0; i < 4; i++) { + list.add(i); + } + + list.remove(Integer.valueOf(3)); + list.remove(1); + assertEquals(list.size(), 2); + assertEquals(Integer.valueOf(0), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(1)); + } + + @Test + public void bruteForceRemoveAt1() { + Random random = new Random(400L); + + list.addAll(getIntegerList(1000)); + List referenceList = new ArrayList<>(list); + + Integer probe = Integer.valueOf(3); + + list.remove(probe); + referenceList.remove(probe); + + int iters = 0; + + while (!list.isEmpty()) { + iters++; + int index = random.nextInt(list.size()); + list.remove(index); + referenceList.remove(index); + + listsEqual(list, referenceList); + } + } + + @Test + public void contractAdaptsToMinimumCapacity() { + list.addAll(getIntegerList(1000_000)); + list.subList(10, 1000_000 - 10).clear(); + list.checkInvarant(); + assertEquals(20, list.size()); + } + + @Test + public void bruteForceRemoveAt2() { + long seed = 1630487847317L; + Random random = new Random(seed); + + for (int i = 0; i < 100; i++) { + list.addAll(getIntegerList(10)); + List indices = new ArrayList<>(list.size()); + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + indices.add(index); + + try { + list.remove(index); + } catch (AssertionError ae) { + System.out.println( + "Message: " + ae.getMessage() + ", indices: " + + indices.toString()); + return; + } + } + + indices.clear(); + } + } + + @Test + public void bugRemoveAt2() { + list.addAll(getIntegerList(10)); + final int[] indices = { 7, 7, 4, 1, 2, 1, 3, 1, 1, 0 }; + + for (int i = 0; i < indices.length; i++) { + final int index = indices[i]; + list.remove(index); + } + } + + @Test + public void bugRemoveAt() { + list.addAll(getIntegerList(10)); + + assertEquals(Integer.valueOf(5), list.remove(5)); + + assertEquals(Integer.valueOf(3), list.remove(3)); + + assertEquals(Integer.valueOf(2), list.remove(2)); + + assertEquals(Integer.valueOf(1), list.remove(1)); + + // list = [0, 4, 5, 7, 8, 8] + assertEquals(Integer.valueOf(8), list.remove(4)); + } + + @Test + public void bugRemoveFirst() { + list.addAll(getIntegerList(5)); + + assertEquals(5, list.size()); + + for (int i = 0; i < 2; i++) { + list.removeFirst(); + } + + Random random = new Random(500L); + List referenceList = new ArrayList<>(list); + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + list.remove(index); + referenceList.remove(index); + assertTrue(listsEqual(list, referenceList)); + } + } + + @Test + public void bugRemoveLast() { + list.addAll(getIntegerList(10)); + + assertEquals(10, list.size()); + + for (int i = 0; i < 5; i++) { + list.removeLast(); + } + + Random random = new Random(600L); + List referenceList = new ArrayList<>(list); + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + list.remove(index); + referenceList.remove(index); + assertTrue(listsEqual(list, referenceList)); + } + } + + private static List getIntegerList(int length) { + List list = new ArrayList<>(length); + + for (int i = 0; i < length; i++) { + list.add(i); + } + + return list; + } + + private static List getIntegerList() { + return getIntegerList(100); + } + + private static boolean listsEqual(IndexedLinkedList list1, + List list2) { + + if (list1.size() != list2.size()) { + return false; + } + + Iterator iter1 = list1.iterator(); + Iterator iter2 = list2.iterator(); + + while (iter1.hasNext() && iter2.hasNext()) { + Integer int1 = iter1.next(); + Integer int2 = iter2.next(); + + if (!int1.equals(int2)) { + return false; + } + } + + if (iter1.hasNext() || iter2.hasNext()) { + throw new IllegalStateException(); + } + + return true; + } +}