6

How can I extract two elements from a Stream by their positions? For example I'm trying to extract element 0 and 1 (these numbers are arbitrary!) from a Stream<String>. A naive approach is this:

List<String> strings = Arrays.asList("s0", "s1", "s2", "s3", "s4");
Consumer<String> c0 = s -> System.out.println("c0.accept(" + s + ")");
Consumer<String> c1 = s -> System.out.println("c1.accept(" + s + ")");
strings.stream().skip(0).peek(c0).skip(1).peek(c1).findAny();

This produces the following output:

c0.accept(s0)
c0.accept(s1)
c1.accept(s1)

I understand that it's because s0 will enter the stream, encounter skip(0), then peek(c0) (which gives the the first line) and then skip(1), which will skip this element and then apparently continue with the next element from the beginning of the stream.

I thought I could use these consumers to extract the strings, but c0 would be overwritten by the second element:

String[] extracted = new String[2];
c0 = s -> extracted[0];
c1 = s -> extracted[1];

EDIT:

These are the characteristics of the stream:

  • There's a stream only, not a list or an array
  • The stream is possibly infinite
  • The stream can be made sequential
steffen
  • 16,138
  • 4
  • 42
  • 81
  • First, `skip(0)` doesn't do anything, you can remove it. Second, you'll have to explain your use case a bit more, because you'd be better off extracting by index using the original list, rather than using streams. – Andreas Sep 09 '15 at 14:23
  • You can't really extract by index from a `Stream` as a stream isn't a `List` - it's an arbitrary _flow_ of items. You will need to tell us what you are trying to do as this sounds very much like an [XY Problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – Boris the Spider Sep 09 '15 at 14:23
  • @Andreas As I said the numbers are arbitrary, so think of `skip(pos0)` instead of `skip(0)` and `skip(pos1)` for `skip(1)`. "The original list" does not exist (as mentioned). – steffen Sep 09 '15 at 14:25
  • @BoristheSpider Yes, I know, but I should be able to say "take element 5 and 10 from the 'flow of items'", isn't it? – steffen Sep 09 '15 at 14:27
  • 1
    @steffen and if the stream is **parallel**? There is no "item 5" in the general case. – Boris the Spider Sep 09 '15 at 14:30
  • 2
    Why not ask the stream for an iterator and then just skip elements as desired? The stream must be sequential. – fps Sep 09 '15 at 15:33
  • 2
    I would not say it's a duplicate. In the [linked](http://stackoverflow.com/q/18552005/4856258) question random access to the stream source was allowed and many solutions use this property. Here OP explicitly says, it's not allowed. On the other hand the problem is more specific and yields different answers. That problem cannot be solved with `limit`, for example. – Tagir Valeev Sep 10 '15 at 01:31

6 Answers6

4

Given your restriction you can combine the limit() with custom collector like this:

public static <T, A, R> Collector<T, ?, R> collectByIndex(Set<Integer> wantedIndices, 
                                                          Collector<T, A, R> downstream) {
    class Acc {
        int pos;
        A acc = downstream.supplier().get();
    }
    return Collector.of(Acc::new, (acc, t) -> {
        if(wantedIndices.contains(acc.pos++))
            downstream.accumulator().accept(acc.acc, t);
    }, (a, b) -> {throw new UnsupportedOperationException();}, // combining not supported
       acc -> downstream.finisher().apply(acc.acc));
}

Here Set<Integer> wantedIndices is the set containing the indices of wanted elements (not limited by 2). Usage:

Set<Integer> wantedIndices = new HashSet<>(Arrays.asList(1, 3));
Stream<String> input = Stream.of("s0", "s1", "s2", "s3", "s4");
List<String> result = input.limit(Collections.max(wantedIndices)+1)
            .collect(collectByIndex(wantedIndices, Collectors.toList()));
// [s1, s3]
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
2

Here's an approach I haven't seen in the other answers. It uses a variation of the ubiquitous Pair class:

class Pair<T> {
    final T first;
    final T last;
    Pair(T t1, T t2) { first = t1; last = t2; }
    Pair(T t) { first = last = t; }
    Pair<T> merge(Pair<T> other) { return new Pair<>(this.first, other.last); }
}

Once you have this, you can easily get the first and last elements of a stream. Given an infinite stream and desired indexes, you can use skip() and limit() to trim the stream to contain just the desired elements:

static <T> Pair<T> firstAndLast(Stream<T> stream, int firstIndex, int lastIndex) {
    // ensure indexes >= 0 and firstIndex <= lastIndex
    return stream.skip(firstIndex)
                 .limit(lastIndex - firstIndex + 1)
                 .map(Pair::new)
                 .reduce(Pair::merge)
                 .orElseThrow(() -> new IllegalArgumentException("nonexistent"));
}

