drJava
drJava

Reputation: 787

Split a list into sublists based on a condition with Stream api

I have a specific question. There are some similar questions but these are either with Python, not with Java, or the requirements are different even if the question sounds similar.

I have a list of values.

List1 = {10, -2, 23, 5, -11, 287, 5, -99}

At the end of the day, I would like to split lists based on their values. I mean if the value is bigger than zero, it will be stay in the original list and the corresponding index in the negative values list will be set zero. If the value is smaller than zero, it will go to the negative values list and the negative values in the original list will be replaced with zero.

The resulting lists should be like that;

List1 = {10, 0, 23, 5, 0, 287, 5, 0}
List2 = {0, -2, 0, 0, -11, 0, 0, -99}

Is there any way to solve this with Stream api in Java?

Upvotes: 40

Views: 64834

Answers (9)

Tamas
Tamas

Reputation: 851

Map<Boolean, List<Integer>> results =
  list1.stream().collect(Collectors.partitioningBy(n -> n < 0));

I think that this one is prettier and easy to read. (You can then get the negative and non-negative list from the map.)

Upvotes: 85

Sanjay Bharwani
Sanjay Bharwani

Reputation: 4769

I have found two solutions for splitting the stream based on condition

Your List is List1 = {10, -2, 23, 5, -11, 287, 5, -99}

List1.stream().collect(Collectors.partitioningBy(num -> num <0))

This would give a Map with below key values

"true" : 10,24,5,287,5
"false": -2,-11,-99

Another solution is on the same line, but instead of using partitioningBy, use groupingBy

List2.stream().collect(Collectors.groupingBy(num -> num <0))

Upvotes: 0

Adrian
Adrian

Reputation: 3134

Since Java 12 it can be done very simple by using Collectors::teeing:

var divided = List.of(10, -2, 23, 5, -11, 287, 5, -99)
            .stream()
            .collect(Collectors.teeing(
                    Collectors.mapping(i -> Math.max(0, i), Collectors.toList()),
                    Collectors.mapping(i -> Math.min(0, i), Collectors.toList()),
                    List::of
            ));

Upvotes: 14

shapiy
shapiy

Reputation: 1155

There are pros and cons in each solution.

  • for loop is the obvious answer, but your question explicitly mentions Streams API.
  • Using different predicates a) causes code duplication, b) is error prone, c) results in additional processing time — 2N
  • Custom Collector is difficult to implement, and gives the impression of redundant work while the problem seems so straightforward, even naïve.

I haven't seen anyone else mentioning this, but you can collect your numbers in a Map<Boolean,List<Integer>> map, where key corresponds to your grouping criterion, and List is the selection of items matching the criterion, for example:

List<Integer> numbers = List.of(10, -2, 23, 5, -11, 287, 5, -99);
Map<Boolean, List<Integer>> numbersByIsPositive = numbers.stream()
    .collect(Collectors.groupingBy(number -> number >= 0));

List<Integer> positiveNumbers = numbersByIsPositive.get(true);
List<Integer> negativeNumbers = numbersByIsPositive.get(false);

Think of auto-boxing and -unboxing when applying this approach.

Output:

Positive numbers: [10, 23, 5, 287, 5]
Negative numbers: [-2, -11, -99]

Upvotes: 6

fps
fps

Reputation: 34460

A generic solution without streams might consist of choosing between two possible consumers, based on a condition:

private static <T> Consumer<T> splitBy(
        Predicate<T> condition,
        Consumer<T> action1,
        Consumer<T> action2,
        T zero) {
    return n -> {
        if (condition.test(n)) {
            action1.accept(n);
            action2.accept(zero);
        } else {
            action1.accept(zero);
            action2.accept(n);
        }
    };
}

For your specific problem, you could use the splitBy method as follows:

List<Integer> list = Arrays.asList(10, -2, 23, 5, -11, 287, 5, -99);

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();

list.forEach(splitBy(n -> n > 0, list1::add, list2::add, 0));

System.out.println(list1); // [10, 0, 23, 5, 0, 287, 5, 0]
System.out.println(list2); // [0, -2, 0, 0, -11, 0, 0, -99]

Upvotes: 5

Eugene
Eugene

Reputation: 120858

Well you could do that in place:

  List<Integer> left = Arrays.asList(10, -2, 23, 5, -11, 287, 5, -99);
    int[] right = new int[left.size()];

    IntStream.range(0, left.size())
            .filter(i -> left.get(i) < 0)
            .forEach(x -> {
                right[x] = left.get(x);
                left.set(x, 0);
            });
    System.out.println(left);
    System.out.println(Arrays.toString(right));

That is a side-effect, but as far as I can tell, it is a safe side-effect.

Upvotes: 4

Robin Topper
Robin Topper

Reputation: 2344

As shmosel already pointed out in the comments, you'll need two iterations using streams:

List<Integer> list = Arrays.asList(10, -2, 23, 5, -11, 287, 5, -99);
List<Integer> positives = list.stream().map(i -> i < 0 ? 0 : i).collect(Collectors.toList());
List<Integer> negatives = list.stream().map(i -> i < 0 ? i : 0).collect(Collectors.toList());

All in one stream is possible if your list is modifiable. This is not better than a for-loop

List<Integer> list = Arrays.asList(10, -2, 23, 5, -11, 287, 5, -99);
List<Integer> list2 = new ArrayList<>();

IntStream.range(0, list.size()).forEach(i -> {
   int j;
   if ((j = list.get(i)) < 0) {
       list2.add(j);
       list.set(i, 0);
   } else {
       list2.add(0);
   }}); 

Upvotes: 8

Timothy Truckle
Timothy Truckle

Reputation: 15622

Java-Streams are a functional programming feature.

The essential pattern of functional programming is that you convert one collection to one other collection. This means your requirement is does not suit to a functional approach and hence java streams are the second best solution (after legacy for(each) loop).


But
Of cause you can split the problem into two separate FP friendly operations.

The downside it that this requires an additional loop over the input collection. For small collections (up to roughly 100000 items) this may not be a problem but for bigger collections you may raise a performance issue.
Disclaimer: do not choose or deny an approach for performance reasons unless you have justified your decision by measurement with a profiling tool!

conclusion:

I'd consider the "legacy loop" the better approach since it may be more readable in the sense that it better expresses your intent (to split up the collection).

Upvotes: 6

Holger
Holger

Reputation: 298213

If you want to do it in a single Stream operation, you need a custom collector:

List<Integer> list = Arrays.asList(10, -2, 23, 5, -11, 287, 5, -99);

List<List<Integer>> result = list.stream().collect(
    () -> Arrays.asList(new ArrayList<>(), new ArrayList<>()),
    (l,i) -> { l.get(0).add(Math.max(0, i)); l.get(1).add(Math.min(0, i)); },
    (a,b) -> { a.get(0).addAll(b.get(0)); a.get(1).addAll(b.get(1)); });

System.out.println(result.get(0));
System.out.println(result.get(1));

Upvotes: 19

Related Questions