2

Given,

class Foo {
private Long id;
private String name;
private String category;
private List<String> categories;
// getters & setters
}

I have a list of objects.

final Foo f1 = new Foo(1L, "a", "c1");
final Foo f2 = new Foo(1L, "a", "c2");
final Foo f3 = new Foo(2L, "a", "c1");

final List<Foo> li = List.of(f1, f2, f3);

which looks like

{[Foo [id=1, name=a, category=c1, categories=null], Foo [id=1, name=a, category=c2, categories=null]], [Foo [id=2, name=a, category=c1, categories=null]]}

I want to transform this to

[Foo [id=1, name=a, category=null, categories=[c1, c2]], Foo [id=2, name=a, category=null, categories=[c1]]]

i.e. collate the individual category into a list of categories.

This is the current code that achieves what I want.

public static void main(final String[] args) {
        final Foo f1 = new Foo(1L, "a", "c1");
        final Foo f2 = new Foo(1L, "a", "c2");
        final Foo f3 = new Foo(2L, "a", "c1");

        final List<Foo> li = List.of(f1, f2, f3);
        li.forEach(e -> System.out.println(e));

        final Map<Long, List<Foo>> collect = li.stream().collect(Collectors.groupingBy(Foo::getId));
        System.out.println(collect);

        final List<Foo> grouped = new ArrayList<>();
        collect.forEach((k, v) -> {
            System.out
                    .println("key=" + k + "val=" + v.stream().map(e1 -> e1.getCategory()).collect(Collectors.toList()));

            final Foo foo = collect.get(k).get(0);
            foo.setCategories(v.stream().map(e1 -> e1.getCategory()).collect(Collectors.toList()));
            foo.setCategory(null);

            grouped.add(foo);
        });

        System.out.println(grouped);
    }

Is there any way to do this using streams & lambdas alone without having to break into multiple steps? The goal is to make this code more elegant, readable, and convey the intention to the reader.

This question is similar in nature to Group by and sum objects like in SQL with Java lambdas? but didn't help me since there an aggregation is done whereas here it's not an aggregation.

Cyriac George
  • 153
  • 2
  • 11
  • Re-expressing a computation strictly in terms of streams and lambdas is usually possible, but in many cases the result is less elegant, harder to read, and inscutable. The harder it is to figure out such a transformation, the more likely it is that you're moving in the wrong direction relative to your stated goals. – John Bollinger Nov 19 '20 at 18:32
  • 1
    FYI, when you're looking to improve working code, https://codereview.stackexchange.com is a better fit. – jaco0646 Nov 19 '20 at 19:13
  • @jaco0646 A whole new world that I didn't know existed (or didn't know was purpose built)! Thank you! – Cyriac George Nov 19 '20 at 20:48

1 Answers1

1

It can be done by implementing a merge function which would accumulate the categories in the categories list, and then using reduce operation of the stream:

class Foo {
    static Foo merge(Foo accum, Foo other) {
        if (null == accum.categories) {
            accum.categories = new ArrayList<>();
            if (null != accum.category) {
                accum.categories.add(accum.category);
                accum.category = null;
            }
        }
        accum.categories.add(other.category);

        return accum;
    }
}

Implementation:

static List<Foo> joinedCategoriesReduce(List<Foo> input) {
    return input
            .stream()
            .collect(Collectors.groupingBy(Foo::getId))  // Map<Integer, List<Foo>>
            .values().stream()   // Stream<List<Foo>>
            .map(v -> v.stream() // Stream<Foo>
                    .reduce(new Foo(v.get(0).getId(), v.get(0).getName(), (String)null), Foo::merge)
            )
            .collect(Collectors.toList());
}

Test

final Foo f1 = new Foo(1L, "a", "c1");
final Foo f2 = new Foo(1L, "a", "c2");
final Foo f3 = new Foo(2L, "a", "c1");

final List<Foo> li = List.of(f1, f2, f3);
joinedCategoriesReduce(li).forEach(System.out::println);

Output

Foo(id=1, name=a, category=null, categories=[c1, c2])
Foo(id=2, name=a, category=null, categories=[c1])

Another option is to provide a Foo constructor accepting a list of categories:

// class Foo
public Foo(Long id, String name, List<String> cats) {
    this.id = id;
    this.name = name;
    this.categories = cats;
}

Then the map entries could be easily remapped to Foo instance with the list of categories:

static List<Foo> joinedCategoriesMap(List<Foo> input) {
    return input
            .stream()
            .collect(Collectors.groupingBy(Foo::getId))
            .values().stream()
            .map(v -> new Foo(
                    v.get(0).getId(),
                    v.get(0).getName(),
                    v.stream().map(Foo::getCategory).collect(Collectors.toList()))
            )
            .collect(Collectors.toList());
}

Online demo

Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
  • Thanks Alex for the answer. Its certainly in the direction of my goal. However, are you really getting the o/p that you posted? I'm getting a different o/p ```Foo [id=1, name=a, category=null, categories=[null, c2]] Foo [id=2, name=a, category=null, categories=[null]] ``` It looks like the `null` in the identity is messing up something. – Cyriac George Nov 19 '20 at 18:58
  • Yes, it works as I wrote, you may check the link to online demo: https://ideone.com/EJEJA6 – Nowhere Man Nov 19 '20 at 19:17
  • 1
    In the final code snippet, after grouping you may stream the values rather than the entries. I find it slightly simpler. – jaco0646 Nov 19 '20 at 19:25
  • My bad! I got `null, c2` due to an err at my side. I have taken your last recommendation and modified it slightly. Instead of using constructors, I used a couple of methods to get the job done. I'm exploring this option as my object `Foo` may not be a simpleton with just 3 members. It could have a whole bunch of members. Here's the [online code](https://ideone.com/b2A2Cj) for it. If it's not too much to ask, could you pls take a look at it and lemme know if you have got any f/b? – Cyriac George Nov 19 '20 at 20:46
  • It's worth to make `copyWithoutCategories` static and make method `populateCategories` return current instance of `Foo` to chain calls fluently as in `StringBuilder`, then it's possible to simplify `{Foo modified = .. ; return modified;}` part, as shown [in the update online](https://ideone.com/kTHUo3) – Nowhere Man Nov 19 '20 at 21:57
  • That's a valid suggestion. I appreciate that. – Cyriac George Nov 19 '20 at 23:33