Other variations would include inlining the construction or merge logic into the stream operations instead of having it on the Pair class. Refactor to taste.

You'd use it like this:

    Stream<String> str = Stream.of("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
    Pair<String> pair = firstAndLast(str, 4, 5);
    System.out.println(pair.first + " " + pair.last);

    e f
Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
1

This solution comes from the comment by Federico Peralta Schaffner:

public String[] collect(Stream<String> stream, int... positions) {
    String[] collect = new String[positions.length];
    Iterator<String> iterator = stream.iterator();
    int skipped = 0;
    for (int pos = 0; pos < positions.length; pos++) {
        while (skipped++ < positions[pos]) {
            iterator.next();
        }
        collect[pos] = iterator.next();
    }
    return collect;
}

This is the most direct and straightforward idea and works great.

steffen
  • 16,138
  • 4
  • 42
  • 81
1

The main obstacle is the single-use nature of Streams which can be circumvented:

static <T> List<T> get(Stream<? extends T> s, int... positions) {
    Spliterator<? extends T> sp=s.spliterator();
    ArrayList<T> list=new ArrayList<>(positions.length);
    int current=0;
    for(int i: positions) {
        if(i<current) throw new IllegalArgumentException("positions not ascending");
        Optional<? extends T> o
            =StreamSupport.stream(sp, false).skip(i-current).findFirst();
        if(!o.isPresent()) break;
        current=i+1;
        list.add(o.get());
    }
    return list;
}

Though I’m not sure whether I like it…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Looks more cumbersome than @steffen solution and you rely on the unspecified fact that `findFirst()` does not move the spliterator further than necessary... – Tagir Valeev Sep 10 '15 at 11:59
  • Which of his solutions do you refer to? And why should `findFirst()` of a sequential stream ever advance the spliterator more than necessary? Anyway, I’m not seeing any use case for this kind of task at all, so it doesn’t matter. It’s more of an entertainment question… – Holger Sep 10 '15 at 12:12
  • Did not notice, that @steffen posted two answers (why SO allows this?). I'm speaking about [this one](http://stackoverflow.com/a/32484436/4856258). It definitely should not, it's just not specified. – Tagir Valeev Sep 10 '15 at 14:20
  • @TagirValeev it's allowed because there are many different ways to solve the same problem and you can choose the best in your opinion :) – Matthieu May 30 '18 at 16:08
0

You could write something like the following:

public static void main(String[] args) throws Exception {
    List<String> strings = Arrays.asList("s0", "s1", "s2", "s3", "s4");
    System.out.println(getNthElement(strings.stream(), 0)); // prints "s0"
    System.out.println(getNthElement(strings.stream(), 1)); // prints "s1"
}

private static <T> T getNthElement(Stream<T> stream, int n) {
    return stream.skip(n).findFirst().get();
}

Note that this will throw an exception if there are less than n elements in the stream. Also, it makes sense only if the Stream is not parallel.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • 1
    It will also read the `Stream` multiple times which might be an issue if the `Stream` is once-only. – Boris the Spider Sep 09 '15 at 14:28
  • @BoristheSpider Yes, it will. I can't see any other way to do it though. – Tunaki Sep 09 '15 at 14:28
  • I would zip the stream with an index and then write a custom collector. But I'm sure what the OP wants exactly so I don't think it's worth answering the XY problem. – Boris the Spider Sep 09 '15 at 14:29
  • Actually I can't read the stream multiple times because I get a stream instance and not a list or an array. And second I can't "zip it", because it's possibly infinite. – steffen Sep 09 '15 at 14:41
  • @steffen what do you mean _I can't "zip it", because it's possibly infinite_?? That's exactly why you **need to** zip it. – Boris the Spider Sep 09 '15 at 14:52
  • @BoristheSpider Ah, ok, I see! That's certainly an option! – steffen Sep 09 '15 at 15:02
0

This is a (not very beautiful, but easy and working) solution:

List<String> strings = Arrays.asList("s0", "s1", "s2", "s3", "s4");
String[] extracted = new String[2];
Consumer<String> c0 = s -> extracted[0] = extracted[0] == null ? s : extracted[0];
Consumer<String> c1 = s -> extracted[1] = extracted[1] == null ? s : extracted[1];
strings.stream().skip(0).peek(c0).skip(1 - 0).peek(c1).findAny();
steffen
  • 16,138
  • 4
  • 42
  • 81