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関数
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文で書いた場合は以下のようになります。
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
ループの内側のループ処理が対応しているのが分かると思います。
テスト
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型になってしまうので、ダウンキャスト時の実行時エラーにはご注意ください。