2

In a sorted stream, I'd like to access the previous element.

I'm looking for something like this:

class Main {
  public static void main(String[] args) {
    List<String> stringList = Arrays.asList("1: First", "2: Second", "3: Third", "4: Fourth");

    List<String> resultList = stringList.stream()
      .sorted()
      .filter(s -> s.contains("Second") && s.previous().contains("First"))
                                           ^^^^^^^^^^^^ doesn't compile
      .collect(Collectors.toList());

    System.out.println(resultList);
  }
}

Expected Output in this case is

Second

Important: I'd like to factor out the lambda in the filter() method to reuse it in different places.

BetaRide
  • 16,207
  • 29
  • 99
  • 177
  • so you want to get the `2` index element in sorted stream? or string contains `Second`? – Ryuzaki L Jul 26 '19 at 07:41
  • I want to check a value of the element that is right before the one in the filter method according to the sort order. The example is just a simplified version of the real logic. – BetaRide Jul 26 '19 at 07:44
  • 3
    That is generally not how streams work, you are supposed to lake a given item and that is it. Think about what happens if you call `parallel` on the stream before the `filter` operation. How are you supposed to be able to access stream elements before you if the processor of that stream section maybe has not even processes those. **Use a `for` loop** – luk2302 Jul 26 '19 at 07:45
  • @luk2302 That's just plain wrong! parallel has nothing to do whether a stream is order or not. See https://stackoverflow.com/questions/29216588/how-to-ensure-order-of-processing-in-java8-streams – BetaRide Jul 26 '19 at 07:57
  • Don’t use a stream for that. It’s against the entire idea. – Ole V.V. Jul 26 '19 at 08:23

4 Answers4

1

If i understand you want to filter based on previous element, then the best way is first sort the stream separately if it is not sorted, then use the IntStream

String result = IntStream.range(1, stringList.size())
                .filter(i->stringList.get(i).contains("Second") && stringList.get(i-1).contains("First"))
                .mapToObj(o->stringList.get(o)).findFirst().orElse("");

Below is approach were i have separated filter

Predicate<Integer> filter = i->stringList.get(i).contains("Second") && stringList.get(i-1).contains("First");

String result = IntStream.range(1, stringList.size())
                         .boxed()
                         .filter(filter)
                         .map(o->stringList.get(o))
                         .findFirst()
                         .orElse("");    //you throw an exception also `orElseThrow`
Ryuzaki L
  • 37,302
  • 12
  • 68
  • 98
  • This is not going to work in my situation. As written in the question, I'd like to reuse the filter predicate. Iterating over the index and using a map prevents this. If I have to pack this into a method, I'm better of using a for loop for this. – BetaRide Jul 26 '19 at 08:01
1

You can save the prev every step:

class Main {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("1: First", "2: Second", "3: Third", "4: Fourth");
        List<String> resultList = stringList.stream()
                .sorted()
                .filter(new Predicate<String>() {
                    private String prev = null;

                    @Override
                    public boolean test(String s) {
                        boolean result = s.contains("Second") && prev != null && prev.contains("First");
                        prev = s;
                        return result;
                    }
                })
                .collect(Collectors.toList());
        System.out.println(resultList);
    }
}

And you can define separated custom Predicate class to reuse:

class Main {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("1: First", "2: Second", "3: Third", "4: Fourth");
        List<String> resultList = stringList.stream()
                .sorted()
                .filter(new WithPrevPredicate() {
                    @Override
                    public boolean test(String prev, String current) {
                        return current.contains("Second") && prev != null && prev.contains("First");
                    }
                })
                .collect(Collectors.toList());
        System.out.println(resultList);
    }

    private static abstract class WithPrevPredicate<T> implements Predicate<T> {
        private T prev = null;

        @Override
        public boolean test(T current) {
            boolean test = test(prev, current);
            prev = current;
            return test;
        }

        public abstract boolean test(T prev, T current);
    }
}
yelliver
  • 5,648
  • 5
  • 34
  • 65
  • Such stateful predicates are strongly discouraged. They assume that the processing order is identical to the encounter order, which Streams do not guaranty. Obviously, it doesn’t work for parallel streams, but even for sequential streams where it happens to provide the intended answer, it is not a correct usage. – Holger Jul 26 '19 at 09:17
1

You coud use index to get the prev item.

List<String> resultList = stringList.stream()
            .sorted()
            .filter(s -> s.contains("Second") && stringList.get(stringList.indexOf(s)-1).contains("first"))
            .collect(Collectors.toList());
shalama
  • 1,133
  • 1
  • 11
  • 25
  • 1
    Nice idea! Some caveats with this: 1. It doesn't work with sorted(), since the order of the List is not preservied. 2. It doens't work with dupplicates in the list. 3. It may be inefficient if equals of the list object is expensive or the list is big. – BetaRide Jul 26 '19 at 08:12
0

You can't really get the previous element in a direct way.

In this case, you can create a stream of ints, representing the indexes of the source list. And then you get each pair of elements in the list using the index and index - 1, and filter:

List<String> stringList = Arrays.asList("1: First", "2: Second", "3: Third", "4: Fourth");
// we are intentionally leaving out the first item because it does not have a previous
List<String> result = IntStream.range(1, stringList.size())
        // stringList.get(x - 1) is the previous item here
        .mapToObj(x -> new StringPair(stringList.get(x - 1), stringList.get(x)))
        .filter(x -> x.getItem1().contains("First") && x.getItem2().contains("Second"))
        .map(StringPair::getItem2).collect(Collectors.toList());
System.out.println(result);

StringPair is a simple class like this:

final class StringPair {
    private String item1;
    private String item2;

    public StringPair(String item1, String item2) {
        this.item1 = item1;
        this.item2 = item2;
    }

    public String getItem1() {
        return item1;
    }

    public String getItem2() {
        return item2;
    }
}

This is probably a good time to ask yourself whether using a Stream is a good idea here. Streams may look cool, but they can't solve everything. Maybe switch to an old-fashioned for loop?

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • This is not going to work in my situation. As written in the question, I'd like to reuse the filter predicate. Iterating over the index and using map() prevents this. If I have to pack this into a method, I'm better of using a for loop for this. – BetaRide Jul 26 '19 at 08:03
  • @BetaRide You _can_ still reuse the predicate. Why do you think you can't? It's just a predicate of `StringPair` instead of `String`. – Sweeper Jul 26 '19 at 08:04