倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Stream#collect() の型推論は correct ?

最近 Java 8 Project Lambda をちょっとイジってるんですが、そこでコンパイラに怒られた話(Java 8 ea-b90)。 本質的には Java 8 がどうこうというより Generics の話かと思いますが。

以前の記事java.util.stream.Stream インターフェースに定義されている collect() メソッドの使い方を見ました:

<R> R collect(Collector<? super T,R> collector)
<R> R collect(Supplier<R> resultFactory,
              BiConsumer<R,? super T> accumulator,
              BiConsumer<R,R> combiner)

特に2つ目の collect() メソッドに関して、これは(StringBuilder のような)ビルダーや(可変)コレクションを使って Stream の各要素を「足し上げて」いくメソッドでした。 例えば、StringBuilder を使って Stream<String> の要素を連結するには

Stream<String> stream = 
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

StringBuilder sb =
    stream.collect(StringBuilder::new,
                   StringBuilder::append,
                   StringBuilder::append);

System.out.println(sb.toString());
    // 「JavaGroovyScalaClojureKotlinJythonJRuby」と表示

のようにするのでした。 第1引数はビルダーのインスタンス生成(初期値)、第2引数はビルダーに要素を「足す」操作、第3引数はビルダー同士を「足す」操作です。 以下、stream オブジェクトとして、このコードとおなじオブジェクト(但し何の操作も走査も行っていない)とします。

一応、目的はランダムな順序にされた Stream オブジェクトを作ろうとして、(unordered() じゃ無理だったので)一度全要素を HashSet オブジェクトに格納してから Set#stream() メソッドで新たに Stream オブジェクトを生成することです。 まぁ、その目的自体はいいとして、この手順で新たに Stream オブジェクトを作ろうとしてコンパイラに怒られたお話。

まず、上記の手順をステップ・バイ・ステップで行うと、特に問題はありません:

Set<String> set= stream.collect(HashSet::new, Set::add, Set::addAll);
Stream<String> stream1 = set.stream();
stream1.forEach(System.out::print);

まぁでも、これ上手くいくなら、普通次のようにしたいですよね?

Stream<String> stream2 =
    stream.collect(HashSet::new, Set::add, Set::addAll).stream();
    // コンパイル・エラー

stream2.forEach(System.out::print);

でも、これはコンパイルを通りません。 メッセージは「不適合な型:Stream<Object>をStream<String>に変換できません」という旨のメッセージです。 それじゃあ、ということで

Stream<Object> stream3 =
    stream.collect(HashSet::new, Set::add, Set::addAll).stream();

stream3.forEach(System.out::println);

とすると確かにコンパイルは通って実行も出来ます。 「<Object>」と書くのは面倒なので

Stream stream4 =
    stream.collect(HashSet::new, Set::add, Set::addAll).stream();
    // 警告

stream4.forEach(System.out::print);

とすると、「未チェックまたは安全ではありません」という旨のジェネリックな型に生の型を使ったときによく見るメッセージが出て来ますがコンパイル&実行は可能です。 ただし、これら2つの場合は、生成した Stream の要素に対して Object のメソッドしか使っていないからうまくいったけど、要素を String 型として使いたい場合にはキャストの手間が必要です。 collect() がジェネリックなメソッドなので

Stream<String> stream5 = 
    stream().<String>collect(HashSet::new, Set::add, Set::addAll).stream();
    // コンパイル・エラー

stream5.forEach(System.out::println);

と、collect() の型パラメータに <String> を指定してみましたが、むしろもっとマジにコンパイラに怒られたw

さて、Generic なメソッド参照ができたらそこで今回のお話終了、なんですがやり方が分からないので、同じことをメソッド参照ではなくラムダ式で書いてみました:

Stream<String> stream6 =
    stream.collect(() -> new HashSet<String>(),
                   (s, e) -> s.add(e),
                   (s1, s2) -> s1.addAll(s2)).stream();

stream6.forEach(System.out::println);

ちょっと長くなりましたが、これは問題なくコンパイル&実行できました。 でも、実質に String 型の型指定を行っているのは第1引数の中だけなので第2引数はラムダ式で、第2、第3引数はメソッド参照で書いてみると

Stream<String> stream7 =
    stream().collect(() -> new HashSet<String>(), Set::add, Set::addAll).stream();

stream7.forEach(System.out::println);

これも OK。 ただし第1引数を型推論で書くのは NG:

Stream<String> stream8 =
    stream().collect(() -> new HashSet<>(), Set::add, Set::addAll).stream();
    // 警告

stream8.forEach(System.out::println);

以上をまとめると

Stream<String> stream7 =
    stream().collect(() -> new HashSet<String>(), Set::add, Set::addAll).stream();

stream7.forEach(System.out::println);

という風に、第1引数にはラムダ式、第2、第3引数にはメソッド参照を使うってのが無難そう。 そういえば、以前の記事で Stream#toArray(IntFunction) によって Stream オブジェクトを配列に変換しようとしたときも、配列オブジェクトの生成だけは手で書いてましたね。 それと同じことだと思えばいっか。 まぁ今後、型推論ができるようになったり(推論が可能なのか怪しいけど)、型パラメータを指定したメソッド参照ができるようになったり(すでにできる?)する可能性もありますが。

追記
java8-ea-b120 でも変わりありませんでした。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版