Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to flatten nested map of lists with Java 8 Stream? [duplicate]

I have a structure that looks like this:

public class Category {
    private String tag;
    private String name;
    private String description;
    private List<Item> items;
}

and Item looks like this

public class Item {
    private String itemTag;
    private String itemName;
    private String itemType;
    private Integer itemStatus;
    private List<Item> items;
}

It's not the best design - I know, but I have no power to change that design.

I'm trying to find a way to flatten this structure to a single Stream and find an Item with matching itemTag. Using this code:

String tagToFind = "someTag";
List<Category> categories = getCategoriesList(); // <-- returns a list of Category
Item item = categories.stream()
                .flatMap(category -> category.getItems().stream())
                .filter(tagToFind.equals(item.getItemTag()))
                .findFirst();

But this only searches one level of the item list. If I want to go a level deeper I can simply do :

Item item = categories.stream()
                .flatMap(category -> category.getItems().stream())
                .flatMap(item->item.getItems().stream()))
                .filter(tagToFind.equals(item.getItemTag()))
                .findFirst();

Which works fine. But I'm trying to find a more scalable way of doing this where it can go as deep as the nested lists go. Is there an efficient way of doing this?

like image 319
Varda Elentári Avatar asked Apr 06 '19 16:04

Varda Elentári


2 Answers

You need a separate method for the recursion. You can do it like this:

Optional<Item> item = categories.stream()
        .flatMap(category -> category.getItems().stream())
        .flatMap(MyClass::flatMapRecursive)
        .filter(i -> tagToFind.equals(i.getItemTag()))
        .findFirst();

Use this flatMapRecursive() method:

public Stream<Item> flatMapRecursive(Item item) {
    return Stream.concat(Stream.of(item), item.getItems().stream()
            .flatMap(MyClass::flatMapRecursive));
}

One more thing to consider: The flatMapRecursive() method does no null checks, so every item need at least an empty list, otherwise you will get a NullPointerException.

If null values are possible for the items you can prevent this using an Optional:

public Stream<Item> flatMapRecursive(Item item) {
    return Stream.concat(Stream.of(item), Optional.ofNullable(item.getItems())
            .orElseGet(Collections::emptyList)
            .stream()
            .flatMap(MyClass::flatMapRecursive));
}

Or doing the null check of items before using it:

public Stream<Item> flatMapRecursive(Item item) {
    if (item.getItems() == null) {
        return Stream.empty();
    }
    return Stream.concat(Stream.of(item), item.getItems().stream()
            .flatMap(MyClass::flatMapRecursive));
}
like image 142
Samuel Philipp Avatar answered Sep 20 '22 23:09

Samuel Philipp


Another way :

public Item getFirstItemWithTag(List<Category> categories, String tag) {

        List<List<Item>> items = categories
                .stream()
                .map(Category::getItems)
                .collect(Collectors.toList());

        for(List<Item> items1 : items) {
            List<Item> itemsToAdd = items1.stream().filter(Objects::nonNull).collect(Collectors.toList());

            Optional<Item> first = itemsToAdd
                    .stream()
                    .filter(item -> item != null && tag.equals(item.getItemTag()))
                    .findFirst();

            if (first.isPresent()) {
                return first.get();
            }

            do {

                Stream<Item> itemStream = itemsToAdd
                        .stream()
                        .map(Item::getItems)
                        .flatMap(Collection::stream)
                        .filter(Objects::nonNull);

                first = itemsToAdd
                        .stream()
                        .filter(item -> item != null && tag.equals(item.getItemTag()))
                        .findFirst();

                if (first.isPresent()) {
                    return first.get();
                }

                itemsToAdd = itemStream
                        .collect(Collectors.toList());
            } while (!itemsToAdd.isEmpty());
        }


        return null;
    }

This also removes the null entries of Item and is faster than collecting full list of Items before filtering as it filters as it discovers.

like image 22
Ali Ben Zarrouk Avatar answered Sep 20 '22 23:09

Ali Ben Zarrouk