6

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?

Varda Elentári
  • 2,242
  • 6
  • 35
  • 55
  • think of how would you write a method that given a category lists all the items until the deepest level they are available... then formulate that method to return a stream... then use it in a single pipeline. – Naman Apr 06 '19 at 17:57
  • Why would an `Item` object have a list of `Item` objects. Does not seem very right to me. – akortex Apr 06 '19 at 18:00
  • 3
    @Aris_Kortex OP clearly states "*It's not the best design - I know, but I have no power to change that design.*" – user1803551 Apr 06 '19 at 18:00

2 Answers2

6

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));
}
Samuel Philipp
  • 10,631
  • 12
  • 36
  • 56
  • 2
    Using ```MyClass::flatMapRecursive``` is super neat. – Ali Ben Zarrouk Apr 06 '19 at 18:40
  • it would seem that there should be some sort of concatenation in the recursive function in order to aggregate those nested streams, right? or did I understand your solution incorrectly ? So something like : `return Stream.concat( item.getItems().stream().flatMap(MyClass::flatMapRecursive), item.getItems().stream());` ? – Varda Elentári Apr 08 '19 at 02:11
  • @VardaElentári Yes you are right. I tested it again, tested not that much before. Sorry for that. But you should use `Stream.of(item)` instead of `item.getItems().stream()`, if you don't want to loose the first level items. I updated my answer according to that. – Samuel Philipp Apr 08 '19 at 19:06
  • 1
    Aaah yes that worked! thanks for updating your answer! – Varda Elentári Apr 08 '19 at 19:16
2

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.

Ali Ben Zarrouk
  • 1,891
  • 16
  • 24
  • 1
    He said he is trying to find **an** `Item` with matching `itemTag`. Im not excluding if the parent has a different tag, Im just moving "floor by floor" until I find a match :). – Ali Ben Zarrouk Apr 06 '19 at 19:55