javadictionaryhashmapjava-stream

HashMap constructor cannot infer arguments when passing two streams as arguments


This is my first approach at streams. I am trying to learn it by myself, but since its a new style of programming I'm having a tough time.

Problem

The following method has a Map<Long, Ingredient> parameter (Ingredient has the String attribute name).

I want to return an inverted Map<String, Long> (the String being the attribute "name" of the class Ingredient).

My working for-loop solution

public static Map<String, Long> focusOnNameAndInvert(Map<Long, Ingredient> articles) {
    ArrayList<String> nameList = new ArrayList<>(articles.values().stream().map(Ingredient::getName).collect(Collectors.toList()));
    ArrayList<Long> keyList = new ArrayList<>(articles.keySet());

    Map<String, Long> nameAndInvertedMap = new HashMap<>();
    for (int i = 0; i<nameList.size(); i++){
        nameAndInvertedMap.put(nameList.get(i), keyList.get(i));
    }

    return nameAndInvertedMap;
}

My stream approach

The <> on the right side of initializing the HashMap is red underlined and says "Cannot infer arguments" (using IntelliJ)

public static Map<String, Long> focusOnNameAndInvert(Map<Long, Ingredient> articles) {
    Map<String, Long> nameAndInvertedMap =  new HashMap<>(
            articles.values().stream().map(Ingredient::getName),
            articles.keySet().stream().map(articles::get));


    return nameAndInvertedMap;
}

Why isn't this working the way I expect, and how can I fix it?


Solution

  • Your method is not doing what you expected because, as you can see from the compiler error, there is no constructor for the HashMap class that accepts a List of keys and a List of values.

    Furthermore, even if there were, you're streaming your Map twice without even collecting the values of your operations. Streams are pipelines of lazily evaluated operations, if you do not add a terminal operation, they are not executed at all.

    You could re-write your method by streaming the entries of the given map and then collecting the entries with the terminal operation collect(Collectors.toMap()). Within the terminal operation, each key/value pair would be swapped, with the Ingredient's name as the key, while with the long key as the value.

    public static Map<String, Long> focusOnNameAndInvert(Map<Long, Ingredient> articles) {
        return articles.entrySet().stream()
                .collect(Collectors.toMap(entry -> entry.getValue().getName(), entry -> entry.getKey()));
    }
    

    Note that collecting the entries with the 2 parameters version of the method toMap() does not work if there are multiple keys, i.e. duplicate ingredient names in your case. A better approach would be using the 3 parameter version of the toMap() method, and handle colliding cases.

    Possible implementations could be:

    public static Map<String, Long> focusOnNameAndInvert(Map<Long, Ingredient> articles) {
        return articles.entrySet().stream()
                .collect(Collectors.toMap(entry -> entry.getValue().getName(), entry -> entry.getKey(), (longVal1, longVal2) -> {
                    throw new RuntimeException(String.format("Duplicate key for values %d %d", longVal1, longVal2));
                }));
    }
    
    public static Map<String, Long> focusOnNameAndInvert(Map<Long, Ingredient> articles) {
        return articles.entrySet().stream()
                .collect(Collectors.toMap(entry -> entry.getValue().getName(), entry -> entry.getKey(), (longVal1, longVal2) -> longVal1));
    }
    
    public static Map<String, Long> focusOnNameAndInvert(Map<Long, Ingredient> articles) {
        return articles.entrySet().stream()
                .collect(Collectors.toMap(entry -> entry.getValue().getName(), entry -> entry.getKey(), (longVal1, longVal2) -> longVal1 + longVal2));
    }