Skip to content

Commit 6dd0183

Browse files
committed
Fix spliterator size hint in CloseableIterator.spliterator().
We now report -1 as size to avoid zero-size results for count() or toList() operators. Closes #2519
1 parent a21d596 commit 6dd0183

File tree

3 files changed

+143
-4
lines changed

3 files changed

+143
-4
lines changed

Diff for: src/main/java/org/springframework/data/util/CloseableIterator.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2021 the original author or authors.
2+
* Copyright 2015-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@
1818
import java.io.Closeable;
1919
import java.util.Iterator;
2020
import java.util.Spliterator;
21-
import java.util.Spliterators;
2221
import java.util.stream.Stream;
2322
import java.util.stream.StreamSupport;
2423

@@ -49,12 +48,14 @@ public interface CloseableIterator<T> extends Iterator<T>, Closeable {
4948
* The default implementation should be overridden by subclasses that can return a more efficient spliterator. To
5049
* preserve expected laziness behavior for the {@link #stream()} method, spliterators should either have the
5150
* characteristic of {@code IMMUTABLE} or {@code CONCURRENT}, or be late-binding.
51+
* <p>
52+
* The default implementation does not report a size.
5253
*
5354
* @return a {@link Spliterator} over the elements in this {@link Iterator}.
5455
* @since 2.4
5556
*/
5657
default Spliterator<T> spliterator() {
57-
return Spliterators.spliterator(this, 0, 0);
58+
return new IteratorSpliterator<>(this);
5859
}
5960

6061
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.util;
17+
18+
import java.util.Comparator;
19+
import java.util.Iterator;
20+
import java.util.Spliterator;
21+
import java.util.Spliterators;
22+
import java.util.function.Consumer;
23+
24+
/**
25+
* A Spliterator using a given Iterator for element operations. The spliterator implements {@code trySplit} to permit
26+
* limited parallelism.
27+
*/
28+
class IteratorSpliterator<T> implements Spliterator<T> {
29+
30+
private static final int BATCH_UNIT = 1 << 10; // batch array size increment
31+
private static final int MAX_BATCH = 1 << 25; // max batch array size;
32+
private final Iterator<? extends T> it;
33+
private long est; // size estimate
34+
private int batch; // batch size for splits
35+
36+
/**
37+
* Creates a spliterator using the given iterator for traversal, and reporting the given initial size and
38+
* characteristics.
39+
*
40+
* @param iterator the iterator for the source
41+
*/
42+
public IteratorSpliterator(Iterator<? extends T> iterator) {
43+
this.it = iterator;
44+
this.est = Long.MAX_VALUE;
45+
}
46+
47+
@Override
48+
public Spliterator<T> trySplit() {
49+
/*
50+
* Split into arrays of arithmetically increasing batch
51+
* sizes. This will only improve parallel performance if
52+
* per-element Consumer actions are more costly than
53+
* transferring them into an array. The use of an
54+
* arithmetic progression in split sizes provides overhead
55+
* vs parallelism bounds that do not particularly favor or
56+
* penalize cases of lightweight vs heavyweight element
57+
* operations, across combinations of #elements vs #cores,
58+
* whether or not either are known. We generate
59+
* O(sqrt(#elements)) splits, allowing O(sqrt(#cores))
60+
* potential speedup.
61+
*/
62+
Iterator<? extends T> i = it;
63+
long s = est;
64+
if (s > 1 && i.hasNext()) {
65+
int n = batch + BATCH_UNIT;
66+
if (n > s) {
67+
n = (int) s;
68+
}
69+
if (n > MAX_BATCH) {
70+
n = MAX_BATCH;
71+
}
72+
Object[] a = new Object[n];
73+
int j = 0;
74+
do {
75+
a[j] = i.next();
76+
} while (++j < n && i.hasNext());
77+
batch = j;
78+
if (est != Long.MAX_VALUE) {
79+
est -= j;
80+
}
81+
return Spliterators.spliterator(a, 0, j, 0);
82+
}
83+
return null;
84+
}
85+
86+
@Override
87+
public void forEachRemaining(Consumer<? super T> action) {
88+
it.forEachRemaining(action);
89+
}
90+
91+
@Override
92+
public boolean tryAdvance(Consumer<? super T> action) {
93+
if (it.hasNext()) {
94+
action.accept(it.next());
95+
return true;
96+
}
97+
return false;
98+
}
99+
100+
@Override
101+
public long estimateSize() {
102+
return -1;
103+
}
104+
105+
@Override
106+
public int characteristics() {
107+
return 0;
108+
}
109+
110+
@Override
111+
public Comparator<? super T> getComparator() {
112+
if (hasCharacteristics(Spliterator.SORTED)) {
113+
return null;
114+
}
115+
throw new IllegalStateException();
116+
}
117+
}

Diff for: src/test/java/org/springframework/data/util/CloseableIteratorUnitTests.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import java.util.Iterator;
2222
import java.util.List;
2323
import java.util.stream.Collectors;
24+
import java.util.stream.IntStream;
2425
import java.util.stream.Stream;
2526

2627
import org.junit.jupiter.api.Test;
@@ -43,6 +44,26 @@ void shouldCreateStream() {
4344
assertThat(iterator.closed).isFalse();
4445
}
4546

47+
@Test // GH-2519
48+
void shouldCount() {
49+
50+
CloseableIteratorImpl<String> iterator = new CloseableIteratorImpl<>(Arrays.asList("1", "2", "3").iterator());
51+
52+
long count = iterator.stream().count();
53+
54+
assertThat(count).isEqualTo(3);
55+
}
56+
57+
@Test // GH-2519
58+
void shouldCountLargeStream() {
59+
60+
CloseableIteratorImpl<Integer> iterator = new CloseableIteratorImpl<>(IntStream.range(0, 2048).boxed().iterator());
61+
62+
long count = iterator.stream().count();
63+
64+
assertThat(count).isEqualTo(2048);
65+
}
66+
4667
@Test // DATACMNS-1637
4768
void closeStreamShouldCloseIterator() {
4869

0 commit comments

Comments
 (0)