简体   繁体   中英

Convert a List to another List using Stream

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]]]

ie 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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM