Skip to content

Commit

Permalink
MoreCollectors.toImmutableMap() convenience method (#328)
Browse files Browse the repository at this point in the history
  • Loading branch information
iamdanfox authored May 23, 2024
1 parent 0460f29 commit ecd5246
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 19 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,25 @@ the order in which they complete.
[ListenableFuture]: https://google.github.io/guava/releases/23.0/api/docs/com/google/common/util/concurrent/ListenableFuture.html
[Map]: https://docs.oracle.com/javase/8/docs/api/java/util/Map.html
[Stream]: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html

## MoreCollectors

### `MoreCollectors.toImmutableMap()`

Collect a Stream of Map.Entry (e.g. a StreamEx EntryStream) into a Guava ImmutableMap, which preserves iteration order.

Beware that using StreamEx's `EntryStream#toImmutableMap()` does NOT preserve iteration order, as it uses a regular HashMap under the hood.

```diff
StreamEx.of(items)
.mapToEntry(
key -> computeValue(key.foo())
- .toImmutableMap() // does not preserve iteration order
+ .collect(MoreCollectors.toImmutableMap()); // preserves iteration order
```

This is equivalent to writing out the slightly more verbose:

```java
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
```
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-328.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: MoreCollectors.toImmutableMap() convenience method
links:
- https://github.com/palantir/streams/pull/328
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ public final class MoreCollectors {
ImmutableList.Builder::build);
}

/**
* Collect a Stream of Map.Entry (e.g. a StreamEx EntryStream) into a Guava {@link ImmutableMap}, which preserves
* iteration order. Duplicate keys will result in an error. Throws NullPointerException if any key or value is null.
*
* For behaviour details, see docs on {@link ImmutableMap#toImmutableMap}.
*
* Beware that {@code EntryStream#toImmutableMap()} does NOT preserve iteration order, as it uses a regular HashMap.
*/
public static <K, V> Collector<Map.Entry<K, V>, ?, ImmutableMap<K, V>> toImmutableMap() {
return ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue);
}

/**
* This collector has similar semantics to {@link Collectors#toMap} except that the resulting map will be
* immutable. Duplicate keys will result in an error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.google.common.collect.Maps;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class MoreCollectorsTests {
Expand Down Expand Up @@ -60,27 +63,68 @@ public void test_parallel_immutable_set() {

private Function<Integer, Integer> valueMap = x -> x * 2;

@Test
public void test_immutable_map() {
Map<Integer, Integer> map = LARGE_LIST.stream().collect(MoreCollectors.toImmutableMap(k -> k, valueMap));
assertThat(map.keySet()).containsExactlyElementsOf(LARGE_LIST);
map.forEach((k, _v) -> assertThat(map.get(k)).isEqualTo(valueMap.apply(k)));
}
@Nested
class ToImmutableMapDeprecated {
@Test
public void test_immutable_map() {
Map<Integer, Integer> map = LARGE_LIST.stream().collect(MoreCollectors.toImmutableMap(k -> k, valueMap));
assertThat(map.keySet()).containsExactlyElementsOf(LARGE_LIST);
map.forEach((k, _v) -> assertThat(map.get(k)).isEqualTo(valueMap.apply(k)));
}

@Test
@SuppressWarnings("DangerousParallelStreamUsage") // explicitly testing parallel streams
public void test_parallel_immutable_map() {
Map<Integer, Integer> map =
LARGE_LIST.parallelStream().collect(MoreCollectors.toImmutableMap(k -> k, valueMap));
assertThat(map.keySet()).containsExactlyElementsOf(LARGE_LIST);
map.forEach((k, _v) -> assertThat(map.get(k)).isEqualTo(valueMap.apply(k)));
@Test
@SuppressWarnings("DangerousParallelStreamUsage") // explicitly testing parallel streams
public void test_parallel_immutable_map() {
Map<Integer, Integer> map =
LARGE_LIST.parallelStream().collect(MoreCollectors.toImmutableMap(k -> k, valueMap));
assertThat(map.keySet()).containsExactlyElementsOf(LARGE_LIST);
map.forEach((k, _v) -> assertThat(map.get(k)).isEqualTo(valueMap.apply(k)));
}

@Test
public void test_immutable_map_duplicate_keys() {
Stream<Integer> stream = Stream.of(1, 1);
assertThatThrownBy(() -> stream.collect(MoreCollectors.toImmutableMap(k -> k, _k -> 2)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Multiple entries with same key: 1=2 and 1=2");
}
}

@Test
public void test_immutable_map_duplicate_keys() {
Stream<Integer> stream = Stream.of(1, 1);
assertThatThrownBy(() -> stream.collect(MoreCollectors.toImmutableMap(k -> k, _k -> 2)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Multiple entries with same key: 1=2 and 1=2");
@Nested
class ToImmutableMap {

@Test
void toImmutableMap_preserves_iteration_order() {
Map<Integer, Integer> map = LARGE_LIST.stream()
.map(i -> Maps.immutableEntry(i, valueMap.apply(i)))
.collect(MoreCollectors.toImmutableMap());
assertThat(map.keySet()).containsExactlyElementsOf(LARGE_LIST);
map.forEach((k, _v) -> assertThat(map.get(k)).isEqualTo(valueMap.apply(k)));
}

@Test
public void toImmutableMap_throws_on_duplicate_keys() {
AtomicInteger counter = new AtomicInteger(888);
Stream<Map.Entry<Integer, Integer>> stream =
Stream.of(1, 1).map(i -> Maps.immutableEntry(i, counter.getAndIncrement()));

assertThatThrownBy(() -> stream.collect(MoreCollectors.toImmutableMap()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Multiple entries with same key: 1=889 and 1=888");
}

@Test
void throws_if_keys_are_null() {
assertThatThrownBy(() -> Stream.of(Maps.immutableEntry(null, 1)).collect(MoreCollectors.toImmutableMap()))
.isInstanceOf(NullPointerException.class)
.hasMessage("null key in entry: null=1");
}

@Test
void throws_if_values_are_null() {
assertThatThrownBy(() -> Stream.of(Maps.immutableEntry(1, null)).collect(MoreCollectors.toImmutableMap()))
.isInstanceOf(NullPointerException.class)
.hasMessage("null value in entry: 1=null");
}
}
}

0 comments on commit ecd5246

Please sign in to comment.