530

Collectors.toMap throws a NullPointerException if one of the values is null. I don't understand this behaviour, maps can contain null pointers as value without any problems. Is there a good reason why values cannot be null for Collectors.toMap?

Also, is there a nice Java 8 way of fixing this, or should I revert to plain old for loop?

An example of my problem:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


class Answer {
    private int id;

    private Boolean answer;

    Answer() {
    }

    Answer(int id, Boolean answer) {
        this.id = id;
        this.answer = answer;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Boolean getAnswer() {
        return answer;
    }

    public void setAnswer(Boolean answer) {
        this.answer = answer;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Answer> answerList = new ArrayList<>();

        answerList.add(new Answer(1, true));
        answerList.add(new Answer(2, true));
        answerList.add(new Answer(3, null));

        Map<Integer, Boolean> answerMap =
        answerList
                .stream()
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
    }
}

Stacktrace:

Exception in thread "main" java.lang.NullPointerException
    at java.util.HashMap.merge(HashMap.java:1216)
    at java.util.stream.Collectors.lambda$toMap$168(Collectors.java:1320)
    at java.util.stream.Collectors$$Lambda$5/1528902577.accept(Unknown Source)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at Main.main(Main.java:48)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

This problem still exists in Java 11.

M. Justin
  • 14,487
  • 7
  • 91
  • 130
Jasper
  • 6,076
  • 2
  • 14
  • 24
  • 6
    `null` always was a bit problematic, like in TreeMap. Maybe a nice moment to try out `Optional`? Otherwise split and use filter. – Joop Eggen Jul 08 '14 at 11:58
  • 14
    @JoopEggen `null` could be a problem for a key, but in this case it's the value. – gontard Jul 08 '14 at 12:13
  • 1
    Not all maps have problem with `null`, `HashMap` for example can have one `null` key and any number of `null` values, you can try creating a custom `Collector` using a `HashMap` instead of using the default one. – kajacx Jul 08 '14 at 13:29
  • 6
    @kajacx But the default implementation is `HashMap` - as shown in the first line of stacktrace. The problem is not that a `Map` can't hold `null` value, but that the second argument of `Map#merge` function can't be null. – czerny May 12 '15 at 21:33
  • 1
    Personally, with the given circumstances, I would go with non-stream solution, or forEach() if the input is parallel. The nice short stream based solutions below could have a terrible performance. – Ondra Žižka Nov 30 '18 at 14:08
  • You have a better solution for your problem by @kajacx – Ikthiander Mar 27 '19 at 09:00
  • I just discovered this today ! I really don't understand the use of Collectors.toMap() then ! It is like if with collect into a list and can't add null values. I fear that at several locations my code is failing for that. – François F. Feb 14 '23 at 14:23

13 Answers13

505

You can work around this known bug in OpenJDK with this:

Map<Integer, Boolean> collect = list.stream()
        .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);

It is not that much pretty, but it works. Result:

1: true
2: true
3: null

(this tutorial helped me the most.)

EDIT:

Unlike Collectors.toMap, this will silently replace values if you have the same key multiple times, as @mmdemirbas pointed out in the comments. If you don't want this, look at the link in the comment.

