12

I have two list instances like this:

List<NameAndAge> nameAndAgeList = new ArrayList<>();
nameAndAgeList.add(new NameAndAge("John", "28"));
nameAndAgeList.add(new NameAndAge("Paul", "30"));
nameAndAgeList.add(new NameAndAge("Adam", "31"));

List<NameAndSalary> nameAndSalaryList = new ArrayList<>();
nameAndSalaryList.add(new NameAndSalary("John", 1000));
nameAndSalaryList.add(new NameAndSalary("Paul", 1100));
nameAndSalaryList.add(new NameAndSalary("Adam", 1200));

where NameAndAge is

class NameAndAge {
    public String name;
    public String age;

    public NameAndAge(String name, String age) {
        this.name = name;
        this.age = age;
    }

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

and NameAndSalary is

private class NameAndSalary {
    private String name;
    private double salary;

    public NameAndSalary(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

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

Now, I want to create a map with key as NameAndAge object from the first list and value as NameAndSalary from the second list where the name is equal in both the objects.

So, when I print the map, it should look like this:

{John: 28=John: 1000.0}
{Paul: 30=Paul: 1100.0}
{Adam: 31=Adam: 1200.0}

I have tried doing this, but the end return type is 'void' so I'm stuck clueless as I am new to Streams.

nameAndAgeList
    .forEach(n ->
        nameAndSalaryList
            .stream()
            .filter(ns -> ns.name.equals(n.name))
            .collect(Collectors.toList()));

Can someone please advise how can this be achieved with Java Streams API?

ZhekaKozlov
  • 36,558
  • 20
  • 126
  • 155
Ravi
  • 879
  • 2
  • 9
  • 23

4 Answers4

10

First of all, assuming you are going to create a HashMap, your key class (NameAndAge) must override equals and hashCode().

Second of all, in order to be efficient, I suggest you first create a Map<String,NameAndSalary> from the second List:

Map<String,NameAndSalary> helper =
    nameAndSalaryList.stream()
                     .collect(Collectors.toMap(NameAndSalary::getName,
                                               Function.identity()));

Finally, you can create the Map you want:

Map<NameAndAge,NameAndSalary> output = 
    nameAndAgeList.stream()
                  .collect(Collectors.toMap(Function.identity(),
                                            naa->helper.get(naa.getName())));
Eran
  • 387,369
  • 54
  • 702
  • 768
  • 2
    I was in the middle of writing a similar answer. There are other assumptions that must be made without adding additional complexity: (1) that the names are unique, and (2) a given name should always appear in objects in both lists. – William Price May 22 '18 at 18:55
  • 1
    @WilliamPrice yes, if the names are not unique , you must add a merge function to `Collectors.toMap()`. As for the second assumption, you can ignore it. If a name appears only on the first list, the corresponding NameAndAge will be assigned a null value in the output Map. If a name appears only on the second list, it won't appear on the output Map. – Eran May 22 '18 at 18:56
  • [Collectors.toMap() will throw NPE](https://stackoverflow.com/questions/24630963/java-8-nullpointerexception-in-collectors-tomap) if either the key _or the value_ is `null`, even if the destination map implementation would allow it. – William Price May 22 '18 at 22:42
  • Thanks @Eran. Although it worked for me, I am still trying to understand what Function.identity() is and all other such stuff as I am pretty new to streams (which is irrelevant to the question I posted). Also agree with your point of adding hashcode and equals, but since in my case, they are always unique entries, there may not be an impact of not overriding them – Ravi May 23 '18 at 07:24
  • @WilliamPrice, Yes. In my case, the names are always unique and there is always going to be a matching name in both the lists. Thanks for pointing it out though – Ravi May 23 '18 at 07:25
  • 1
    @Ravi `Function.identity()` is a function that returns whatever you pass into it. When you use `Function.identity()` as the key generator of the `toMap` collector, this means that the keys will be the elements of the `Stream` (i.e. the `NameAndAge` instances in the second Stream pipeline). – Eran May 23 '18 at 07:26
  • Aah I see. Thanks @Eran. I now understand what it is. This is similar to naa -> naa right? – Ravi May 23 '18 at 07:29
  • 1
    This is the only solution that is even *close* to being 1. maintainable (maybe even more when pulling these lines out into generic methods like `mapWithKeys` and `mapWithValues`) and 2. efficient in that it is O(n), and does not search the whole second list for each element that is found in the first one (which is O(n^2)). – Marco13 May 23 '18 at 18:50
1

This should do the trick, too:

Map<NameAndAge, NameAndSalary> map = new HashMap<>();
nameAndAgeList.forEach(age -> {
     NameAndSalary salary = nameAndSalaryList.stream().filter(
              s -> age.getName().equals(s.getName())).
              findFirst().
              orElseThrow(IllegalStateException::new);
      map.put(age, salary);
});

Mind that it would throw an IllegalStateException if a matching name can't be found.

Jan B.
  • 6,030
  • 5
  • 32
  • 53
0

This should work too

List<String> commonNames = nameAndAgeList
    .stream()
    .filter(na -> 
         nameAndSalaryList.anyMatch((ns) -> ns.getName().equals(na.getName()))
    .collect(Collectors.toList());

Map<NameAndAge, NameAndSalary> map = 
    commonNames.stream().collect(Collectors.toMap(name -> 
        nameAndAgeList.get(name), name -> nameAndSalaryList.get(name)));
Rick Ridley
  • 573
  • 2
  • 9
-2

Without creating any intermediate object, without using a forEach, one liner:

Map<NameAndAge, NameAndSalary> resultMap1 = nameAndAgeList.stream()
            .map(nameAndAge -> nameAndSalaryList.stream()
                    .filter(nameAndSalary -> nameAndAge.getName().equals(nameAndSalary.getName()))
                    .map(nameAndSalary -> new SimpleEntry<>(nameAndAge, nameAndSalary))
                    .collect(Collectors.toList()).get(0))
            .collect(Collectors.toMap(simpleEntry -> simpleEntry.getKey(), simpleEntry -> simpleEntry.getValue()));

You will have to add the getter functions to the domain classes though. Accessing the properties directly might not be a good idea.

mark42inbound
  • 364
  • 1
  • 4
  • 19