Skip to content

Commit 845c0e5

Browse files
authored
[collections-722] deeply nested chainedIterator performance problems fixed (#628)
* [collections-722] basically 'renovated' stale PR commons-collections/pull/308 from collections-770, ensures that rechaining chainedIterators multiple times now result in a single IteratorChain instance with all picked up underlying iterators, thus avoiding deeply nested imperformant IteratorChain instances. Additionally the UnmodifiableIterator gets a special treatment, so that SetUtils.SetView.iterator() also benefits from this solution. Raised test coverage. * [collections-722] added 722 to changes.xml
1 parent ec38f6f commit 845c0e5

File tree

4 files changed

+105
-2
lines changed

4 files changed

+105
-2
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix exception message in org.apache.commons.collections4.functors.FunctorUtils.validate(Consumer...)</action>
3434
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix exception message in org.apache.commons.collections4.iterators.UnmodifiableIterator.remove() to match java.util.Iterator.remove().</action>
3535
<action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Joerg Budischewski" issue="COLLECTIONS-838">Calling SetUtils.union on multiple instances of SetView causes JVM to hang</action>
36+
<action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Joerg Budischewski" issue="COLLECTIONS-722">Improve IteratorUtils.chainedIterator() performance.</action>
3637
<!-- ADD -->
3738
<action type="add" dev="ggregory" due-to="Gary Gregory">Add generics to UnmodifiableIterator for the wrapped type.</action>
3839
<!-- UPDATE -->

src/main/java/org/apache/commons/collections4/iterators/IteratorChain.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,29 @@ public IteratorChain(final Iterator<? extends E> first, final Iterator<? extends
168168
*/
169169
public void addIterator(final Iterator<? extends E> iterator) {
170170
checkLocked();
171-
iteratorQueue.add(Objects.requireNonNull(iterator, "iterator"));
171+
Objects.requireNonNull(iterator, "iterator");
172+
if (iterator instanceof UnmodifiableIterator) {
173+
final Iterator<? extends E> underlyingIterator = ((UnmodifiableIterator) iterator).unwrap();
174+
if (underlyingIterator instanceof IteratorChain) {
175+
// in case it is an IteratorChain, wrap every underlying iterators as unmodifiable
176+
// multiple rechainings would otherwise lead to exponential growing number of function calls
177+
// when the iteratorChain gets used.
178+
for (Iterator<? extends E> nestedIterator : ((IteratorChain<? extends E>) underlyingIterator).iteratorQueue) {
179+
iteratorQueue.add(UnmodifiableIterator.unmodifiableIterator(nestedIterator));
180+
}
181+
} else {
182+
// we don't know anything about the underlying iterator, simply add it here
183+
iteratorQueue.add(iterator);
184+
}
185+
} else if (iterator instanceof IteratorChain) {
186+
// add the wrapped iterators directly instead of reusing the given instance
187+
// multiple rechainings would otherwise lead to exponential growing number of function calls
188+
// when the iteratorChain gets used.
189+
iteratorQueue.addAll(((IteratorChain) iterator).iteratorQueue);
190+
} else {
191+
// arbitrary other iterator
192+
iteratorQueue.add(iterator);
193+
}
172194
}
173195

174196
/**

src/main/java/org/apache/commons/collections4/iterators/UnmodifiableIterator.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,4 @@ public void remove() {
9191
T unwrap() {
9292
return iterator;
9393
}
94-
9594
}

src/test/java/org/apache/commons/collections4/iterators/IteratorChainTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@
2525
import java.time.Duration;
2626
import java.util.ArrayList;
2727
import java.util.Arrays;
28+
import java.util.HashMap;
2829
import java.util.Iterator;
2930
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Map.Entry;
3033
import java.util.NoSuchElementException;
34+
import java.util.Set;
3135

3236
import org.apache.commons.collections4.IteratorUtils;
3337
import org.apache.commons.collections4.Predicate;
@@ -42,10 +46,14 @@ public class IteratorChainTest extends AbstractIteratorTest<String> {
4246
protected String[] testArray = {
4347
"One", "Two", "Three", "Four", "Five", "Six"
4448
};
49+
protected String[] testArray1234 = {
50+
"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight"
51+
};
4552

4653
protected List<String> list1;
4754
protected List<String> list2;
4855
protected List<String> list3;
56+
protected List<String> list4;
4957

5058
public List<String> getList1() {
5159
return list1;
@@ -89,6 +97,9 @@ public void setUp() {
8997
list3 = new ArrayList<>();
9098
list3.add("Five");
9199
list3.add("Six");
100+
list4 = new ArrayList<>();
101+
list4.add("Seven");
102+
list4.add("Eight");
92103
}
93104

94105
@Test
@@ -149,9 +160,14 @@ void testConstructList() {
149160
expected.addAll(list2);
150161
expected.addAll(list3);
151162
final IteratorChain<String> iter = new IteratorChain<>(list);
163+
assertEquals(iter.size(), list.size());
164+
assertFalse(iter.isLocked());
152165
final List<String> actual = new ArrayList<>();
153166
iter.forEachRemaining(actual::add);
154167
assertEquals(actual, expected);
168+
assertTrue(iter.isLocked());
169+
assertThrows(UnsupportedOperationException.class, () -> iter.addIterator(list1.iterator()),
170+
"adding iterators after iteratorChain has been traversed must fail");
155171
}
156172

157173
@Test
@@ -263,4 +279,69 @@ void testRemoveFromFilteredIterator() {
263279
assertEquals(1, list2.size());
264280
}
265281

282+
@Test
283+
public void testChainOfChains() {
284+
final Iterator<String> iteratorChain1 = new IteratorChain<>(list1.iterator(), list2.iterator());
285+
final Iterator<String> iteratorChain2 = new IteratorChain<>(list3.iterator(), list4.iterator());
286+
final Iterator<String> iteratorChainOfChains = new IteratorChain<>(iteratorChain1, iteratorChain2);
287+
288+
for (final String testValue : testArray1234) {
289+
final String iterValue = (String) iteratorChainOfChains.next();
290+
assertEquals(testValue, iterValue, "Iteration value is correct");
291+
}
292+
293+
assertFalse(iteratorChainOfChains.hasNext(), "Iterator should now be empty");
294+
assertThrows(NoSuchElementException.class, iteratorChainOfChains::next, "NoSuchElementException must be thrown");
295+
}
296+
297+
@Test
298+
public void testChainOfUnmodifiableChains() {
299+
final Iterator<String> iteratorChain1 = new IteratorChain<>(list1.iterator(), list2.iterator());
300+
final Iterator<String> unmodifiableChain1 = IteratorUtils.unmodifiableIterator(iteratorChain1);
301+
final Iterator<String> iteratorChain2 = new IteratorChain<>(list3.iterator(), list4.iterator());
302+
final Iterator<String> unmodifiableChain2 = IteratorUtils.unmodifiableIterator(iteratorChain2);
303+
final Iterator<String> iteratorChainOfChains = new IteratorChain<>(unmodifiableChain1, unmodifiableChain2);
304+
305+
for (final String testValue : testArray1234) {
306+
final String iterValue = (String) iteratorChainOfChains.next();
307+
assertEquals(testValue, iterValue, "Iteration value is correct");
308+
}
309+
310+
assertFalse(iteratorChainOfChains.hasNext(), "Iterator should now be empty");
311+
assertThrows(NoSuchElementException.class, iteratorChainOfChains::next, "NoSuchElementException must be thrown");
312+
}
313+
314+
@Test
315+
public void testChainOfUnmodifiableChainsRetainsUnmodifiableBehaviourOfNestedIterator() {
316+
final Iterator<String> iteratorChain1 = new IteratorChain<>(list1.iterator(), list2.iterator());
317+
final Iterator<String> unmodifiableChain1 = IteratorUtils.unmodifiableIterator(iteratorChain1);
318+
final Iterator<String> iteratorChain2 = new IteratorChain<>(list3.iterator(), list4.iterator());
319+
final Iterator<String> unmodifiableChain2 = IteratorUtils.unmodifiableIterator(iteratorChain2);
320+
final Iterator<String> iteratorChainOfChains = new IteratorChain<>(unmodifiableChain1, unmodifiableChain2);
321+
322+
iteratorChainOfChains.next();
323+
assertThrows(UnsupportedOperationException.class, iteratorChainOfChains::remove,
324+
"Calling remove must fail when nested iterator is unmodifiable");
325+
}
326+
327+
@Test
328+
public void testMultipleChainedIteratorPerformWellCollections722() {
329+
final Map<Integer, List<Integer>> source = new HashMap<>();
330+
for (int i = 0; i < 50; i++) {
331+
source.put(i, Arrays.asList(1, 2, 3));
332+
}
333+
334+
Iterator<Integer> iterator = IteratorUtils.emptyIterator();
335+
final Set<Entry<Integer, List<Integer>>> entries = source.entrySet();
336+
for (final Entry<Integer, List<Integer>> entry : entries) {
337+
final Iterator<Integer> next = entry.getValue().iterator();
338+
iterator = IteratorUtils.chainedIterator(iterator, next);
339+
}
340+
final Iterator<Integer> lastIterator = iterator;
341+
assertTimeoutPreemptively(Duration.ofSeconds(2), () -> {
342+
while (lastIterator.hasNext()) {
343+
lastIterator.next().toString();
344+
}
345+
});
346+
}
266347
}

0 commit comments

Comments
 (0)