9

With Java streams it is easy to find an element that matches a given property.
Such as:

 String b = Stream.of("a1","b2","c3") .filter(s -> s.matches("b.*")) .findFirst().get(); System.out.println("b = " + b); 

Produces:
b=b2

However often one wants a value or values right after a match, rather than the match itself. I only know how to do this with old fashion for loops.

 String args[] = {"-a","1","-b","2","-c","3"}; String result = ""; for (int i = 0; i < args.length-1; i++) { String arg = args[i]; if(arg.matches("-b.*")) { result= args[i+1]; break; } } System.out.println("result = " + result); 

Which will produce:
result=2

Is there a clean way of doing this with Java 8 Streams? For example setting result to "2" given the array above and predicate s -> s.matches("-b.*").

If you can get the next value, it would also be useful to also be able to get a list/array of the next N values or all values until another predicate is matched such as s -> s.matches("-c.*").

4
  • 7
    I think the answer is no. I'm reasonably confident that any solution using streams would be really convoluted and readability would suffer. Commented Aug 3, 2015 at 14:03
  • 6
    Streams are a very versatile tool but you know the saying: "when you've got a hammer, everything looks like a nail". But in reality not everything is a nail. Commented Aug 3, 2015 at 14:06
  • This would be easy if streams supported the partition operation (I'm using Clojure's naming). That would allow you to reorganize it into a stream of pairs. There is a way to do that at the spliterator level (I have the code for that), but it's probably not worth it for your case. Commented Aug 3, 2015 at 14:19
  • What you are looking for is a "Transform" duplex stream that emits its own outputs from a source stream "piped" to it. I doubt Java-8 supports that much of "plumbing" with streams. Commented Sep 1, 2015 at 14:34

8 Answers 8

4

This is the kind of spliterator it takes to have this solved with streams:

import java.util.ArrayList; import java.util.List; import java.util.Spliterator; import java.util.Spliterators.AbstractSpliterator; import java.util.function.Consumer; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class PartitioningSpliterator<E> extends AbstractSpliterator<List<E>> { private final Spliterator<E> spliterator; private final int partitionSize; public PartitioningSpliterator(Spliterator<E> toWrap, int partitionSize) { super(toWrap.estimateSize(), toWrap.characteristics()); if (partitionSize <= 0) throw new IllegalArgumentException( "Partition size must be positive, but was " + partitionSize); this.spliterator = toWrap; this.partitionSize = partitionSize; } public static <E> Stream<List<E>> partition(Stream<E> in, int size) { return StreamSupport.stream(new PartitioningSpliterator(in.spliterator(), size), false); } @Override public boolean tryAdvance(Consumer<? super List<E>> action) { final HoldingConsumer<E> holder = new HoldingConsumer<>(); if (!spliterator.tryAdvance(holder)) return false; final ArrayList<E> partition = new ArrayList<>(partitionSize); int j = 0; do partition.add(holder.value); while (++j < partitionSize && spliterator.tryAdvance(holder)); action.accept(partition); return true; } @Override public long estimateSize() { final long est = spliterator.estimateSize(); return est == Long.MAX_VALUE? est : est / partitionSize + (est % partitionSize > 0? 1 : 0); } static final class HoldingConsumer<T> implements Consumer<T> { T value; @Override public void accept(T value) { this.value = value; } } } 

Once you have this tucked away somewhere in the project, you can say

partition(Stream.of("-a","1","-b","2","-c","3"), 2) .filter(pair -> pair.get(0).equals("-b")) .findFirst() .map(pair -> pair.get(1)) .orElse(""); 

As a side point, the presented spliterator supports parallelism by relying on the default implementation of trySplit in AbstractSpliterator.

Sign up to request clarification or add additional context in comments.

2 Comments

I couldn't find a FixedBatchSpliterator class.
That's extra support I have for better parallelization of certain tasks. It is not essential, just delete that factory method (I deleted it from the answer). Alternatively, refer to this answer where I cover that class as well.
4

I found it through this blog post:
https://blog.jooq.org/when-the-java-8-streams-api-is-not-enough/

The library called jOOL has a Github link
https://github.com/jOOQ/jOOL

and Maven central Info here:
http://mvnrepository.com/artifact/org.jooq/jool/0.9.6

The code for the example became:

import org.jooq.lambda.Seq; 

