11
\$\begingroup\$

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?

\$\endgroup\$
  • \$\begingroup\$ Happens that I was also desperately searching for "merge" in the Stream API while reviewing the same thing here, something which takes Stream<T>, Stream<T>, BiFunction<T, R> and returns Stream<R>. Seems we found a gap in the Stream API. \$\endgroup\$ – Christian Hujer Jan 24 '15 at 10:39
  • \$\begingroup\$ Related third-party library: github.com/jOOQ/jOOL \$\endgroup\$ – h.j.k. Jan 25 '15 at 15:27
7
\$\begingroup\$

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();
}
\$\endgroup\$
  • \$\begingroup\$ I like this answer, but I assume the reason Scanner was used was to be able to use it on System.in as well - as can be done in the question. Any way this approach can be extended to also support System.in ? (I guess not, because System.in is infinite as long as it is not closed) \$\endgroup\$ – Simon Forsberg Jan 24 '15 at 14:15
  • 2
    \$\begingroup\$ @SimonAndréForsberg - I did not include the System.in support, but note that it can be easily done with BufferedReader.lines()... let me fix my answer.... \$\endgroup\$ – rolfl Jan 24 '15 at 14:18
  • \$\begingroup\$ Nice one, didn't think of that solution! Already +1'd though ;) \$\endgroup\$ – Simon Forsberg Jan 24 '15 at 14:41
  • \$\begingroup\$ You could always use Pattern.splitAsStream to tidy the toInts method. \$\endgroup\$ – Boris the Spider Apr 25 '16 at 9:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.