9

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.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Bali
  • 705
  • 4
  • 13
  • 21
  • 1
    You can `peek` before `collect` and add each `Person` instance to a `final List` you have in scope, and basically `get` the last value each time from it when you encounter a duplicate. Not able to test in my local right now so not answering with code. – Koray Tugay Jan 10 '19 at 15:46
  • 2
    You could consider not using streams for this job. They are not always the best choice. To do this with the collectors available via java.util.stream.Collectors you need to perform two passes, whereas you could do it more efficiently (i.e. in one pass), with comparable code clarity, via an old-school enhanced `for` loop. – John Bollinger Jan 10 '19 at 16:01
  • What does `person.getAddress()` return? A collection? – VGR Jan 10 '19 at 16:50
  • 1
    @VGR no, a String. – DodgyCodeException Jan 10 '19 at 16:53
  • See also [How to get the key in Collectors.toMap merge function?](https://stackoverflow.com/q/44407112/2711488) and compare with [this answer](https://stackoverflow.com/a/45210944/2711488). – Holger Jan 10 '19 at 18:36
  • @JohnBollinger You can do it with 1 pass using Streams I think. – Koray Tugay Jan 12 '19 at 23:55
  • @KorayTugay, I don't doubt that you could do it in one pass with streams, by writing a for-purpose `Collector`. But I said you can't do that with the pre-built Collectors available from the `java.util.streams.Collectors` utility class. If you want to persuade me differently then you'll need to do a lot better than "I think". – John Bollinger Jan 13 '19 at 03:08

4 Answers4

6

@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.forEach(item -> {
    if(myMap.containsKey(item.getName()))
        {/*do something*/}
    else 
        myMap.put(item.getName(), item.getAddress());
});
Justin
  • 2,960
  • 2
  • 34
  • 48
ygbgames
  • 191
  • 9
  • Yes, Aomine's answer does indeed iterate twice. You can also use `Map.putIfAbsent` to simplify your code. – DodgyCodeException Jan 10 '19 at 16:55
  • 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. – ygbgames Jan 11 '19 at 02:21
  • 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."); }`. – DodgyCodeException Jan 11 '19 at 09:44
6

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.

Naman
  • 27,789
  • 26
  • 218
  • 353
5

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()));
Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
1

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
Koray Tugay
  • 22,894
  • 45
  • 188
  • 319