...

 String result = Seq.of(args) .skipWhile(s -> !s.matches("-b.*")) .skip(1) .findFirst() .get(); 

1 Comment

Once you assume only sequential stream processing, a lot of useful higher-order functions become just a trivial implementation step away. However, Java Streams API has a clear focus on data-parallel processing.
1

I cannot say the following is efficient, but it follows an easily appliable pattern. (In a real functional language though, it might be efficient, when adding a filter.)

First collect [[-c, 3], [-b, 2], [-a, 1]] from the string stream.

 List<List<String>> optionLists = Stream.of("-a","1","-b","2","-c","3") .collect(ArrayList<List<String>>::new, (lists, arg) -> { if (arg.startsWith("-")) { List<String> list = new LinkedList<>(); list.add(arg); lists.add(0, list); } else { List<String> list = lists.get(0); list.add(arg); } }, List::addAll); System.out.println(optionLists); 

And then one might turn it in a map for all options.

 List<String> bargs = optionLists.stream() .collect(Collectors.toMap(lst -> lst.get(0), lst -> lst.subList(1, lst.size()))).get("-b"); System.out.println("For -b: " + bargs); 

3 Comments

As pbabcdefp lucidly anticipated, any solution using streams would be really convoluted and readability would suffer :)
@MarkoTopolnik I agree; worked with Stream implementation, but in general I tend to prefer in java to still use a collection to have compact code. Some constructive things are still missing, like your splitting, your reference, maybe jOOL.
Yes, this lack has spawned a myriad of libs which add the obviously missings things, but keep in mind that in most cases these libs do not support parallelization. Personally I don't use any of them because I prefer to stay within the abilities of the JDK. As you, I prefer the old school way if something is not feasible with Streams (or, occasionally, some sort of hack).
1

As far as I can tell there doesn't seem to be an easy way to do this, and a lot of the reason for this stems from the fact that Java's Stream API lacks lacks the features of getting the index of a stream element as well as the ability to zip streams together.

We could imagine an easy solution to the problem if we could get the index of a certain element in the stream, and then simply use the .skip() function paired with the .limit(n) function to discard the elements up to and including the desired match point, and then limit the results to the next n elements.

You might want to check out Protonpack, which is a "Streams utility library for Java 8 supplying takeWhile, skipWhile, zip and unfold." With this library an easy solution for the problem might look like:

Stream<String> stringStream = Stream.of("-a","1","-b","2","-c","3"); Stream<String> nAfterMatch = StreamUtils.skipWhile(stringStream, s -> !(s.matches("-b.*"))) .limit(n); 

Comments

0

For the case where you want the value directly after it seems that you could use som Pair or Tuple class.

String b = Stream.of(Pair.of("-a","1"),Pair.of("-b","2"),Pair.of("-c","3")) .filter(p -> p.first.matches("-b.*")) .map(p -> p.second) .findFirst().get(); 

1 Comment

Is there an easier way to convert the starting array to a stream of Pair/Tuples in the first step?
0

This can be a work-around solution:

String[] args = {"-a","1","-b","2","-c","3"}; String b = IntStream.range(1, args.length) .mapToObj(i -> new Pair<>(args[i-1], args[i])) .filter(pp -> pp.getFirst().startsWith("-b")) .findFirst().get().getSecond(); System.out.println("b = " + b); //=> Output: b = 2 

.mapToObj is being used to make pairs of string values from args array using consecutive indices from input array args.

Comments

0

Thanks to Java Streams takeWhile it is possible:

Stream<String> strings = Stream.of("a1","b2","c3"); final AtomicBoolean matchFound = new AtomicBoolean(false); final AtomicReference<String> stringAfterMatch = new AtomicReference<>(); strings.takeWhile(string -> stringAfterMatch.get() == null).forEach(string -> { if (matchFound.get()) { stringAfterMatch.set(string); } if (string.matches("b.*")) { matchFound.set(true); } }); String b = stringAfterMatch.get(); 

Comments

0
private static boolean containsWords(String input, String[] words) { Arrays.stream(words).allMatch(new Predicate<String>() { @Override public boolean test(String t) { if(input.contains(t)) counter+=1; //add a global counter //ExtensionUtils.keyWordHash.put(t, input); // or you can put the strings to a hashmap and retrieve later return input.contains(t); } }); return Arrays.stream(words).allMatch(input::contains); } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.