kajacx
  • 12,361
  • 5
  • 43
  • 70
  • Nice example. Do you know if a similar syntax is available for `TreeMap` with passing a comparator to `new`? – Jagger Feb 16 '15 at 13:06
  • 3
    @Jagger yes, a definition of a supplier (the first argument) is a function that passes no parameters and returns a result, thus the lambda for your case would be `() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)` to create a case insensitive `String` keyed `TreeMap`. – Brett Ryan Oct 19 '15 at 12:58
  • 2
    This is the correct answer, and IMHO what the JDK should be doing for its default non-overloaded version instead. Maybe merge is faster, but I have not tested. – Brett Ryan Oct 19 '15 at 12:59
  • 1
    I had to specify the type parameters in order to compile, that way: `Map collect = list.stream().collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);`. I had: `incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map,java.util.Map) method java.util.Map.putAll(java.util.Map) is not applicable (actual and formal argument lists differ in length)` – Anthony O. Feb 18 '16 at 10:39
  • This is a fancy way of @gontard solution (rephrasing the classical instructions to lambda expression) but works fine!..Thanks ! – Shessuky Nov 03 '18 at 12:20
  • 3
    This might be quite slow on a large input. You create a `HashMap` and then call `putAll()` for every single entry. Personally, at given circumstances, I would go with non-stream solution, or `forEach()` if the input is parallel. – Ondra Žižka Nov 30 '18 at 14:06
  • "and then call putAll() for every single entry" is that how it works? I thought that is used only if the stream is parralel to join the results. – kajacx Nov 30 '18 at 15:14
  • `Map.merge` can be avoided also by switching from `toMap` to `Collectors.groupingBy` but only when it's certain that there are no duplicate keys. Downside here is that you need to use `Collectors.reduce` or similar for the values instead of the simple method reference. – Tuomas Kiviaho Apr 16 '19 at 10:01
  • @kajacx you are correct. That comment is simply wrong unless you have one thread in your pool per element in the stream. – Taugenichts Apr 24 '19 at 16:26
  • 10
    Beware that this solution behaves differently than the original toMap implementation. Original implementation detects duplicate keys and throws an IllegalStatException, but this solution silently accepts the latest key. Emmanuel Touzery's solution (https://stackoverflow.com/a/32648397/471214) is closer to the original behaviour. – mmdemirbas Aug 08 '19 at 08:35
  • Here even newer Bug-Ticket which covers even Java 17.. https://bugs.openjdk.org/browse/JDK-8261865 – Dawid Oct 10 '22 at 16:42
209

It is not possible with the static methods of Collectors. The javadoc of toMap explains that toMap is based on Map.merge:

@param mergeFunction a merge function, used to resolve collisions between values associated with the same key, as supplied to Map#merge(Object, Object, BiFunction)}

and the javadoc of Map.merge says:

@throws NullPointerException if the specified key is null and this map does not support null keys or the value or remappingFunction is null

You can avoid the for loop by using the forEach method of your list.

Map<Integer,  Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));

but it is not really simple than the old way:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}
Brett Ryan
  • 26,937
  • 30
  • 128
  • 163
gontard
  • 28,720
  • 11
  • 94
  • 117
  • 3
    In that case I would rather use the old-fashioned for-each. Should I consider this a bug in toMerge? as the use of this merge function is really an implementation detail, or is the a good reasoning for not allowing toMap to process null values? – Jasper Jul 08 '14 at 12:41
  • 1
    I don't see that in the javadoc, your quote is about a different overload of the toMap function. I don't provide a merge function. Anyway, thanks for your answer, a bit dissapointing for me that the old way is still the most attractive option (i had rather used the java-8 way :) ). – Jasper Jul 08 '14 at 16:39
  • Yes it is from an overloaded `toMap` method. This method is the base method of the others ones, so i guess is documentation is more complete. But you are right, it is not explicitly specified in the method you are using. – gontard Jul 09 '14 at 09:37
  • 160
    Never thought that null values in map would make such an impact on standard API, I'd rather consider it as a flaw. – Askar Kalykov Apr 15 '15 at 10:44
  • 26
    Actually the API docs don't state anything about the use of [`Map.merge`](http://docs.oracle.com/javase/8/docs/api/index.html?java/lang/Integer.html). This IMHO is a flaw in the implementation that restricts a perfectly acceptable use-case that has been overlooked. The overloaded methods of `toMap` do state the use of `Map.merge` but not the one the OP is using. – Brett Ryan Oct 19 '15 at 12:33
  • 1
    @BrettRyan it mentions it by the overloads that accept `mergeFunction`, but not by the first one. Regardless, it definitely seems like an oversight. – shmosel May 31 '16 at 21:45
  • 14
    @Jasper there is even bug report https://bugs.openjdk.java.net/browse/JDK-8148463 – pixel Sep 23 '16 at 09:30
  • This should not have been accepted as the answer. @kajacx gave the right answer. – Ikthiander Mar 27 '19 at 08:58
  • 15
    I don't care what it has under the hood. All I care when reading Javadoc is the contract. Javadoc should say that it `throws NullPointerException` if any of the elements is null! – ACV May 07 '19 at 07:55
  • 1
    @acv sir yes sir! – gontard May 22 '19 at 18:59
  • This answer has better explanation . – Prem Mar 26 '20 at 18:15
  • @AskarKalykov 100% agree. I noticed today that `Map.ofEntries` has the same flaw. For no reason that I can think of (except for laziness on the JDK developers' part because now they don't have to do `null` checks in their `equals` and `hashCode` methods) they are disallowing `null` values there, too. – Frans Mar 28 '22 at 13:00
  • Yes, that's what the spec says. However, it looks like a typo in the spec, because it makes no sense. It should probably have read: throws `NullPointerException` if the specified key is null and this map does not support null keys or the value **of** `remappingFunction` is null – Karsten Spang Sep 19 '22 at 14:09
32

I wrote a Collector which, unlike the default java one, does not crash when you have null values:

public static <T, K, U>
        Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
                Function<? super T, ? extends U> valueMapper) {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                Map<K, U> result = new HashMap<>();
                for (T item : list) {
                    K key = keyMapper.apply(item);
                    if (result.putIfAbsent(key, valueMapper.apply(item)) != null) {
                        throw new IllegalStateException(String.format("Duplicate key %s", key));
                    }
                }
                return result;
            });
}

Just replace your Collectors.toMap() call to a call to this function and it'll fix the problem.

Emmanuel Touzery
  • 9,008
  • 3
  • 65
  • 81
  • 6
    But allowing `null` values and using `putIfAbsent` does not play well together. It does not detect duplicate keys when they map to `null`… – Holger Aug 08 '19 at 17:23
12

Yep, a late answer from me, but I think it may help to understand what's happening under the hood in case anyone wants to code some other Collector-logic.

I tried to solve the problem by coding a more native and straight forward approach. I think it's as direct as possible:

public class LambdaUtilities {

  /**
   * In contrast to {@link Collectors#toMap(Function, Function)} the result map
   * may have null values.
   */
  public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
    return toMapWithNullValues(keyMapper, valueMapper, HashMap::new);
  }

  /**
   * In contrast to {@link Collectors#toMap(Function, Function, BinaryOperator, Supplier)}
   * the result map may have null values.
   */
  public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Supplier<Map<K, U>> supplier) {
    return new Collector<T, M, M>() {

      @Override
      public Supplier<M> supplier() {
        return () -> {
          @SuppressWarnings("unchecked")
          M map = (M) supplier.get();
          return map;
        };
      }

      @Override
      public BiConsumer<M, T> accumulator() {
        return (map, element) -> {
          K key = keyMapper.apply(element);
          if (map.containsKey(key)) {
            throw new IllegalStateException("Duplicate key " + key);
          }
          map.put(key, valueMapper.apply(element));
        };
      }

      @Override
      public BinaryOperator<M> combiner() {
        return (left, right) -> {
          int total = left.size() + right.size();
          left.putAll(right);
          if (left.size() < total) {
            throw new IllegalStateException("Duplicate key(s)");
          }
          return left;
        };
      }

      @Override
      public Function<M, M> finisher() {
        return Function.identity();
      }

      @Override
      public Set<Collector.Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
      }

    };
  }

}

