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:
+ *
+ * - {@code F.node} - the actual element node,
+ * - {@code F.index} - the appearance index of {@code F.node} in the actual
+ * list.
+ *
+ *
+ *
+ *
+ * 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 extends E> 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 extends E> 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 extends E> 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:
+ *
+ * - All the fingers in the finger list are sorted by indices.
+ * - There is no duplicate indices.
+ * - The index of the leftmost finger is no less than zero.
+ * - 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}.
+ *
+ * - Each finger {@code F} points to the {@code i}th linked list node,
+ * where {@code i = F.index}.
+ *
+ * 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 super E> 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 super E> 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 extends E> 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 super E> 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 super E> 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 super E> 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 extends E> 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 extends E> c) {
+ Iterator extends E> 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 super E> 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 extends E> c) {
+ Iterator extends E> 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 super E> 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 super E> 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 extends E> c) {
+ return addAll(this.size, c);
+ }
+
+ @Override
+ public boolean addAll(int index, Collection extends E> 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 super E> 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 super E> 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 super E> 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