Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 203
drJava Avatar asked Jul 10 '17 08:07

drJava


8 Answers

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

like image 89
Tamas Avatar answered Oct 12 '22 01:10

Tamas


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));
like image 30
Holger Avatar answered Oct 12 '22 01:10

Holger


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
            ));
like image 25
Adrian Avatar answered Oct 12 '22 01:10

Adrian


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

like image 43
Timothy Truckle Avatar answered Oct 12 '22 01:10

Timothy Truckle


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);
   }}); 
like image 37
Robin Topper Avatar answered Oct 12 '22 02:10

Robin Topper


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]
like image 24
shapiy Avatar answered Oct 12 '22 01:10

shapiy


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.

like image 42
Eugene Avatar answered Oct 12 '22 01:10

Eugene


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]
like image 21
fps Avatar answered Oct 12 '22 01:10

fps