And the tests using JUnit and assertj:

  @Test
  public void testToMapWithNullValues() throws Exception {
    Map<Integer, Integer> result = Stream.of(1, 2, 3)
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null));

    assertThat(result)
        .isExactlyInstanceOf(HashMap.class)
        .hasSize(3)
        .containsEntry(1, 1)
        .containsEntry(2, null)
        .containsEntry(3, 3);
  }

  @Test
  public void testToMapWithNullValuesWithSupplier() throws Exception {
    Map<Integer, Integer> result = Stream.of(1, 2, 3)
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null, LinkedHashMap::new));

    assertThat(result)
        .isExactlyInstanceOf(LinkedHashMap.class)
        .hasSize(3)
        .containsEntry(1, 1)
        .containsEntry(2, null)
        .containsEntry(3, 3);
  }

  @Test
  public void testToMapWithNullValuesDuplicate() throws Exception {
    assertThatThrownBy(() -> Stream.of(1, 2, 3, 1)
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
            .isExactlyInstanceOf(IllegalStateException.class)
            .hasMessage("Duplicate key 1");
  }

  @Test
  public void testToMapWithNullValuesParallel() throws Exception {
    Map<Integer, Integer> result = Stream.of(1, 2, 3)
        .parallel() // this causes .combiner() to be called
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null));

    assertThat(result)
        .isExactlyInstanceOf(HashMap.class)
        .hasSize(3)
        .containsEntry(1, 1)
        .containsEntry(2, null)
        .containsEntry(3, 3);
  }

  @Test
  public void testToMapWithNullValuesParallelWithDuplicates() throws Exception {
    assertThatThrownBy(() -> Stream.of(1, 2, 3, 1, 2, 3)
        .parallel() // this causes .combiner() to be called
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
            .isExactlyInstanceOf(IllegalStateException.class)
            .hasCauseExactlyInstanceOf(IllegalStateException.class)
            .hasStackTraceContaining("Duplicate key");
  }

And how do you use it? Well, just use it instead of toMap() like the tests show. This makes the calling code look as clean as possible.

EDIT:
implemented Holger's idea below, added a test method

sjngm
  • 12,423
  • 14
  • 84
  • 114
  • 1
    The combiner does not check for duplicate keys. If you want to avoid to check for every key, you can use something like `(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }` – Holger Jul 22 '19 at 15:25
  • @Holger Yep, that's true. Especially since `accumulator()` actually does check that. Maybe I should do some parallel streams once :) – sjngm Jul 23 '19 at 15:39
11

Here's somewhat simpler collector than proposed by @EmmanuelTouzery. Use it if you like:

public static <T, K, U> Collector<T, ?, Map<K, U>> toMapNullFriendly(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper) {
    @SuppressWarnings("unchecked")
    U none = (U) new Object();
    return Collectors.collectingAndThen(
            Collectors.<T, K, U> toMap(keyMapper,
                    valueMapper.andThen(v -> v == null ? none : v)), map -> {
                map.replaceAll((k, v) -> v == none ? null : v);
                return map;
            });
}

We just replace null with some custom object none and do the reverse operation in the finisher.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
9

I have slightly modified Emmanuel Touzery's null-safe map Collector implementation.

This version:

  • Allows null keys
  • Allows null values
  • Detects duplicate keys (even if they are null) and throws IllegalStateException as in the original JDK implementation
  • Detects duplicate keys also when the key already mapped to the null value. In other words, separates a mapping with null-value from no-mapping
public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
    return Collectors.collectingAndThen(
        Collectors.toList(),
        list -> {
            Map<K, U> map = new LinkedHashMap<>();
            list.forEach(item -> {
                K key = keyMapper.apply(item);
                U value = valueMapper.apply(item);
                if (map.containsKey(key)) {
                    throw new IllegalStateException(String.format(
                            "Duplicate key %s (attempted merging values %s and %s)",
                            key, map.get(key), value));
                }
                map.put(key, value);
            });
            return map;
        }
    );
}

Unit tests:

@Test
public void toMapOfNullables_WhenHasNullKey() {
    assertEquals(singletonMap(null, "value"),
        Stream.of("ignored").collect(Utils.toMapOfNullables(i -> null, i -> "value"))
    );
}

