While reviewing Multiplying Lists, I got the impression that the problem was naturally suited to Java 8 streams. Unfortunately, since there is no convenient way to zip two streams, the solution ended up being uglier than I expected.
The task is to take lines of input, in the form a0 a1 a2 ... an | b0 b1 b2 ... bn
, where the ai
and bi
are integers, and output the products a0b0 a1b1 a2b2 ... anbn
.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
public class StreamMultiplier {
public static IntStream multiply(IntStream aStream, IntStream bStream) {
PrimitiveIterator.OfInt a = aStream.iterator(),
b = bStream.iterator();
PrimitiveIterator.OfInt product = new PrimitiveIterator.OfInt() {
public boolean hasNext() { return a.hasNext() && b.hasNext(); }
public int nextInt() { return a.next() * b.next(); }
};
return StreamSupport.intStream(
Spliterators.spliteratorUnknownSize(product, Spliterator.ORDERED),
false
);
}
public static IntStream toIntStream(String spaceDelimitedInts) {
return Arrays.stream(spaceDelimitedInts.trim().split("\\s+"))
.mapToInt(Integer::parseInt);
}
public static void main(String[] args) throws FileNotFoundException {
Scanner input = (args.length > 0) ? new Scanner(new File(args[0]))
: new Scanner(System.in);
while (input.hasNextLine()) {
String[] halves = input.nextLine().split("\\|", 2);
System.out.println(
multiply(toIntStream(halves[0]), toIntStream(halves[1]))
.mapToObj(String::valueOf)
.collect(Collectors.joining(" "))
);
}
}
}
Is this use of Spliterators
correct, and can it be improved?
1 Answer 1
Streams are not designed to naturally fit every computational situation in Java. As you have found, you have needed to perform a number of compound/stream/Iterator/array state changes.
- Your data starts as a compound structure in a single file
- you scan that using a Scanner, using the Scanner's next structures.
- You split the lines in to a 2-element array
- you split each element using regex mapped to a parser to get two int arrays
- you use the array-stream to convert the two arrays to int streams
- you convert the streams to lock-step primitive int iterators
- you re-combine the lock-step product result in to another primitive int iterator
- you convert that iterator in to a stream
- you map the product stream to strings
- you collect-and-print the results as a single line.
Note that your work contains far more data state changes than would be necessary using non-stream functionality. The fact that you have to scan the entire line and convert all values to ints means that all the data is in int[]
arrays already, and there is no real benefit for any optimization in the streams using short circuiting.
Additionally, your result is not able to be run on a parallel stream because it would not survive in the Iterator stage.
Correlating two active streams is not an operation that is supported using any 'natural' mechanism in Java8.
But, there are ways to do it using non-stream mechanisms, and also much more natural Java8 ways too.
Stream Solution
First up, use a stream for the file IO. Files.lines(...)
provides a stream of the lines in a file. Next up, instead of streaming over the values in the source arrays, use a stream of what the two arrays have in common, the indices. Stream over the indices.
By streaming the indices you can process the data in a natural stream way and pull the data by index from the corresponding positions in the sources.
Finally, the product
operator should be supplied as an input lambda, and not built in to the stream-iterator-stream translation.
The solution I would use, if forced to use streams for as much as possible, would be:
public static void main(String[] args) throws IOException {
try (Stream<String> lineStream = args.length > 0
? Files.lines(Paths.get(args[0]), StandardCharsets.UTF_8)
: new BufferedReader(new InputStreamReader(System.in)).lines()) {
lineStream.map(line -> processLineWithOperator(line, (a, b) -> a * b))
.forEach(outLine -> System.out.println(outLine));
}
}
private static String processLineWithOperator(String line, IntBinaryOperator operator) {
String[] halves = line.split("\\|");
int[] halfA = toInts(halves[0]);
int[] halfB = toInts(halves[1]);
return IntStream.range(0, halfA.length)
.map(index -> operator.applyAsInt(halfA[index], halfB[index]))
.mapToObj(String::valueOf)
.collect(Collectors.joining(" "));
}
private static int[] toInts(String spaceDelimitedInts) {
return Arrays.stream(spaceDelimitedInts.split("\\s+"))
.mapToInt(Integer::parseInt)
.toArray();
}
-
\$\begingroup\$ I like this answer, but I assume the reason
Scanner
was used was to be able to use it onSystem.in
as well - as can be done in the question. Any way this approach can be extended to also supportSystem.in
? (I guess not, becauseSystem.in
is infinite as long as it is not closed) \$\endgroup\$Simon Forsberg– Simon Forsberg2015年01月24日 14:15:36 +00:00Commented Jan 24, 2015 at 14:15 -
2\$\begingroup\$ @SimonAndréForsberg - I did not include the
System.in
support, but note that it can be easily done withBufferedReader.lines()
... let me fix my answer.... \$\endgroup\$rolfl– rolfl2015年01月24日 14:18:48 +00:00Commented Jan 24, 2015 at 14:18 -
\$\begingroup\$ Nice one, didn't think of that solution! Already +1'd though ;) \$\endgroup\$Simon Forsberg– Simon Forsberg2015年01月24日 14:41:12 +00:00Commented Jan 24, 2015 at 14:41
-
\$\begingroup\$ You could always use
Pattern.splitAsStream
to tidy thetoInts
method. \$\endgroup\$Boris the Spider– Boris the Spider2016年04月25日 09:38:49 +00:00Commented Apr 25, 2016 at 9:38
Stream<T>
,Stream<T>
,BiFunction<T, R>
and returnsStream<R>
. Seems we found a gap in the Stream API. \$\endgroup\$