Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Turning an array of Java8 streams into a stream of tuples

Let's say I have an array of Java 8 streams: Stream<T>[] streams, I'd like to make a Stream where each element of the new stream is an array composed by picking one element from each of the initial base streams (let's assume they're all sequential).

For instance if I have:

  streams [ 0 ] returning: ( "A", "B", "C" ), 
  streams [ 1 ] returning ( "X", "Y", "Z" ) 
  and streams [ 2 ] as ( "0", "1", "2" )

I'd like a stream that returns

  ( { "A", "X", "0" }, { "B", "Y", "1" }, { "C", "Z", "2" } )

Is there some code that already implements this? I have an idea of how to do it, it would be a generalisation of the pair case, but I'd like to know if something reusable is already around.

EDIT: sorry, I realised I need some clarification:

  • I don't want to create the whole matrix, I want a stream that dynamically returns one row at a time (first A/X/0, then B/Y/1, etc), without having to occupy memory with all the rows in advance. I'm fine with reasonable assumptions over the sizes of base streams (eg, taking the minimum, stopping as soon as there is a stream that has no more elements to return).

  • I know this can be implemented by first turning the base streams into iterators, then creating a new iterator which of next() picks one element from each of the underlining iterators and returns a new row. That is what the pair example I've linked above does and I could implement it that way on myself, here I'm trying to understand if it has been already done in some library (I know JDK has no such function).

like image 630
zakmck Avatar asked Jul 24 '17 23:07

zakmck


1 Answers

First things first, it's a very bad idea to keep an array of streams, because they can't be reused and it complicates already complicated possible solutions.

No, it's not possible in the plain JDK. There is no zip functionality, neither we have Tuples, so I am afraid this is the best thing you can come up with:

Stream[] streams = Stream.of(
  Stream.of("A", "B", "C"),
  Stream.of("X", "Y", "Z"),
  Stream.of("0", "1", "2"))
    .toArray(Stream[]::new);

String[][] arrays = Arrays.stream(streams)
  .map(s -> s.toArray(String[]::new))
  .toArray(String[][]::new);

int minSize = Arrays.stream(arrays)
  .mapToInt(s -> s.length)
  .min().orElse(0);

String[][] zipped = IntStream.range(0, minSize)
  .mapToObj(i -> Arrays.stream(arrays)
  .map(s -> s[i])
    .toArray(String[]::new))
  .toArray(String[][]::new);

First, we need to convert an array of streams into an array of arrays or anything else that we can traverse more than once.

Second, you did not specify what to do if streams inside the array have varying lengths, I assumed standard zip behaviour which joins elements as long as we can extract elements from each collection.

Third, I am creating here a stream of all possible indexes for zipping (IntStream.range(0, minSize)) and manually extracting element by element from each nested array.

It's fine to use .get() on Optional here because calculating minSize guarantees that there will be something in there.

Here is a more reasonable approach assuming that we are dealing with lists of lists:

List<List<String>> lists = Arrays.asList(
  Arrays.asList("A", "B", "C"),
  Arrays.asList("X", "Y", "Z"),
  Arrays.asList("0", "1", "2"));

final int minSize = lists.stream()
  .mapToInt(List::size)
  .min().orElse(0);

List<List<String>> result = IntStream.range(0, minSize)
  .mapToObj(i -> lists.stream()
  .map(s -> s.get(i))
    .collect(Collectors.toList()))
  .collect(Collectors.toList());

Java 9's Stream API additions will probably allow us to drop the calculation of minSize.

If you want the generation of sequences to remain lazy, you can simply not collect the results:

IntStream.range(0, minSize)
  .mapToObj(i -> lists.stream()
    .map(s -> s.get(i))
    .collect(Collectors.toList()));
like image 98
Grzegorz Piwowarek Avatar answered Oct 22 '22 10:10

Grzegorz Piwowarek