@Test
public void toMapOfNullables_WhenHasNullValue() {
    assertEquals(singletonMap("key", null),
        Stream.of("ignored").collect(Utils.toMapOfNullables(i -> "key", i -> null))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateNullKeys() {
    assertThrows(new IllegalStateException("Duplicate key null"),
        () -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> null, i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_NoneHasNullValue() {
    assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
        () -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_OneHasNullValue() {
    assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
        () -> Stream.of(1, null, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_AllHasNullValue() {
    assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
        () -> Stream.of(null, null, null).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
    );
}
M. Justin
  • 14,487
  • 7
  • 91
  • 130
mmdemirbas
  • 9,060
  • 5
  • 45
  • 53
  • Slight improvement to the duplicate message to mirror the one in Java 11, which also includes the duplicate's values: `U value = valueMapper.apply(item); if (map.containsKey(key)) { throw new IllegalStateException(String.format("Duplicate key %s (attempted merging values %s and %s)", key, map.get(key), value)); } map.put(key, value); ` – M. Justin Mar 18 '21 at 21:13
4

If the value is a String, then this might work: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

Gnana
  • 614
  • 1
  • 7
  • 18
  • 7
    That works only if you are okay with modifying the data. Downstream methods might expect null values rather than empty strings. – Sam Buchmiller Mar 13 '19 at 17:16
4

Sorry to reopen an old question, but since it was edited recently saying that the "issue" still remains in Java 11, I felt like I wanted to point out this:

answerList
        .stream()
        .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

gives you the null pointer exception because the map does not allow null as a value. This makes sense because if you look in a map for the key k and it is not present, then the returned value is already null (see javadoc). So if you were able to put in k the value null, the map would look like it's behaving oddly.

As someone said in the comments, it's pretty easy to solve this by using filtering:

answerList
        .stream()
        .filter(a -> a.getAnswer() != null)
        .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

in this way no null values will be inserted in the map, and STILL you will get null as the "value" when looking for an id that does not have an answer in the map.

I hope this makes sense to everyone.

Luca
  • 1,116
  • 2
  • 15
  • 24
  • 8
    It would make sense if a map did not allow null values, but it does. You can do `answerMap.put(4, null);` without any problems. You are right that with your proposed solution you will get the same result for anserMap.get() if it is not present as if the value would be inserted as null. However, if you iterate over all entries of the map there is a obviously a difference. – Jasper Nov 07 '18 at 10:26
3

According to the Stacktrace

Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1216)
at java.util.stream.Collectors.lambda$toMap$148(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda$5/391359742.accept(Unknown Source)
at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at com.guice.Main.main(Main.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

When is called the map.merge

        BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);

It will do a null check as first thing

if (value == null)
    throw new NullPointerException();

I don't use Java 8 so often so i don't know if there are a better way to fix it, but fix it is a bit hard.

You could do:

Use filter to filter all NULL values, and in the Javascript code check if the server didn't send any answer for this id means that he didn't reply to it.

Something like this:

Map<Integer, Boolean> answerMap =
        answerList
                .stream()
                .filter((a) -> a.getAnswer() != null)
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

Or use peek, which is used to alter the stream element for element. Using peek you could change the answer to something more acceptable for map but it means edit your logic a bit.

Sounds like if you want to keep the current design you should avoid Collectors.toMap

Marco Acierno
  • 14,682
  • 8
  • 43
  • 53
2

Retaining all questions ids with small tweak

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
user944849
  • 14,524
  • 2
  • 61
  • 83
sigirisetti
  • 329
  • 2
  • 10
2
public static <T, K, V> Collector<T, HashMap<K, V>, HashMap<K, V>> toHashMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper
)
{
    return Collector.of(
            HashMap::new,
            (map, t) -> map.put(keyMapper.apply(t), valueMapper.apply(t)),
            (map1, map2) -> {
                map1.putAll(map2);
                return map1;
            }
    );
}

public static <T, K> Collector<T, HashMap<K, T>, HashMap<K, T>> toHashMap(
        Function<? super T, ? extends K> keyMapper
)
{
    return toHashMap(keyMapper, Function.identity());
}
Igor Zubchenok
  • 675
  • 8
  • 18
  • 1
    upvoting because this compiles. Accepted answer does not compile because Map::putAll does not have a return value. – Taugenichts Apr 24 '19 at 16:22
2

For completeness, I'm posting a version of the toMapOfNullables with a mergeFunction param:

public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) {
    return Collectors.collectingAndThen(Collectors.toList(), list -> {
        Map<K, U> result = new HashMap<>();
        for(T item : list) {
            K key = keyMapper.apply(item);
            U newValue = valueMapper.apply(item);
            U value = result.containsKey(key) ? mergeFunction.apply(result.get(key), newValue) : newValue;
            result.put(key, value);
        }
        return result;
    });
}
2

Just wrap the nullable value as an Optional. Pretty elegant workaround.

    listOfValues.stream()
    .collect(Collectors.toMap(e -> e.getKey(), e -> 
    Optional.ofNullable(e.getValue())))
Simon
  • 997
  • 1
  • 15
  • 29