7

I have 2 Java Classes.

class A {
 String name;
 List<B> numbers;
}

class B {
 Integer number;
}

I want to get the distinct of Class A and concat the List of B in it.

e.g. Suppose I have a List with the following objects.

List<A>{
 name = "abc"
 List<B>{1,2}

 name= "xyz"
 List<B>{3,4}

 name = "abc"
 List<B>{3,5}
}

The result should be:

List<A>{
 name = "abc"
 List<B>{1,2,3,5}

 name="xyz"
 List<B>{3,4}
}

Any help would be appreciated.

Note: I want to achieve this functionality using Java 8 streams.

Thanks

Paul Boddington
  • 37,127
  • 10
  • 65
  • 116
Umair Ansari
  • 418
  • 3
  • 15
  • @PaulBoddington Yes I am creating the new class A instance with the values from the database and adding it into the list. Once the list is populated. I need to get distinct object of class A with concatenated list of class B. – Umair Ansari Mar 23 '16 at 01:16
  • 1
    Thanks. I deleted the comment because at first glance it looked like `A` and `B` were pseudocode but then realised they are full classes that compile. – Paul Boddington Mar 23 '16 at 01:20

3 Answers3

3

You may use toMap collector:

Collection<A> result = list.stream()
         .collect(Collectors.toMap(a -> a.name, a -> a, 
                      (a, b) -> {a.numbers.addAll(b.numbers); return a;}))
         .values();

You may copy the result after that into List (like new ArrayList<>(result)), but as we don't preserve any particular order, having List is not very useful. In the most of scenarios having Collection as a result is fine.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • 3
    You can preserve the order by using the four-argument overload of `toMap()` and passing `LinkedHashMap::new` as the `Supplier`. – jaco0646 Mar 23 '16 at 02:30
  • For me it gives `java.lang.UnsupportedOperationException` at line 39: http://pastebin.com/fAvd6jMi – ctomek Mar 25 '16 at 19:15
  • @ctomek, your lists are created as `Arrays.asList` which do not support addition of new elements. This could be easily fixed using `ArrayList` instead. – Tagir Valeev Mar 26 '16 at 09:59
2

I don't think there is a way to get around the map. You can however use groupingBy to hide it, but it is effectively the same code and performance as Paul Boddington suggested.

List<A> merge(List<A> input) {
    return input.stream()
            .collect(groupingBy(a -> a.name)) // map created here
            .entrySet()
            .stream()
            .map(entry -> new A(
                    entry.getKey(),
                    entry.getValue().stream()
                            .flatMap(list -> list.numbers.stream())
                            .collect(toList()) // merging behaviour
            )).collect(toList());
}

There is no mutation of original list and you can easily change the behaviour of merging the lists - for example if you want to get rid of duplicates just add .distinct() after flatMap(list -> list.numbers.stream()) (remember about adding equals to B) or in a similar way you can sort them by just adding .sorted() (you have to make B implement Comparable interface or just use .sorted(Comparator<B>)).

Here is full code with tests and imports:

import org.junit.Test;

import java.util.List;

import static com.shazam.shazamcrest.MatcherAssert.assertThat;
import static com.shazam.shazamcrest.matcher.Matchers.sameBeanAs;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

public class Main {

    class A {
        final String name;
        final List<B> numbers;
        A(String name, List<B> numbers) {
            this.name = name;
            this.numbers = numbers;
        }
    }

    class B {
        final Integer number;
        B(Integer number) {
            this.number = number;
        }
    }

    List<A> merge(List<A> input) {
        return input.stream()
                .collect(groupingBy(a -> a.name))
                .entrySet()
                .stream()
                .map(entry -> new A(
                        entry.getKey(),
                        entry.getValue().stream()
                                .flatMap(list -> list.numbers.stream())
                                .collect(toList())
                )).collect(toList());
    }

    @Test
    public void test() {
        List<A> input = asList(
                new A("abc", asList(new B(1), new B(2))),
                new A("xyz", asList(new B(3), new B(4))),
                new A("abc", asList(new B(3), new B(5)))
        );

        List<A> list = merge(input);

        assertThat(list, sameBeanAs(asList(
                new A("abc", asList(new B(1), new B(2), new B(3), new B(5))),
                new A("xyz", asList(new B(3), new B(4)))
        )));
    }

}

EDIT:

Following your questions in the comments, if you want to add multiple fields into groupingBy clause, you would need to create a class that represents a key in the map. If you have fields that you don't want to include in the key, then you have to define how to merge two different values - similarly to what you do with numbers. Depending on what the fields are, merging behaviour can be just choosing the first value and discarding the other (what I have done with numbers in the code below).

class A {
    final String name;
    final String type;
    final List<B> numbers;
    A(String name, String type, List<B> numbers) {
        this.name = name;
        this.type = type;
        this.numbers = numbers;
    }
}

class B {
    final Integer number;
    B(Integer number) {
        this.number = number;
    }
}

class Group {
    final String name;
    final String type;
    Group(String name, String type) {
        this.name = name;
        this.type = type;
    }
    // this needs to have equals and hashCode methods as we use it as a key in a map
}

List<A> merge(List<A> input) {
    return input.stream()
            .collect(groupingBy(a -> new Group(a.name, a.type)))
            .entrySet()
            .stream()
            .map(entry -> new A(
                    entry.getKey().name, // entry.getKey() is now Group, not String
                    entry.getKey().type,
                    entry.getValue().get(0).numbers // no merging, just take first
            )).collect(toList());
}
Jaroslaw Pawlak
  • 5,538
  • 7
  • 30
  • 57
1

Here's my answer. I've added a constructor for A to make it slightly easier.

public class Main {

    static class A {
        String name;
        List<B> numbers;

        A(String name, List<B> numbers) {
            this.name = name;
            this.numbers = new ArrayList<>(numbers);
        }
    }

    static class B {
        Integer number;
    }

    static List<A> merge(List<A> list) {
        Map<String, List<B>> map = new LinkedHashMap<>();
        for (A a : list)
            map.computeIfAbsent(a.name, k -> new ArrayList<>()).addAll(a.numbers);
        return map.entrySet()
                  .stream()
                  .map(e -> new A(e.getKey(), e.getValue()))
                  .collect(Collectors.toList());
    }
}

There is almost certainly a simple way to replace the first 3 lines of the merge method with something beginning list.stream()... making the entire method a one-liner. I was not able to work it out though. Perhaps somebody else can edit this answer showing how?

Paul Boddington
  • 37,127
  • 10
  • 65
  • 116