Aman
Aman

Reputation: 127

Filtering/removing from a nested List of Objects using streams in Java

Say that we have a 3-dimensional List of Objects:

class OneDObject {
  int id;
  List<Integer> list;
  OneDObject(int id, List<Integer>list) { /* Constructor */ }
  // Getters and Setters
}

class TwoDObject {
  int id;
  List<OneDObject> list;
  TwoDObject(int id, List<OneDObject> list) { /* Constructor */ }
  // Getters and Setters
}

var l1 = List.of(1,2,4);
var l2 = List.of(2,4,6);
var obj1d1 = new OneDObject(1, l1);
var obj1d2 = new OneDObject(2, l2);
var l3 = List.of(obj1d1, obj1d2);
var l4 = List.of(obj1d1);
var obj2d1 = new TwoDObject(3, l3);
var obj2d2 = new TwoDObject(4, l4);
var l5 = List.of(obj2d1, obj2d2);   // 3-d list

Say that I want to filter "l5" such that if any element in the inner most list is an odd number then the entire list should be deleted, and if that makes the 2nd level list as empty, then that should be deleted in return.

So, for the given example, before filtering if it is:

[[[1,2,4],[2,4,6]], [[1,2,4]]]

After filtering, it should be:

[[[2,4,6]]]

How can I do this using streams in Java?

Upvotes: 3

Views: 1326

Answers (3)

M. Justin
M. Justin

Reputation: 21113

The following returns the desired result if you don't actually care about mutating the initial lists and keeping the same in-memory TwoDObject objects you started with — i.e. if creating new filtered objects is acceptable.

List<TwoDObject> filtered = l5.stream()
        .map(do2 -> new TwoDObject(do2.getId(), do2.getList().stream().filter(
                do1 -> do1.getList().stream().noneMatch(e -> e % 2 == 1)).toList()))
        .filter(do2 -> !do2.getList().isEmpty())
        .toList();

This creates a new copied TwoDObject value for each element of the list, but with its list value filtered to just the OneDObject values that contain no odd numbers. It then filters out any of these new TwoDObject values with zero items remaining in its list (i.e. any TwoDObject where all its OneDObject elements were filtered out by the previous step).

Upvotes: 0

Gautham M
Gautham M

Reputation: 4935

Since you need your lists to be updated, in the below solution I am using removeIf method of the List to remove any elements which does not meet the necessary criteria. So for removeIf to work, the list should not be immutable. So replace the var list = List.of(...) code with var list = new ArrayList<>(List.of(...)); (Note: Null checks have been ignored as well.)

Now, this problem could be split into components:

  1. Predicate to identify if a list has any odd elements.
Predicate<OneDObject> hasOdd = obj-> obj.getList().stream().anyMatch(i -> i % 2 != 0);
  1. Predicate to remove objects from 2d list, which has odd elements in its 1d list.
Predicate<TwoDObject> validate2d = obj -> {
    // remove any 1d list that has atleast one odd number.
    obj.getList().removeIf(hasOdd);
    // check if there are any valid 1d lists
    return obj.getList().isEmpty();
};
  1. Now apply the predicate to the final list:
l5.removeIf(validate2d); // l5 will now contain only the 2d object having [2,4,6] list

Upvotes: 3

Florian Hartung
Florian Hartung

Reputation: 153

Here's the final code (in Java, but I think it should almost be interchangeable with Kotlin)

List<TwoDObject> l6 = l5.stream()
                .peek(twoDObject -> {
                    List<OneDObject> filteredOneDObjectList = twoDObject.getList()
                            .stream()
                            .filter(oneDObject -> oneDObject.getList()
                                    .stream()
                                    .noneMatch(i -> i % 2 == 1))
                            .toList();

                    twoDObject.setList(filteredOneDObjectList);
                })
                .filter(twoDObject -> twoDObject.getList().size() > 0)
                .toList();

First we go through every twoDObject by calling Stream#peek, then stream its list and filter out every oneDObject, which contains an odd number. Then the list is saved back into the current twoDObject.

In the end we filter out all empty twoDObjects.


Note that Stream#peek should normally only be used for the purpose of debugging and not mutating the stream elements.
In this case it could also be replaced with

List<TwoDObject> l6 = l5.stream()
                .map(twoDObject -> {
                    ...

                    return twoDObject;
                })
                ...

Upvotes: 1

Related Questions