3

suppose I have these two lists

List<Person> persons = Arrays.asList(
                new Person(1, "Mike", "Canada"),
                new Person(2, "Jill", "England"),
                new Person(3, "Will", "Whales"),
                new Person(4, "Mary", "Spain"));


List<Metadata> metadata= Arrays.asList(
                new metadata(1, "2000-01-01", "Naturalized", "Bachelor's of Arts"),
                new metadata(2, "2001-01-01", "ExPat", "Masters of Chemestry"),
                new metadata(3, "2017-05-01", "Citizen", "Buiness Management"),
                new metadata(4, "2018-04-16", "Work Visa", "Nursing"));

where the end result is a new list:

List<PersonWithMetadata> personsAndMEtadata = Arrays.asList(
                new PersonWithMetadata(1, "Mike", "Canada", "2000-01-01", "Naturalized", "Bachelor's of Arts"),
                new PersonWithMetadata(2, "Jill", "England", "2001-01-01", "ExPat", "Masters of Chemestry"),
                new PersonWithMetadata(3, "Will", "Whales", "2017-05-01", "Citizen", "Buiness Management"),
                new PersonWithMetadata(4, "Mary", "Spain", "2018-04-16", "Work Visa", "Nursing"));

I am trying to find a Java streams way of combining the first two lists into the third--like an SQL join on the first input being an id number. It seems like there should be a way to do this, but I'm at a loss. how is this done? Also, suppose that there is at most one match between the two input lists.

Mureinik
  • 297,002
  • 52
  • 306
  • 350
StillLearningToCode
  • 2,271
  • 4
  • 27
  • 46
  • Do they follow the same order? – shmosel Jul 13 '18 at 18:31
  • 2
    Have you tried something specifically and it failed? And is there a particular reason you have to use streams? – Roddy of the Frozen Peas Jul 13 '18 at 18:31
  • try starting with IntSream.range and go through the indices that way. this code has "design smell" consider a proper data structure. it would be better if you used a map so you can do lookup by id. store all relavant info at that id in a proper class. rather than parallel lists. – Patrick Parker Jul 13 '18 at 18:42
  • I've posted first an answer showing an example with Guava `Streams.zip` but found later on this answer: [Google Guava “zip” two lists](https://stackoverflow.com/questions/19040802/google-guava-zip-two-lists). Another more general answer: [How to zip two Java Lists](https://stackoverflow.com/questions/31963297/how-to-zip-two-java-lists) I deleted my answer but want to mention the linked answers. – LuCio Jul 13 '18 at 22:41

4 Answers4

5

YCF_L's solution should work, but it's an O(n2) solution. An O(n) solution could be achieved by converting one list to map from the id to the object, and then iterating over the other and getting the matching value from the map:

Map<Integer, Person> personMap = 
    persons.stream().collect(Collectors.toMap(Person::getId, Function.identity());

List<PersonWithMetadata> result = 
    metadata.stream()
            .map(m -> new PersonWithMetadata(personMap.get(m.getId()), m)
            .collect(Collectors.toList());

In the sample data the lists have matching objects in matching order. If this assumption is true for the real problem too, the solution could be must easier - you could stream the indexes and get the corresponding values from the lists:

List<PersonWithMetadata> result = 
    IntStream.reange(0, persons.size())
             .map(i -> new PersonWithMetadata(persons.get(i), metadata.get(i))
             .collect(Collectors.toList());
Mureinik
  • 297,002
  • 52
  • 306
  • 350
3

You can try this way :

List<PersonWithMetadata> personsAndMEtadata = persons.stream()
        .map(p -> {
                    //search for the meta data based on the person id
                    Metadata meta = metadata.stream()
                            .filter(m -> m.getId() == p.getId())
                            .findFirst()
                            .get();
                    // then create a PersonWithMetadata object based on Person and metadata
                    return new PersonWithMetadata(
                            p.getId(), p.getFirstName(), p.getLastName(),
                            meta.getDate(), meta.getCity(), meta.getJob()
                    );

                }
        ).collect(Collectors.toList());

About this line :

Metadata meta = metadata.stream().filter(m -> m.getId() == p.getId()).findFirst().get();

I assume that you have a meta data with the id of person, else you will get NullPointerException.

Youcef LAIDANI
  • 55,661
  • 15
  • 90
  • 140
  • Just to point something out, this solution is O(n^2), whereas the problem is easily solvable in O(n) without using streams... – Obicere Jul 13 '18 at 18:56
  • 1
    @Obicere check the answer of Mureinik, you can either solve it in O(n) – Youcef LAIDANI Jul 13 '18 at 18:58
  • @Obicere beside it is not totally *O(n^2)* if the two Lists are orders by id as it was shown so It will pick always the first element and break – Youcef LAIDANI Jul 13 '18 at 19:13
  • 1
    Its nothing personal, but this is the reason why many people dislike streams. Many developers like to write quick code, and in this case you were the first to answer. Writing code quick is good, but not when you sacrifice efficiency. Overall this answer promotes this "get it done" attitude with streams that I believe justifies the downvote, as it is a poor answer to the question. And also, it is still O(n^2) even if it finds it in roughly half the time. – Obicere Jul 13 '18 at 19:24
1

I believe what you're looking for is the zip function that was sadly omitted from the API.

The protonpack library provides it, which would allow you to zip and then map the tuple to the new structure.

StreamUtils.zip(persons, metadata, (person, metadata) -> ... )
Kraylog
  • 7,383
  • 1
  • 24
  • 35
1

The below example builds Map of Metadata objects using the ID as the key. This will help with performance as there's no need to iterate over the Metadata list for each Person in the List

Code

public static void main(String[] args) {
    List<Person> persons = Arrays.asList(
            new Person(1, "Mike", "Canada"),
            new Person(2, "Jill", "England"),
            new Person(3, "Will", "Whales"),
            new Person(4, "Mary", "Spain"));


    List<Metadata> metadataList = Arrays.asList(
            new Metadata(1, "2000-01-01", "Naturalized", "Bachelor's of Arts"),
            new Metadata(2, "2001-01-01", "ExPat", "Masters of Chemestry"),
            new Metadata(3, "2017-05-01", "Citizen", "Buiness Management"),
            new Metadata(4, "2018-04-16", "Work Visa", "Nursing"));

    //Iterate over metadataList once and create map based on ID as key
    Map<Integer, List<Metadata>> metadataMap = metadataList.stream()
            .collect(Collectors.groupingBy(Metadata::getId));

    //Iterate over personList and fetch metadata from Map to build PersonWithMetadata
    List<PersonWithMetadata> personWithMetadataList = persons.stream().map(person -> {
        List<Metadata> metadata = metadataMap.get(person.id);
        if (metadata.isEmpty()) {
            //TODO: Handle scenario for no metadata for person
        }

        //TODO: Build PersonWithMetadata

        return new PersonWithMetadata();

    }).collect(Collectors.toList());

}
Garreth Golding
  • 985
  • 2
  • 11
  • 19