package org.threadly.concurrent.collections; import java.util.Arrays; import java.util.Collection; import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.NoSuchElementException; import java.util.RandomAccess; import org.threadly.util.ArgumentVerifier; /** * A thread safe list implementation with an array back end. Make sure to read the javadocs * carefully, as several functions behave subtly different from the java.util.List definition. * <p> * The design of this implementation is NOT to completely avoid synchronization. We have a hybrid * implementation of volatile and synchronized to allow for cheaper reading, but keeping high * consistency. It works with the idea that the internal data is immutable. Each read has an * immutable version of the data. Thus making writes more expensive (almost like a * {@link java.util.concurrent.CopyOnWriteArrayList}). * <p> * There are several differences between this and a CopyOnWriteArrayList. The first being that we * don't have to copy the structure on every write operation. By setting the front and/or rear * padding, we can add items to the front or end of the list while avoiding a copy operation. In * addition removals also do not require a copy operation. Furthermore, this implementation * differs from a CopyOnWriteArrayList is that it does allow some synchronization. Which can * give higher consistency guarantees for some operations by allowing you to synchronize on the * modification lock to perform multiple atomic operations. * <p> * The main motivation in implementing this was to avoid array copies as much as possible (which * for large lists can be costly). But also the implementation to cheaply reposition an item in * the list was necessary for other performance benefits. * <p> * A couple notable points is that subList calls are very cheap, but modifications to sublist are * completely independent from their source list. * <p> * Unlike CopyOnWriteArrayList, Iterators can attempt to modify the state of the backing structure * (assuming it still makes sense to do so). Although unlike CopyOnWriteArrayList iterators, once * an Iterator is created it will never see updates to the structure. For that reason it is * impossible to have a {@code ConcurrentModificationExcception}. * * @since 1.0.0 * @param <T> type of object to retain */ public class ConcurrentArrayList<T> implements List<T>, Deque<T>, RandomAccess { private static final short HASH_CODE_PRIME_NUMBER = 31; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; protected static <E> DataSet<E> makeEmptyDataSet(int frontPadding, int rearPadding) { ArgumentVerifier.assertNotNegative(frontPadding, "frontPadding"); ArgumentVerifier.assertNotNegative(rearPadding, "rearPadding"); return new DataSet<>(EMPTY_OBJECT_ARRAY, 0, 0, frontPadding, rearPadding); } protected final Object modificationLock; protected volatile DataSet<T> currentData; /** * Constructs a new {@link ConcurrentArrayList} with a new internal NativeLock implementation. */ public ConcurrentArrayList() { this(0, 0); } /** * Constructs a new {@link ConcurrentArrayList} with specific padding. Specifying the padding * amounts can optimize this implementation more for the specific use case. If there is space * in the array for adds to the front or end, then we are able to avoid an array copy. * * @param frontPadding padding to add to front of array to possible avoid array copies * @param rearPadding padding to add to end of array to possible avoid array copies */ public ConcurrentArrayList(int frontPadding, int rearPadding) { this(null, frontPadding, rearPadding); } /** * Constructs a new {@link ConcurrentArrayList} with a provided lock object. This is the lock * used to guard modifications, and is returned from {@link #getModificationLock()}. * * @param modificationLock lock to synchronize on internally */ protected ConcurrentArrayList(Object modificationLock) { this(modificationLock, 0, 0); } /** * Constructs a new {@link ConcurrentArrayList} with a provided lock object. This is the lock * used to guard modifications, and is returned from {@link #getModificationLock()}. Specifying * the padding amounts can optimize this implementation more for the specific use case. If * there is space in the array for adds to the front or end, then we are able to avoid an array * copy. * * @param modificationLock lock to synchronize on internally * @param frontPadding padding to add to front of array to possible avoid array copies * @param rearPadding padding to add to end of array to possible avoid array copies */ protected ConcurrentArrayList(Object modificationLock, int frontPadding, int rearPadding) { this(ConcurrentArrayList.<T>makeEmptyDataSet(frontPadding, rearPadding), modificationLock); } /** * Internal constructor which provides the modification lock and the initial {@link DataSet}. * This is used for constructing sub-lists, but may also be useful to extending classes. * * @param startSet {@link DataSet} to use internally * @param modificationLock lock to synchronize on internally */ protected ConcurrentArrayList(DataSet<T> startSet, Object modificationLock) { ArgumentVerifier.assertNotNull(startSet, "startSet"); if (modificationLock == null) { modificationLock = new Object(); } this.modificationLock = modificationLock; currentData = startSet; } /** * If you want to chain multiple calls together and ensure that no threads modify the structure * during that time you can get the lock to prevent additional modifications. * <p> * This lock should be synchronized on to prevent modifications. * * @return lock used internally */ public Object getModificationLock() { return modificationLock; } /** * This changes the configuration for the front padding amount for future modification * operations. * * @param frontPadding New value to over allocate the front of new buffers */ public void setFrontPadding(int frontPadding) { ArgumentVerifier.assertNotNegative(frontPadding, "frontPadding"); synchronized (modificationLock) { currentData.frontPadding = frontPadding; } } /** * This changes the configuration for the rear padding amount for future modification operations. * * @param rearPadding New value to over allocate the rear of new buffers */ public void setRearPadding(int rearPadding) { ArgumentVerifier.assertNotNegative(rearPadding, "rearPadding"); synchronized (modificationLock) { currentData.rearPadding = rearPadding; } } /** * Getter for current amount to added padding to the front of new buffers. * * @return current amount to added padding to the front of new buffers */ public int getFrontPadding() { return currentData.frontPadding; } /** * Getter for current amount to added padding to the rear of new buffers. * * @return current amount to added padding to the rear of new buffers */ public int getRearPadding() { return currentData.rearPadding; } /** * Trims the internally array to discard any unused storage. It is good to invoke this if * future adds are unlikely, and it is desired to keep memory usage minimal. This does not * adjust the set front or rear padding, so additional modifications will expand the array based * off those set values. To make sure additional modifications do not expand the array any more * than necessary invoke {@link #setFrontPadding(int)} and {@link #setRearPadding(int)} with * {@code 0}. */ public void trimToSize() { synchronized (modificationLock) { currentData = currentData.trimToSize(); } } @Override public int size() { return currentData.size; } @Override public boolean isEmpty() { return currentData.size == 0; } @Override public T get(int index) { try { return currentData.get(index); } catch (ArrayIndexOutOfBoundsException e) { // translate to the expected exception type throw new IndexOutOfBoundsException(); } } @Override public int indexOf(Object o) { return currentData.indexOf(o); } @Override public int lastIndexOf(Object o) { return currentData.lastIndexOf(o); } @Override public boolean contains(Object o) { return currentData.indexOf(o) >= 0; } @Override public boolean containsAll(Collection<?> c) { if (c == null || c.isEmpty()) { return true; } DataSet<T> workingSet = currentData; Iterator<?> it = c.iterator(); while (it.hasNext()) { if (workingSet.indexOf(it.next()) < 0) { return false; } } return true; } @Override public Object[] toArray() { DataSet<T> workingSet = currentData; return Arrays.copyOfRange(workingSet.dataArray, workingSet.dataStartIndex, workingSet.dataEndIndex); } @SuppressWarnings("unchecked") @Override public <E> E[] toArray(E[] a) { DataSet<T> workingSet = currentData; if (a.length < workingSet.size) { return (E[])Arrays.copyOfRange(workingSet.dataArray, workingSet.dataStartIndex, workingSet.dataEndIndex, a.getClass()); } else { System.arraycopy(workingSet.dataArray, workingSet.dataStartIndex, a, 0, workingSet.size); return a; } } @Override public boolean add(T e) { if (e == null) { return false; } synchronized (modificationLock) { currentData = currentData.addToEnd(e); } return true; } @Override public boolean addAll(Collection<? extends T> c) { if (c == null || c.isEmpty()) { return false; } Iterator<? extends T> it = c.iterator(); while (it.hasNext()) { if (it.next() == null) { throw new IllegalArgumentException("Can not store null values"); } } synchronized (modificationLock) { currentData = currentData.addAll(c); } return true; } @Override public boolean addAll(int index, Collection<? extends T> c) { if (index < 0) { throw new IndexOutOfBoundsException("Index can not be negative"); } else if (c == null) { return false; } Iterator<? extends T> it = c.iterator(); while (it.hasNext()) { if (it.next() == null) { it.remove(); } } if (c.isEmpty()) { return false; } synchronized (modificationLock) { if (index > currentData.size) { throw new IndexOutOfBoundsException("Index is beyond the array size: " + index); } currentData = currentData.addAll(index, c); } return true; } @Override public boolean retainAll(Collection<?> c) { if (c == this) { return false; } else if (c == null || c.isEmpty()) { if (isEmpty()) { return false; } else { clear(); return true; } } DataSet<T> originalSet; DataSet<T> resultSet; synchronized (modificationLock) { originalSet = currentData; currentData = resultSet = currentData.retainAll(c); } return ! resultSet.equalsExactly(originalSet); } @Override public void clear() { synchronized (modificationLock) { currentData = makeEmptyDataSet(currentData.frontPadding, currentData.rearPadding); } } @Override public void addFirst(T e) { // nulls can't be accepted because of how we attempt to prevent array copies if (e == null) { throw new UnsupportedOperationException("This structure can not accept nulls"); } synchronized (modificationLock) { currentData = currentData.addToFront(e); } } @Override public void addLast(T e) { // nulls can't be accepted because of how we attempt to prevent array copies if (e == null) { throw new UnsupportedOperationException("This structure can not accept nulls"); } synchronized (modificationLock) { currentData = currentData.addToEnd(e); } } @Override public boolean offerFirst(T e) { addFirst(e); // this implementation has no capacity limit return true; } @Override public boolean offerLast(T e) { addLast(e); // this implementation has no capacity limit return true; } @Override public T removeFirst() { T result = pollFirst(); if (result == null) { throw new NoSuchElementException(); } return result; } @Override public T removeLast() { T result = pollLast(); if (result == null) { throw new NoSuchElementException(); } return result; } @Override public T pollFirst() { synchronized (modificationLock) { T result = peekFirst(); if (result != null) { currentData = currentData.remove(0); } return result; } } @Override public T pollLast() { synchronized (modificationLock) { T result = peekLast(); if (result != null) { currentData = currentData.remove(currentData.size - 1); } return result; } } @Override public T getFirst() { T result = peekFirst(); if (result == null) { throw new NoSuchElementException(); } return result; } @Override public T getLast() { T result = peekLast(); if (result == null) { throw new NoSuchElementException(); } return result; } @Override public T peek() { return peekFirst(); } @Override public T peekFirst() { DataSet<T> set = currentData; if (set.size > 0) { return set.get(0); } else { return null; } } @Override public T peekLast() { DataSet<T> set = currentData; if (set.size > 0) { return set.get(set.size - 1); } else { return null; } } @Override public boolean removeAll(Collection<?> c) { if (c == null || c.isEmpty()) { return false; } DataSet<T> originalSet; DataSet<T> resultSet; synchronized (modificationLock) { originalSet = currentData; currentData = resultSet = currentData.removeAll(c); } return ! resultSet.equalsExactly(originalSet); } protected boolean remove(Object o, boolean searchBackwards) { if (o == null) { return false; } synchronized (modificationLock) { int index; if (searchBackwards) { index = currentData.lastIndexOf(o); } else { index = currentData.indexOf(o); } if (index < 0) { return false; } else { currentData = currentData.remove(index); return true; } } } @Override public boolean removeFirstOccurrence(Object o) { return remove(o, false); } @Override public boolean removeLastOccurrence(Object o) { return remove(o, true); } @Override public boolean remove(Object o) { return removeFirstOccurrence(o); } @Override public T remove(int index) { if (index < 0) { throw new IndexOutOfBoundsException("Index can not be negative"); } DataSet<T> originalSet; synchronized (modificationLock) { if (index > currentData.size - 1) { throw new IndexOutOfBoundsException("Index is beyond the array max index: " + index); } originalSet = currentData; currentData = currentData.remove(index); } return originalSet.get(index); } @Override public boolean offer(T e) { return offerLast(e); } @Override public T remove() { return removeFirst(); } @Override public T poll() { return pollFirst(); } @Override public T element() { return getFirst(); } @Override public void push(T e) { addFirst(e); } @Override public T pop() { return removeFirst(); } @Override public T set(int index, T element) { if (index < 0) { throw new IndexOutOfBoundsException("Index can not be negative"); } DataSet<T> originalSet; synchronized (modificationLock) { if (index > currentData.size - 1) { throw new IndexOutOfBoundsException("Index is beyond the array max index: " + index); } originalSet = currentData; currentData = currentData.set(index, element); } return originalSet.get(index); } @Override public void add(int index, T element) { if (index < 0) { throw new IndexOutOfBoundsException("Index can not be negative"); } synchronized (modificationLock) { if (index > currentData.size) { throw new IndexOutOfBoundsException("Index is beyond the array size: " + index); } currentData = currentData.add(index, element); } } /** * Move a stored item to a new index. By default a forward search will happen to find the item. * * @param item item to be moved * @param newIndex new index for placement */ public void reposition(T item, int newIndex) { reposition(item, newIndex, false); } /** * Move a stored item to a new index. If you have an idea if it is closer to the start or end of * the list you can specify which end to start the search on. * * @param item item to be moved * @param newIndex new index for placement * @param searchBackwards true to start from the end and search backwards */ public void reposition(T item, int newIndex, boolean searchBackwards) { if (newIndex < 0) { throw new IndexOutOfBoundsException("New index can not be negative"); } synchronized (modificationLock) { if (newIndex > currentData.size) { throw new IndexOutOfBoundsException(newIndex + " is beyond the array's size: " + currentData.size); } int index = searchBackwards ? lastIndexOf(item) : indexOf(item); if (index < 0) { throw new NoSuchElementException("Could not find item: " + item); } else if (index == newIndex) { return; } currentData = currentData.reposition(index, newIndex); } } /** * Move a stored item located at an index to a new index. Provide the size for newIndex to move * the item to the end of the list. Otherwise all items after the new index will be shifted * right. * * @param originalIndex index for item to be moved to. * @param newIndex new index location for item. */ public void reposition(int originalIndex, int newIndex) { if (newIndex < 0) { throw new IndexOutOfBoundsException("new index can not be negative"); } else if (originalIndex < 0) { throw new IndexOutOfBoundsException("original index can not be negative"); } if (originalIndex == newIndex) { return; } synchronized (modificationLock) { if (newIndex > currentData.size) { throw new IndexOutOfBoundsException("new index " + newIndex + " is beyond the array's length: " + currentData.size); } else if (originalIndex > currentData.size) { throw new IndexOutOfBoundsException("original index " + originalIndex + " is beyond the array's length: " + currentData.size); } currentData = currentData.reposition(originalIndex, newIndex); } } @Override public Iterator<T> iterator() { return listIterator(0); } @Override public ListIterator<T> listIterator() { return listIterator(0); } @Override public ListIterator<T> listIterator(int index) { return new DataSetListIterator(currentData, index); } @Override public Iterator<T> descendingIterator() { final ListIterator<T> li = listIterator(currentData.size); return new Iterator<T>() { @Override public boolean hasNext() { return li.hasPrevious(); } @Override public T next() { return li.previous(); } @Override public void remove() { li.remove(); } }; } /** * This returns a sub list from the current list. The initial call is very cheap because it * uses the current data backing to produce (no copying necessary). * <p> * This differers from other subList implementations in that any modifications to this list * will be treated as a completely new list, and wont ever reflect on the source list. This is * very different from other java.util.List implementations, and should be noted carefully. * * @param fromIndex start index (inclusive) for new list to include * @param toIndex end index (exclusive) to be included in new list * @return new independent list */ @Override public List<T> subList(int fromIndex, int toIndex) { DataSet<T> workingData = currentData; if (fromIndex < 0) { throw new IndexOutOfBoundsException("from index can not be negative"); } else if (fromIndex > workingData.size) { throw new IndexOutOfBoundsException("from index must be <= size: " + workingData.size); } else if (toIndex > workingData.size) { throw new IndexOutOfBoundsException("to index must be <= size: " + workingData.size); } else if (toIndex <= fromIndex) { throw new IndexOutOfBoundsException("fromIndex must be < toIndex"); } DataSet<T> newSet = new DataSet<>(workingData.dataArray, workingData.dataStartIndex + fromIndex, workingData.dataEndIndex - (workingData.dataEndIndex - toIndex), currentData.frontPadding, currentData.rearPadding); return new ConcurrentArrayList<>(newSet, modificationLock); } @Override public String toString() { return currentData.toString(); } @SuppressWarnings("rawtypes") @Override public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof ConcurrentArrayList) { ConcurrentArrayList cal = (ConcurrentArrayList)o; return currentData.equalsEquivelent(cal.currentData); } else if (o instanceof List) { List list = (List)o; if (list.size() != this.size()) { return false; } Iterator thisIt = this.iterator(); Iterator listIt = list.iterator(); while (thisIt.hasNext() && listIt.hasNext()) { if (! thisIt.next().equals(listIt.next())) { return false; } } if (thisIt.hasNext() || listIt.hasNext()) { return false; } return true; } else { return false; } } @Override public int hashCode() { return currentData.hashCode(); } /** * This is an iterator implementation that is designed to iterate over a given dataSet. * Modifiable actions will attempt to make changes to the parent class. * * @since 1.0.0 */ protected class DataSetListIterator implements ListIterator<T> { private DataSet<T> dataSet; private int nextIndex; public DataSetListIterator(DataSet<T> dataSet, int index) { this.dataSet = dataSet; nextIndex = index; } @Override public boolean hasNext() { return nextIndex < dataSet.size; } @Override public T next() { verifyPosition(); return dataSet.get(nextIndex++); } @Override public boolean hasPrevious() { return nextIndex - 1 >= 0; } @Override public T previous() { nextIndex--; verifyPosition(); return dataSet.get(nextIndex); } private void verifyPosition() { if (nextIndex < 0 || nextIndex >= dataSet.size) { throw new NoSuchElementException(); } } @Override public int nextIndex() { return nextIndex; } @Override public int previousIndex() { return nextIndex - 1; } @Override public void remove() { synchronized (modificationLock) { // you can not cause concurrent modification exceptions with this implementation if (currentData == dataSet) { ConcurrentArrayList.this.remove(--nextIndex); dataSet = currentData; } else { int globalIndex = ConcurrentArrayList.this.indexOf(dataSet.get(nextIndex - 1)); if (globalIndex >= 0) { ConcurrentArrayList.this.remove(globalIndex); } } } } @Override public void set(T e) { synchronized (modificationLock) { if (currentData == dataSet) { ConcurrentArrayList.this.set(nextIndex - 1, e); dataSet = currentData; } else { int globalIndex = ConcurrentArrayList.this.indexOf(dataSet.get(nextIndex - 1)); if (globalIndex >= 0) { ConcurrentArrayList.this.set(globalIndex, e); } } } } @Override public void add(T e) { synchronized (modificationLock) { if (currentData == dataSet) { ConcurrentArrayList.this.add(nextIndex, e); nextIndex++; dataSet = currentData; } else { int globalIndex = ConcurrentArrayList.this.indexOf(dataSet.get(nextIndex - 1)); if (globalIndex >= 0) { ConcurrentArrayList.this.add(globalIndex + 1, e); } } } } } /** * This is designed to be an immutable version of the list. Modifiable actions will return a * new instance that is based off this one. Because the array may change in areas outside of * the scope of this dataArray, it is expected that the modificationLock is held while any * modifiable operations are happening. * * @since 1.0.0 * @param <T> type of object that is held */ protected static class DataSet<T> { protected final Object[] dataArray; protected final int dataStartIndex; // inclusive protected final int dataEndIndex; // exclusive protected final int size; private int frontPadding; // locked around modificationLock private int rearPadding; // locked around modificationLock protected DataSet(Object[] dataArray, int frontPadding, int rearPadding) { this(dataArray, frontPadding, dataArray.length - rearPadding, frontPadding, rearPadding); } protected DataSet(Object[] dataArray, int dataStartIndex, int dataEndIndex, int frontPadding, int rearPadding) { this.dataArray = dataArray; this.dataStartIndex = dataStartIndex; this.dataEndIndex = dataEndIndex; this.size = dataEndIndex - dataStartIndex; this.frontPadding = frontPadding; this.rearPadding = rearPadding; } /** * Returns a new {@link DataSet} that contains only relevant and active items in the data * array. The returned DataSet has the same set front and rear padding, so additional * modifications will expand the array based off those set values. * * @return New trimmed {@link DataSet}, or {@code this} if already trimmed */ public DataSet<T> trimToSize() { if (dataStartIndex == 0 && dataEndIndex == dataArray.length) { return this; } else { Object[] newData = new Object[size]; System.arraycopy(dataArray, dataStartIndex, newData, 0, size); return new DataSet<>(newData, 0, size, frontPadding, rearPadding); } } /** * Call to reposition one index to a new index. * * @param origCurrentIndex original index to move * @param origNewIndex new index position * @return a new {@link DataSet} which represents the change (or the same reference if the inputs are a no-op) */ public DataSet<T> reposition(int origCurrentIndex, int origNewIndex) { if (size == 1) { // no-op, moving single item to same position return this; } else if (origNewIndex == size && origCurrentIndex == size - 1) { // no-op, moving end item to end return this; } int currentIndex = origCurrentIndex + dataStartIndex; int newIndex = origNewIndex + dataStartIndex; if (newIndex > currentIndex) { // move right Object[] newData; // will not be allocated till necessary if (newIndex == dataEndIndex) { if (currentIndex == dataStartIndex && dataArray.length - 1 > dataEndIndex && dataArray[dataEndIndex] == null) { // reposition front item to end without an array copy dataArray[dataEndIndex] = dataArray[currentIndex]; return new DataSet<>(dataArray, dataStartIndex + 1, dataEndIndex + 1, frontPadding, rearPadding); } else { newData = new Object[size + frontPadding + rearPadding]; // moving to end can be done with two array copies at most System.arraycopy(dataArray, dataStartIndex, newData, frontPadding, origCurrentIndex); System.arraycopy(dataArray, currentIndex + 1, newData, frontPadding + origCurrentIndex, size - origCurrentIndex - 1); } } else { newData = new Object[size + frontPadding + rearPadding]; // work backwards System.arraycopy(dataArray, newIndex, // write from new position to end newData, frontPadding + origNewIndex, size - origNewIndex); System.arraycopy(dataArray, currentIndex + 1, // write from removed position to new position newData, frontPadding + origCurrentIndex, origNewIndex - origCurrentIndex); System.arraycopy(dataArray, dataStartIndex, // write from start to removed position newData, frontPadding, origCurrentIndex); } newData[frontPadding + origNewIndex - 1] = dataArray[currentIndex]; return new DataSet<>(newData, frontPadding, rearPadding); } else if (newIndex < currentIndex) { // move left Object[] newData; // will not be allocated till necessary if (newIndex == dataStartIndex) { if (dataStartIndex > 0 && currentIndex == dataEndIndex - 1 && dataArray[dataStartIndex - 1] == null) { // reposition the end item to the front without an array copy dataArray[dataStartIndex - 1] = dataArray[currentIndex]; return new DataSet<>(dataArray, dataStartIndex - 1, dataEndIndex - 1, frontPadding, rearPadding); } else { newData = new Object[size + frontPadding + rearPadding]; // moving to front can be done with two array copies at most System.arraycopy(dataArray, dataStartIndex, newData, frontPadding + 1, origCurrentIndex); System.arraycopy(dataArray, currentIndex + 1, newData, frontPadding + origCurrentIndex + 1, dataEndIndex - currentIndex - 1); } } else { newData = new Object[size + frontPadding + rearPadding]; // work forward System.arraycopy(dataArray, dataStartIndex, // write from start to new position newData, frontPadding, origNewIndex); System.arraycopy(dataArray, newIndex, // write from new position to current position newData, frontPadding + origNewIndex + 1, origCurrentIndex - origNewIndex); if (origCurrentIndex < size - 1) { System.arraycopy(dataArray, currentIndex + 1, // write from current position to end newData, frontPadding + origCurrentIndex + 1, size - origCurrentIndex - 1); } } newData[frontPadding + origNewIndex] = dataArray[currentIndex]; return new DataSet<>(newData, frontPadding, rearPadding); } else { // no-op, moving to same position return this; } } /** * Used to get a newly sized array which copies the beginning items till the new size is * reached (or all items if the newSize is larger). The newly created array will also respect * the front and rear padding, which is NOT included in the {@code newSize} provided. * * @param newSize size for data within the new array * @return a new array */ private Object[] getArrayCopy(int newSize) { Object[] newData = new Object[newSize + frontPadding + rearPadding]; System.arraycopy(dataArray, dataStartIndex, newData, frontPadding, Math.min(size, newSize)); return newData; } /** * Returns the item at a given index, this function handles the front padding for you. * * @param index index of item * @return stored item */ @SuppressWarnings("unchecked") public T get(int index) { return (T)dataArray[index + dataStartIndex]; } /** * Call to check for the index of a given item. The return index has already had the front * padding removed from the actual index. * * @param o Object to search for * @return index of item, or -1 if not found */ public int indexOf(Object o) { for (int i = dataStartIndex; i < dataEndIndex; i++) { if (dataArray[i].equals(o)) { return i - dataStartIndex; } } return -1; } /** * Call to check for the last index of a given item. The return index has already had the * front padding removed from the actual index. * * @param o Object to search for * @return index of item, or -1 if not found */ public int lastIndexOf(Object o) { for (int i = dataEndIndex - 1; i >= dataStartIndex; i--) { if (dataArray[i].equals(o)) { return i - dataStartIndex; } } return -1; } /** * Sets a specific index with a given element. * * @param index index to set the item at * @param element element to place in the array * @return a new {@link DataSet} which represents the change */ public DataSet<T> set(int index, T element) { if (index == size) { return addToEnd(element); } else { Object[] newData = getArrayCopy(size); newData[index + frontPadding] = element; return new DataSet<>(newData, frontPadding, rearPadding); } } /** * Adds an item to the front of the structure. * * @param e item to be added * @return a new {@link DataSet} which represents the change */ public DataSet<T> addToFront(T e) { if (dataStartIndex > 0 && dataArray[dataStartIndex - 1] == null) { // there is space in the current array dataArray[dataStartIndex - 1] = e; return new DataSet<>(dataArray, dataStartIndex - 1, dataEndIndex, frontPadding, rearPadding); } else { Object[] newData = new Object[size + 1 + frontPadding + rearPadding]; newData[frontPadding] = e; System.arraycopy(dataArray, dataStartIndex, newData, frontPadding + 1, size); return new DataSet<>(newData, frontPadding, rearPadding); } } /** * Adds an item to the end of the structure. * * @param e item to be added * @return a new {@link DataSet} which represents the change */ public DataSet<T> addToEnd(T e) { if (dataArray.length - 1 >= dataEndIndex && dataArray[dataEndIndex] == null) { // there is space in the current array dataArray[dataEndIndex] = e; return new DataSet<>(dataArray, dataStartIndex, dataEndIndex + 1, frontPadding, rearPadding); } else { Object[] newData = getArrayCopy(size + 1); newData[size + frontPadding] = e; return new DataSet<>(newData, frontPadding, rearPadding); } } /** * Adds an item at a specific index within the structure. * * @param origIndex index to place the item * @param element item to be added * @return a new {@link DataSet} which represents the change */ public DataSet<T> add(int origIndex, T element) { if (origIndex == 0) { // add to front return addToFront(element); } else if (origIndex == size) { // add to end return addToEnd(element); } else { // add into middle Object[] newData = new Object[size + 1 + frontPadding + rearPadding]; System.arraycopy(dataArray, dataStartIndex, newData, frontPadding, origIndex); newData[frontPadding + origIndex] = element; System.arraycopy(dataArray, dataStartIndex + origIndex, newData, frontPadding + origIndex + 1, size - origIndex); return new DataSet<>(newData, frontPadding, rearPadding); } } /** * Adds all items within the collection to the end of the structure. * * @param c collection to add items from * @return a new {@link DataSet} which represents the change, or this reference if the collection is empty */ public DataSet<T> addAll(Collection<? extends T> c) { return addAll(size, c); } /** * Adds all items within the collection at a given index. * * @param origIndex index to start insertion at * @param c collection to add items from * @return a new {@link DataSet} which represents the change, or this reference if the collection is empty */ public DataSet<T> addAll(int origIndex, Collection<? extends T> c) { if (c == null || c.isEmpty()) { return this; } Object[] toAdd = c.toArray(); if (origIndex == 0) { // add to front if (toAdd.length <= dataStartIndex && dataArray[dataStartIndex - 1] == null) { // if previous one is null, all previous ones are null // we can copy the new items in, without copying our existing array System.arraycopy(toAdd, 0, dataArray, dataStartIndex - toAdd.length, toAdd.length); return new DataSet<>(dataArray, dataStartIndex - toAdd.length, dataEndIndex, frontPadding, rearPadding); } else { Object[] newData = new Object[size + toAdd.length + frontPadding + rearPadding]; System.arraycopy(toAdd, 0, newData, frontPadding, toAdd.length); System.arraycopy(dataArray, dataStartIndex, newData, frontPadding + toAdd.length, size); return new DataSet<>(newData, frontPadding, rearPadding); } } else if (origIndex == size) { // add to end if (dataEndIndex + toAdd.length <= dataArray.length && dataArray[dataEndIndex] == null) { // if next one is null, all future ones should be // we can copy the new items in, without copying our existing array System.arraycopy(toAdd, 0, dataArray, dataEndIndex, toAdd.length); return new DataSet<>(dataArray, dataStartIndex, dataEndIndex + toAdd.length, frontPadding, rearPadding); } else { Object[] newData = getArrayCopy(size + toAdd.length); System.arraycopy(toAdd, 0, newData, size + frontPadding, toAdd.length); return new DataSet<>(newData, frontPadding, rearPadding); } } else { // add in middle Object[] newData = new Object[size + toAdd.length + frontPadding + rearPadding]; System.arraycopy(dataArray, dataStartIndex, newData, frontPadding, origIndex); System.arraycopy(toAdd, 0, newData, frontPadding + origIndex, toAdd.length); System.arraycopy(dataArray, dataStartIndex + origIndex, newData, frontPadding + origIndex + toAdd.length, size - origIndex); return new DataSet<>(newData, frontPadding, rearPadding); } } /** * Removes a specific index from the collection. * * @param origIndex index to remove from * @return a new {@link DataSet} which represents the change */ public DataSet<T> remove(int origIndex) { int index = origIndex + dataStartIndex; if (index == dataStartIndex) { // remove from front without copy return new DataSet<>(dataArray, dataStartIndex + 1, dataEndIndex, frontPadding, rearPadding); } else if (index == dataEndIndex - 1) { // remove from end without copy return new DataSet<>(dataArray, dataStartIndex, dataEndIndex - 1, frontPadding, rearPadding); } else { // remove from middle Object[] newData = new Object[size - 1 + frontPadding + rearPadding]; System.arraycopy(dataArray, dataStartIndex, newData, frontPadding, origIndex); System.arraycopy(dataArray, index + 1, newData, frontPadding + origIndex, size - origIndex - 1); return new DataSet<>(newData, frontPadding, rearPadding); } } /** * Removes a specific index from the collection. * * @param c Collection which contains items to be removed * @return a new {@link DataSet} which represents the change, or the same reference if no modification was necessary */ public DataSet<T> removeAll(Collection<?> c) { Object[] resultArray = null; // will only be allocated once modification occurs int i = frontPadding; for (int currentIndex = 0; currentIndex < size; currentIndex++) { Object currItem = this.get(currentIndex); if (! c.contains(currItem)) { if (resultArray != null) { resultArray[i++] = currItem; } else { /* if result array has not been created yet, we will do a single array copy * once a modification occurs. */ i++; } } else { // modification occurred, create array and copy if (resultArray == null) { resultArray = new Object[size + frontPadding + rearPadding]; System.arraycopy(dataArray, dataStartIndex, resultArray, frontPadding, i); } } } if (resultArray != null) { return new DataSet<>(resultArray, frontPadding, i, frontPadding, rearPadding); } else { return this; } } /** * Keeps only the items in the provided collection. * * @param c Collection to examine for items to retain * @return a new {@link DataSet} which represents the change, or the same reference if no modification was necessary */ public DataSet<T> retainAll(Collection<?> c) { Object[] resultArray = null; // will only be allocated once modification occurs int i = frontPadding; for (int currentIndex = 0; currentIndex < size; currentIndex++) { Object currItem = this.get(currentIndex); if (c.contains(currItem)) { if (resultArray != null) { resultArray[i++] = currItem; } else { i++; } } else { // modification occurred, create array and copy if (resultArray == null) { resultArray = new Object[size + frontPadding + rearPadding]; System.arraycopy(dataArray, dataStartIndex, resultArray, frontPadding, i); } } } if (resultArray != null) { return new DataSet<>(resultArray, frontPadding, i, frontPadding, rearPadding); } else { return this; } } @Override public boolean equals(Object o) { if (this == o) { return true; } else if (o instanceof DataSet) { @SuppressWarnings("rawtypes") DataSet ds = (DataSet)o; return equalsEquivelent(ds); } else { return false; } } /** * Call to see if the lists are equivalently equal, meaning that they have the same items in * the same order. This is what most equals operations for lists expect. * * @param ds Other DataSet to compare against (can't be {@code null}) * @return {@code true} if they are equal */ @SuppressWarnings("rawtypes") public boolean equalsEquivelent(DataSet ds) { if (this.size != ds.size) { return false; } for (int i = 0; i < size; i++) { Object thisItem = this.get(i); Object thatItem = ds.get(i); if ((thisItem == null && thatItem != null) || (thisItem != null && ! thisItem.equals(thatItem))) { return false; } } return true; } /** * This is a call to check if the DataSet is exactly equal. This is unique for the design of * this collection. This can be a quicker check, but may result in saying DataSet's are not * equal, when they basically are equal. * * @param ds Other DataSet to compare against (can't be null) * @return {@code true} if they are equal */ @SuppressWarnings("rawtypes") public boolean equalsExactly(DataSet ds) { if (this == ds) { return true; } else { if (dataStartIndex != ds.dataStartIndex || dataEndIndex != ds.dataEndIndex || dataArray.length != ds.dataArray.length) { return false; } else { for (int i = 0; i < dataArray.length; i++) { if ((dataArray[i] == null && ds.dataArray[i] != null) || (dataArray[i] != null && ! dataArray[i].equals(ds.dataArray[i]))) { return false; } } return true; } } } @Override public int hashCode() { int hashCode = 1; for (int i = dataStartIndex; i < dataEndIndex; i++) { hashCode = HASH_CODE_PRIME_NUMBER * hashCode + dataArray[i].hashCode(); } return hashCode; } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append('['); for (int i = 0; i < dataArray.length; i++) { if (i != 0) { result.append(", "); } if (i == dataStartIndex) { result.append('S'); } result.append(i).append('-').append(dataArray[i]); if (i == dataEndIndex - 1) { result.append('E'); } } result.append(']'); return result.toString(); } } }