Skip to content

Commit 038609b

Browse files
authored
Make lists returned from groupBy calls unmodifiable (#95)
+ Reimplement integrator to be slightly easier to follow + FWIW I also reimplemented this entirely with the `GroupChangingGatherer` but it felt clumsy so here we are
1 parent eb67150 commit 038609b

File tree

3 files changed

+33
-20
lines changed

3 files changed

+33
-20
lines changed

src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -472,17 +472,17 @@ public static <INPUT> GroupChangingGatherer<INPUT> groupNonIncreasing(final Comp
472472
return GroupChangingGatherer.usingComparator(ChangingOperation.NonIncreasing, comparator);
473473
}
474474

475-
/// Turn a `Stream<INPUT>` into a `Stream<List<INPUT>>` where consecutive
476-
/// equal elements, where equality is measured by `Object.equals(Object)`.
475+
/// Turn a `Stream<INPUT>` into a `Stream<List<INPUT>>` where consecutive equal elements are in the same `List`
476+
/// and equality is measured by `Object.equals(Object)`. The lists emitted to the output stream are unmodifiable.
477477
///
478478
/// @param <INPUT> Type of elements in the input stream
479479
/// @return A non-null `GroupingByGatherer`
480480
public static <INPUT extends @Nullable Object> GroupingByGatherer<INPUT> grouping() {
481481
return new GroupingByGatherer<>();
482482
}
483483

484-
/// Turn a `Stream<INPUT>` into a `Stream<List<INPUT>>` where consecutive
485-
/// equal elements, where equality is measured by the given `mappingFunction`, are in the same `List`.
484+
/// Turn a `Stream<INPUT>` into a `Stream<List<INPUT>>` where consecutive equal elements are in the same `List`
485+
/// and equality is measured by the given `mappingFunction`. The lists emitted to the output stream are unmodifiable.
486486
///
487487
/// @param mappingFunction A non-null function, the results of which are used to measure equality of consecutive elements.
488488
/// @param <INPUT> Type of elements in the input stream

src/main/java/com/ginsberg/gatherers4j/GroupingByGatherer.java

+11-16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.jspecify.annotations.Nullable;
2020

2121
import java.util.ArrayList;
22+
import java.util.Collections;
2223
import java.util.List;
2324
import java.util.function.BiConsumer;
2425
import java.util.function.Function;
@@ -38,7 +39,7 @@ public class GroupingByGatherer<INPUT extends @Nullable Object> implements
3839
mappingFunction = null;
3940
}
4041

41-
GroupingByGatherer(Function<@Nullable INPUT, @Nullable Object> mappingFunction) {
42+
GroupingByGatherer(final Function<@Nullable INPUT, @Nullable Object> mappingFunction) {
4243
mustNotBeNull(mappingFunction, "Mapping function must not be null");
4344
this.mappingFunction = mappingFunction;
4445
}
@@ -51,34 +52,28 @@ public Supplier<State<INPUT>> initializer() {
5152
@Override
5253
public Integrator<State<INPUT>, INPUT, List<INPUT>> integrator() {
5354
return Integrator.ofGreedy((state, element, downstream) -> {
54-
final Object thisMatch = mappingFunction == null ? element: mappingFunction.apply(element);
55-
if (state.working == null) {
55+
final Object thisMappedElement = mappingFunction == null ? element: mappingFunction.apply(element);
56+
if (!state.working.isEmpty() && !safeEquals(state.previousMappedElement, thisMappedElement)) {
57+
downstream.push(Collections.unmodifiableList(state.working));
5658
state.working = new ArrayList<>();
57-
state.working.add(element);
58-
state.pastMatch = thisMatch;
59-
} else if (!safeEquals(state.pastMatch, thisMatch)) {
60-
downstream.push(state.working);
61-
state.working = new ArrayList<>();
62-
state.working.add(element);
63-
state.pastMatch = thisMatch;
64-
} else {
65-
state.working.add(element);
6659
}
60+
state.previousMappedElement = thisMappedElement;
61+
state.working.add(element);
6762
return !downstream.isRejecting();
6863
});
6964
}
7065

7166
@Override
7267
public BiConsumer<State<INPUT>, Downstream<? super List<INPUT>>> finisher() {
7368
return (state, downstream) -> {
74-
if (state.working != null) {
75-
downstream.push(state.working);
69+
if (!state.working.isEmpty()) {
70+
downstream.push(Collections.unmodifiableList(state.working));
7671
}
7772
};
7873
}
7974

8075
public static class State<INPUT> {
81-
@Nullable Object pastMatch = null;
82-
@Nullable List<INPUT> working = null;
76+
@Nullable Object previousMappedElement = null;
77+
List<INPUT> working = new ArrayList<>();
8378
}
8479
}

src/test/java/com/ginsberg/gatherers4j/GroupingByGathererTest.java

+18
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ void groupingByIdentity() {
7474
);
7575
}
7676

77+
@SuppressWarnings("DataFlowIssue")
7778
@Test
7879
void mappingFunctionMustNotBeNull() {
7980
assertThatThrownBy(() -> new GroupingByGatherer<>(null)).isInstanceOf(IllegalArgumentException.class);
@@ -96,6 +97,23 @@ void nullsMatch() {
9697
);
9798
}
9899

100+
@Test
101+
void returnedListUnmodifiable() {
102+
// Arrange
103+
final Stream<String> input = Stream.of("A", "A", "B", "B", "C", "C", "C");
104+
105+
// Act
106+
final List<List<String>> output = input.gather(Gatherers4j.grouping()).toList();
107+
108+
// Assert
109+
assertThat(output).hasSize(3);
110+
output.forEach(it ->
111+
assertThatThrownBy(() ->
112+
it.add("D")
113+
).isInstanceOf(UnsupportedOperationException.class)
114+
);
115+
}
116+
99117
@Test
100118
void singleElementStream() {
101119
// Arrange

0 commit comments

Comments
 (0)