handle duplicate key in Collectors.toMap() function

12,682

Solution 1

if you want to access the whole person object in the merge function then pass Function.identity() for the valueMapper:

Map<String, Person> myMap = 
        persons.stream()
               .collect(toMap(p -> p.getName(), 
                      Function.identity(), // or p -> p
                     (p1, p2) -> { /* do logic */ }));

But as you can see the resulting map values are Person objects, if you still want a Map<String, String> as a result and still access the whole Person object in the mergeFunction then you can do the following:

 persons.stream()
         .collect(toMap(p -> p.getName(), Function.identity(),(p1, p2) -> { /* do logic */ }))
         .entrySet()
         .stream()
         .collect(toMap(Map.Entry::getKey, p -> p.getValue().getAddress()));

Solution 2

@Aomine: solution looks good and works for me too. Just wanted to confirm that with this it iterates twice right ?? Cause with simple solution like below it iterates only once but achieve what is required.

Map<String,String> myMap= new HashMap<>();
persons.stream().foreach(item-> {
    if(myMap.containsKey(item.getName()))
        {/*do something*/}
    else 
        myMap.put(item.getName(),item.getAddress());
});

Solution 3

I believe the forEach approach along with Map.merge would be much simpler and appropriate for the current use case :

Map<String, String> myMap = new HashMap<>();
persons.forEach(person -> myMap.merge(person.getName(), person.getAddress(), (adrs1, adrs2) -> {
    System.out.println("duplicate " + person.getName() + " is found!");
    return adrs1;
}));

Note: Map.merge also uses BiFunction (parent of BinaryOperator as used in toMap), hence you could correlate the merge function here to your existing desired functionality easily.

Solution 4

Here is another possibility using peek

import java.util.*;
import java.util.stream.*;

class App {
    public static void main(String[] args) {
        List<Person> persons = Arrays.asList(new Person("foo", "bar"), new Person("baz", "qux"), new Person("foo", "zuz"));

        Set<String> names = new HashSet<>();
        Map<String, String> nameAddress = persons.stream().peek(p -> {
            if (names.contains(p.getName())) {
                System.out.println("Duplicate key being skipped: " + p);
            } else {
                names.add(p.getName());
            }
        }).collect(Collectors.toMap(person -> person.getName(), person -> person.getAddress(), (addr1, addr2) -> addr1));

    }
}

class Person {
    String name;
    String address;

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    @Override
    public String toString() {
        return name + " " + address;
    }
}

Output for me will be as follows for the snippet above:

Duplicate key being skipped: foo zuz
Share:
12,682
Bali
Author by

Bali

Updated on June 11, 2022

Comments

  • Bali
    Bali about 2 years

    I am creating a map which its (key,value) will be (name, address) in my Person object:

    Map<String, String> myMap = persons.stream.collect(Collector.toMap(person.getName(), person.getAddress(), (address1, address2) -> address1));
    

    In the duplicate key situation, I would like to skip to add the second address to the map and would like to log the name also. Skipping the duplicate address I can do already using mergeFunction, but in oder to log the name I need in this mergeFunction the person object, something like:

    (address1, address2) -> {
                               System.out.println("duplicate "+person.name() + " is found!");
                               return address1;
                            }
    

    I am getting stuck by passing person object to this merge function.

  • Bali
    Bali over 5 years
    is it possible that I keep the Map<String, String> but I would like to access the person.name() in my logic method?
  • Bali
    Bali over 5 years
    hi Aomine, your second solution sounds good, but I got compile error at the last line : p -> p.getValue().getAddress(). Do you have any idea?
  • Bali
    Bali over 5 years
    @Aomine sorry my typo mistake. It works now, thank you very much for your help :)
  • DodgyCodeException
    DodgyCodeException over 5 years
    Yes, Aomine's answer does indeed iterate twice. You can also use Map.putIfAbsent to simplify your code.
  • ygbgames
    ygbgames over 5 years
    Actually i can use it but in the questions. It was asked for to use the name if it is already present so didnt use Map.putIfAbsent.
  • DodgyCodeException
    DodgyCodeException over 5 years
    You can still call Map.putIfAbsent. You supply the name as the key and the address as the value. So if putIfAbsent returns non-null, it means this is a duplicate key, and now you already have the name (which you passed to putIfAbsent as an argument) which you can use in your System.out.println/etc. So it's simply this: if (myMap.putIfAbsent(item.getName(), item.getAdress()) != null) { System.out.println(item.getName() + " already exists."); }.
  • Sham Fiorin
    Sham Fiorin over 4 years
    Love the solution, i', using: