【Java】StreamAPIでzip関数を自作する

# zip関数とは
2つ以上の反復可能なオブジェクト（リスト等）を引数に受け取り、各オブジェクトの要素を前から順にまとめたリストを作成する関数です。
例えば`[1, 2, 3]`と`['a', 'b', 'c']`の2つのリストをzip関数に渡すと`[(1, 'a'), (2, 'b'), (3, 'c')]`というリストが返されます。
ここで、`()`はタプル（不変リストのようなもの）を表します。

Python等ではzip関数が標準機能として提供されていますが、Javaにはzip関数が存在しません。
よってJavaでzip関数を使用したい場合は外部ライブラリを使用するか、自分で実装する必要があります。

# 本記事の目的
Javaの標準ライブラリのみを用いた簡易的なzip関数の実装方法をご紹介します。


# 結論
Javaにはタプルがないため、`List.of`で取得できる不変リストで代用します。
## zip関数
```java:DoZip.java
package sample;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Zipper {

	public static List<List<Object>> zip(List<?> ...lists){
		if(Stream.of(lists).anyMatch(Objects::isNull)) return Collections.emptyList(); // ガード節

		int minSize =  Stream.of(lists).mapToInt(List::size).min().orElse(0);

		return IntStream.range(0, minSize)
				.mapToObj(i -> Stream.of(lists).map(l -> l.get(i)).toArray()) // i番目の要素を配列にまとめる
				.map(List::of) // List.ofで不変リストに変換
				.collect(Collectors.toList());
	}

}
```
`Zipper.zip`では任意クラスのリストを可変長引数として受け取り、各要素を前から順にまとめたリストを返します。
長さの違うリストを渡された場合は最小のリストと同じ大きさのリストを返します。
また、いずれかのリストがnullである場合は`return`文の`map(l -> l.get(i))`の箇所で`NullPointerException`が発生してしまうため、1行目のif文（ガード節）でそのようなケースを除外しています。
`return`文のStreamは入れ子になっているせいで複雑に見えるかもしれませんが、やっていることはシンプルです。
`mapToObj`の中で各リストの要素を前からまとめる処理（タプル生成）をし、その後に`List.of`で不変リストに変換しています
これをStream APIではなくfor文で書いた場合は以下のようになります。
```java:return文のStream APIをfor文で書いた場合
		List<List<Object>> zip = new ArrayList<>();
		int tupleSize = lists.length;

		for(int i=0; i < minSize; i++) {
			Object[] tuple = new Object[tupleSize]; // 不変リスト生成用配列を確保

			for(int j=0; j < tupleSize; i++) {
				tuple[j] = lists[j].get(i); // i番目の要素を配列にまとめる
			}

			zip.add(List.of(tuple)); // List.ofで不変リストに変換
		}

		return zip;
```
見比べてみると、`mapToObj`の処理と2重`for`ループの内側のループ処理が対応しているのが分かると思います。


## テスト
```java:DoZipTest.java
package main;

import static sample.Zipper.*;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class ZipperTest {

	public static void printZip(List<? extends List<?>> zip) {
		System.out.println(
				zip.stream()
				.map(l -> l.stream().map(Object::toString).collect(Collectors.joining(", ", "(", ")")))
				.collect(Collectors.joining(", ", "[", "]")));
	}

	public static void main(String[] args) {
		List<Integer> l1 = List.of(1,2,3);
		List<String> l2 = List.of("a", "b", "c", "d");
		System.out.print("zip([1,2,3], [\"a\", \"b\", \"c\", \"d\"]) -> ");
		printZip(zip(l1, l2));

		l1 = List.of(4,3,1,2);
		l2 = List.of("c", "d", "b", "a");
		System.out.print("zip([4,3,1,2], [\"c\", \"d\", \"b\", \"a\"]) -> ");
		printZip(zip(l1, l2));

		l1 = null;
		l2 = List.of("a", "b", "c", "d");
		System.out.print("zip(null, [\"a\", \"b\", \"c\", \"d\"]) -> ");
		printZip(zip(l1, l2));

		l1 = Collections.emptyList();
		l2 = List.of("a", "b", "c", "d");
		System.out.print("zip([], [\"a\", \"b\", \"c\", \"d\"]) -> ");
		printZip(zip(l1, l2));

		l1 = List.of(1,2,3);
		l2 = List.of("a", "b", "c", "d");
		List<Boolean> l3 = List.of(true, false, true, true);
		List<Double> l4 = List.of(0.1, 0.3);
		System.out.print("zip([1, 2, 3], [\"a\", \"b\", \"c\", \"d\"], [true, false, true, true], [0.1, 0.3]) -> ");
		printZip(zip(l1, l2, l3, l4));
	}

}
```
```:実行結果
zip([1,2,3], ["a", "b", "c", "d"]) -> [(1, a), (2, b), (3, c)]
zip([4,3,1,2], ["c", "d", "b", "a"]) -> [(4, c), (3, d), (1, b), (2, a)]
zip(null, ["a", "b", "c", "d"]) -> []
zip([], ["a", "b", "c", "d"]) -> []
zip([1, 2, 3], ["a", "b", "c", "d"], [true, false, true, true], [0.1, 0.3]) -> [(1, a, true, 0.1), (2, b, false, 0.3)]
```
長さの違うリストや空リストを渡した場合も期待通りに動作します。
要素がすべてObject型になってしまうので、ダウンキャスト時の実行時エラーにはご注意ください。

# 参考文献
[Java 関数型プログラミング練習帳 - zipWith -](https://qiita.com/yonetty/items/5d915f99ef80d3e0fe67)