diff --git a/src/main/java/org/apache/commons/collections4/iterators/AbstractListIteratorDecorator.java b/src/main/java/org/apache/commons/collections4/iterators/AbstractListIteratorDecorator.java index 70db4beb51..f9a9cb2080 100644 --- a/src/main/java/org/apache/commons/collections4/iterators/AbstractListIteratorDecorator.java +++ b/src/main/java/org/apache/commons/collections4/iterators/AbstractListIteratorDecorator.java @@ -28,7 +28,7 @@ public class AbstractListIteratorDecorator implements ListIterator { /** The iterator being decorated */ - private final ListIterator iterator; + private ListIterator iterator; //----------------------------------------------------------------------- /** @@ -54,6 +54,15 @@ protected ListIterator getListIterator() { return iterator; } + /** + * Sets the iterator being decorated. + * + * @param iterator the decorated iterator + */ + protected void setListIterator(ListIterator iterator) { + this.iterator = iterator; + } + //----------------------------------------------------------------------- /** {@inheritDoc} */ diff --git a/src/main/java/org/apache/commons/collections4/list/CopyOnWriteList.java b/src/main/java/org/apache/commons/collections4/list/CopyOnWriteList.java new file mode 100644 index 0000000000..a545db8364 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/CopyOnWriteList.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.ListIterator; +import org.apache.commons.collections4.iterators.AbstractListIteratorDecorator; + +/** The list which copies its original, shared list on + * modifications and then operates on copy. + *

+ * Modifications made to this list copies original list, so + * subsequent read and write operates on copied list. From the + * other hand as long as there is no modification the {@code CopyOnWriteList} + * and {@code originalList} shares same list. After modification new + * backing list is created which can be modified without affecting state of + * {@code originalList} or other {@code CopyOnWriteList} created from + * {@code originalList}. + *

+ *

+ * Following snippet shows how {@link CopyOnWriteList} can be used: + *

+ *
{@code
+ * //Original list build on application startup
+ * private static List originalList = buildOriginalList();
+ *
+ * // Per instance list
+ * private List list = new CopyOnWriteList<>(originalList);
+ *
+ * // Setup instance
+ * private void setup(String condition) {
+ *   if ("rare".equals(condition)) { // Very rare case
+ *      // list shares memory with original list
+ *      list.add(new VerySpecialElement());
+ *      // list has private backing list, which is copy of originalList
+ *      // with new element added
+ *   }
+ * }
+ * }
+ * Notes: + *
    + *
  • + * behavior of this class is different than CopyOnWriteArrayList from JDK, + *
  • + *
  • + * the changes in original list are not watched and will not trigger + * copying elements, + *
  • + *
  • + * this class doesn't clone elements, + *
  • + *
  • + * this class is partly thread safe - copying original list + * is synchronised as other instances wrapping same {@code originalList} + * can do same from other threads. + *
  • + *
+ * + * @param the type of elements + * + * @author Radek Smogura + * @since 4.5 + * @serial + */ +public class CopyOnWriteList extends AbstractListDecorator { + private static final long serialVersionUID = -8926547289582L; + + /** Holds default collection type. */ + public static final Class DEFAULT_COLLECTION_TYPE = ArrayList.class; + + private Class> copiedCollectionType; + + /** Lock used to synchronise copying of original list. */ + private Object copyLock; + + /** Copies original list. + * + * @return instance the instance of the new collection of given type + */ + protected List copyOriginalList() { + // Instantiate new collection of given type; + List instance; + try { + instance = copiedCollectionType.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalStateException("Can't instantiate class " + copiedCollectionType, e); + } + instance.addAll(decorated()); + return instance; + } + + /** Copies original collection. + * @return {@code true} if copy happened + * {@code false} if does not (already copied) + */ + protected boolean copyAndReplaceOriginalList() { + if (copiedCollectionType == null) { + return false; + } + + // Do copy, if needed synchronise + final List copy; + if (copyLock != null) { + synchronized(copyLock) { + copy = copyOriginalList(); + } + } else { + copy = copyOriginalList(); + } + + // Replace original list with copy + setCollection(copy); + + // Prevent future copying and save GC + copiedCollectionType = null; + copyLock = null; + + return true; + } + + /** Creates new copy on write list with default type. + * This constructor is equivalent to + * {@code CopyOnWriteList(originalList, originalList, + * CopyOnWriteList.DEFAULT_COLLECTION_TYPE} + * + * @param originalList collection + */ + public CopyOnWriteList(List originalList) { + this(originalList, originalList, (Class>) DEFAULT_COLLECTION_TYPE); + } + + /** Creates new copy on write list with default type. + * + * @param originalList collection + * @param copyLock the lock used to synchronise copying of original list + * (if {@code null} no synchronisation takes place) + * @param copiedCollectionType the type of new list which should be used + * as backing list + */ + public CopyOnWriteList(List originalList, + Object copyLock, + Class> copiedCollectionType) { + super(originalList); + if (copiedCollectionType == null) { + throw new IllegalArgumentException("Parameter copiedCollectionType can't be null"); + } + this.copyLock = copyLock; + this.copiedCollectionType = copiedCollectionType; + } + + @Override + public boolean add(E object) { + copyAndReplaceOriginalList(); + return super.add(object); + } + + @Override + public void add(int index, E object) { + copyAndReplaceOriginalList(); + super.add(index, object); + } + + @Override + public boolean addAll(Collection coll) { + copyAndReplaceOriginalList(); + return super.addAll(coll); + } + + @Override + public boolean addAll(int index, Collection coll) { + copyAndReplaceOriginalList(); + return super.addAll(index, coll); + } + + @Override + public void clear() { + copyAndReplaceOriginalList(); + super.clear(); + } + + @Override + public E remove(int index) { + copyAndReplaceOriginalList(); + return super.remove(index); + } + + @Override + public boolean remove(Object object) { + copyAndReplaceOriginalList(); + return super.remove(object); + } + + @Override + public boolean removeAll(Collection coll) { + copyAndReplaceOriginalList(); + return super.removeAll(coll); + } + + @Override + public boolean retainAll(Collection coll) { + copyAndReplaceOriginalList(); + return super.retainAll(coll); + } + + @Override + public E set(int index, E object) { + copyAndReplaceOriginalList(); + return super.set(index, object); + } + + @Override + public List subList(int fromIndex, int toIndex) { + final List sub = decorated().subList(fromIndex, toIndex); + return new CopyOnWriteList<>(sub); + } + + @Override + public void sort(Comparator c) { + copyAndReplaceOriginalList(); + super.sort(c); + } + + @Override + public ListIterator listIterator() { + ListIterator originalIterator = decorated().listIterator(); + + return new CopyOnWriteIterator(originalIterator); + } + + @Override + public ListIterator listIterator(int index) { + ListIterator originalIterator = decorated().listIterator(index); + + return new CopyOnWriteIterator(originalIterator); + } + + @Override + public Iterator iterator() { + return listIterator(); + } + + /** Copy-on-write iterator which copies list on modifications and + * uses copied list + */ + private class CopyOnWriteIterator extends AbstractListIteratorDecorator { + /** Iterators are bit more complicated, we have to record which element + * according to position is visited + * In case of modifications we copy list and we replace original + * iterator with new list's iterator and we skip stored number of + * elements (actually... it's high level description) + + * We can do this as list is set of elements each with assigned position + * and list contract requires that iterator will return elements + * in "proper order". + * This doesn't work for general collections as we can't assign + * position to elements, elements can be duplicated, and iterators + * may not return elements in same order + */ + + /** Holds backed list to detect concurrent changes. */ + private List backedList; + + /** Checks if there was a change to list outside this iterator. */ + private void checkConcurrentChanges() { + if (backedList != decorated()) { + throw new ConcurrentModificationException("The list has been changed outside iterator"); + } + } + + /** Copies original list and creates new iterator replacing + * originally decorated iterator. + * + * If copy happened earlier this method is no-op. + * + * @return {@code true} if copy happened + * {@code false} if does not (already copied) + */ + private boolean copyAndReplaceIterator() { + if (copyAndReplaceOriginalList()) { + int currentElement = this.previousIndex(); + // List is replaced, decorated() returns replacement + List copiedList = decorated(); + + // TODO In case of exception we will be in slightly inconsistent state + ListIterator iteratorNew = copiedList.listIterator(currentElement); + iteratorNew.next(); + this.backedList = copiedList; + + setListIterator(iteratorNew); + return true; + } else { + return false; + } + } + + public CopyOnWriteIterator(ListIterator iterator) { + super(iterator); + this.backedList = decorated(); + } + + @Override + public void add(E obj) { + checkConcurrentChanges(); + copyAndReplaceIterator(); + super.add(obj); + } + + @Override + public void set(E obj) { + checkConcurrentChanges(); + copyAndReplaceIterator(); + super.set(obj); + } + + @Override + public void remove() { + checkConcurrentChanges(); + copyAndReplaceIterator(); + super.remove(); + } + + @Override + public E next() { + checkConcurrentChanges(); + return super.next(); + } + } + +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/collections4/list/CopyOnWriteListTest.java b/src/test/java/org/apache/commons/collections4/list/CopyOnWriteListTest.java new file mode 100644 index 0000000000..1e4df056ea --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/CopyOnWriteListTest.java @@ -0,0 +1,417 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list; + +import java.util.*; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertNotSame; + + +/** + * + * @author Radek Smogura + */ +public class CopyOnWriteListTest { + List original; + List expectedList = new ArrayList(); + CopyOnWriteList assertedList; + + ListIterator expected; + ListIterator asserted; + + protected void assertOriginalList() { + assertEquals(3, original.size()); + assertEquals(1, original.get(0).intValue()); + assertEquals(2, original.get(1).intValue()); + assertEquals(3, original.get(2).intValue()); + } + + protected void openIterators() { + expected = expectedList.listIterator(); + asserted = assertedList.listIterator(); + } + + protected void checkIterators(ListIterator expected, ListIterator asserted) { + while (expected.hasNext()) { + assertEquals(expected.hasNext(), asserted.hasNext()); + assertEquals(expected.nextIndex(), asserted.nextIndex()); + + assertEquals(expected.previousIndex(), asserted.previousIndex()); + assertEquals(expected.hasPrevious(), asserted.hasPrevious()); + + assertEquals(expected.next(), asserted.next()); + } + + //And back + while (expected.hasPrevious()) { + assertEquals(expected.hasNext(), asserted.hasNext()); + assertEquals(expected.nextIndex(), asserted.nextIndex()); + + assertEquals(expected.previousIndex(), asserted.previousIndex()); + assertEquals(expected.hasPrevious(), asserted.hasPrevious()); + + assertEquals(expected.previous(), asserted.previous()); + } + } + + protected void check() { + // Checks used iterators + checkIterators(expected, asserted); + + //And check listIteratorsWithIndex() + for (int i=0; i < expectedList.size(); i++) { + checkIterators(expectedList.listIterator(i), assertedList.listIterator(i)); + } + + for (int i=0; i < expectedList.size(); i++) { + assertEquals(expectedList.get(i), assertedList.get(i)); + } + } + + protected void doOnTasks(P param, Task task, T... targets) { + for (T t : targets) { + task.execute(param, t); + } + } + + @Before + public void setup() { + original = new ArrayList(); + + original.add(1); + original.add(2); + original.add(3); + + expectedList = new ArrayList(original); + assertedList = new CopyOnWriteList(original); + } + + @Test + public void testNoCopy() { + for (Integer i : assertedList) { + i.hashCode(); + } + assertSame(original, assertedList.decorated()); + } + + /// Plain operations + @Test + public void testAdd() { + Task> t = new AddElementTask(); + doOnTasks(1, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size() + 1, assertedList.size()); + } + + @Test + public void testInsertIndexed() { + Task> t = new InsertElementTask(); + for (int i=0; i < original.size(); i++) { + setup(); + doOnTasks(i, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size() + 1, assertedList.size()); + } + } + + @Test + public void testAddAll() { + Task> t = new AddAllElementTask(); + doOnTasks(1, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size() + 2, assertedList.size()); + } + + @Test + public void testInsertAllIndexed() { + Task> t = new InsertAllElementTask(); + for (int i=0; i < original.size(); i++) { + setup(); + doOnTasks(i, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size() + 2, assertedList.size()); + } + } + + @Test + public void testRemoveIndexed() { + Task> t = new RemoveIndexedElementTask(); + for (int i=0; i < original.size(); i++) { + setup(); + doOnTasks(i, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size() - 1, assertedList.size()); + } + } + + @Test + public void testRemoveObj() { + Task> t = new RemoveObjElementTask(); + for (int i=0; i < original.size(); i++) { + setup(); + doOnTasks(i+1, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size() - 1, assertedList.size()); + } + } + + @Test + public void testRemoveAll() { + Task> t = new RemoveAllElementTask(); + for (int i=0; i < original.size(); i++) { + setup(); + doOnTasks(i+1, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + } + } + + @Test + public void testRetainAll() { + Task> t = new RetainAllElementTask(); + for (int i=0; i < original.size(); i++) { + setup(); + doOnTasks(i+1, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + } + } + + @Test + public void testSet() { + Task> t = new SetElementTask(); + doOnTasks(1, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertNotSame(original, assertedList.decorated()); + assertEquals(original.size(), assertedList.size()); + } + + @Test + public void testSubList() { + List subList = assertedList.subList(0,assertedList.size()); + assertEquals(subList,assertedList); + subList = assertedList.subList(0,0); + assertEquals(0, subList.size()); + int i = 3; + if(i > assertedList.size()) + { + i = assertedList.size(); + } + subList = assertedList.subList(0,i); + assertEquals(i, subList.size()); + } + + /** + * Test sorting a copy on write list. + */ + @Test + public void testListSort() { + final Comparator comparator = new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return 0; + } + }; + + assertedList.sort(comparator); + + } + + /// Iterators + @Test + public void testIteratorRemoveIdxElement() { + for (int i=0; i < original.size(); i++) { + setup(); + openIterators(); + Task> t = new RemoveIteratorElementTask(); + doOnTasks(i + 1, t, expected, asserted); + check(); + assertOriginalList(); + assertEquals(original.size() - 1, assertedList.size()); + } + } + + @Test + public void testIteratorSetElement() { + for (int i=0; i < original.size(); i++) { + setup(); + openIterators(); + Task> t = new SetIteratorElementTask(); + doOnTasks(i + 1, t, expected, asserted); + check(); + assertOriginalList(); + } + } + + @Test + public void testIteratorAddElement() { + for (int i=0; i < original.size(); i++) { + setup(); + openIterators(); + Task> t = new AddIteratorElementTask(); + doOnTasks(i + 1, t, expected, asserted); + check(); + assertOriginalList(); + assertEquals(original.size() + 1, assertedList.size()); + } + } + + //// Misc tests + @Test + public void testConcurrentModification() { + Iterator i = assertedList.iterator(); + assertedList.remove(2); + try { + i.next(); + fail("Expected ConcurrentModificationException"); + }catch(ConcurrentModificationException ignored) { + + } + } + @Test + public void testClear() { + Task> t = new ClearElementsTask(); + doOnTasks(0, t, expectedList, assertedList); + openIterators(); + check(); + assertOriginalList(); + assertEquals(0, assertedList.size()); + } + + private class AddElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.add(10); + } + }; + + private class InsertElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.add(param, 11); + } + }; + + private class AddAllElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.addAll(Arrays.asList(14, 15)); + } + }; + + private class InsertAllElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.addAll(param, Arrays.asList(14, 15)); + } + }; + + private class RemoveIndexedElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.remove(param.intValue()); + } + }; + + private class RemoveObjElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.remove(param); + } + }; + + private class RemoveAllElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.removeAll(Arrays.asList(param, param + 1)); + } + }; + + private class RetainAllElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.retainAll(Arrays.asList(param, param + 1)); + } + }; + + private class SetElementTask implements Task> { + @Override + public void execute(Integer param, List integerList) { + integerList.set(param,10); + } + }; + + private class SetIteratorElementTask implements Task> { + @Override + public void execute(Integer param, ListIterator i) { + for (int j=0; j < param; j++) i.next(); + i.set(10); + } + }; + private class RemoveIteratorElementTask implements Task> { + @Override + public void execute(Integer param, ListIterator i) { + for (int j=0; j < param; j++) i.next(); + i.remove(); + } + }; + private class AddIteratorElementTask implements Task> { + @Override + public void execute(Integer param, ListIterator i) { + for (int j=0; j < param; j++) i.next(); + i.add(11); + } + }; + private class ClearElementsTask implements Task> { + @Override + public void execute(Integer param, List i) { + i.clear(); + } + }; + + private interface Task { + void execute(P param, T i); + } +} \ No newline at end of file