Java8Puzzler・その1(解答編)

Java8Puzzler・その1の解答編です。

第一問

import java.util.stream.*;

public class Puzzler1
{
    public static void main(String[] args)
    {
        String s = Stream.of("Hello", "Fizz", "Buzz", "JavaSE8")
            .map(String::length)
            .map(Integer::toString)
            .collect(Collectors.toStringJoiner(", "))
            .toString();
        System.out.println(s);
    }
}

一見するとよくあるStreamのサンプルです。

"Hello"、"Fizz"、"Buzz"、"JavaSE8"が要素のStreamを作り、map(String::length)で文字数に変換し、map(Integer::toString)でIntegerからStringに変換し、collect(Collectors.toStringJoiner(", "))で連結しようとしています。
ちなみにcollect(Collectors.toStringJoiner(", "))は各要素(文字列)を", "で連結します。前後に余計なものをつけるという事は行われません。
例えばStream.of("Hello", "Fizz", "Buzz", "JavaSE8").collect(Collectors.toStringJoiner(", ")は"Hello, Fizz, Buzz, JavaSE8"となります。

そのため(1)は末尾に", "が付いているため間違いです。同様に(2)も[]に囲まれているため間違いです。
では(3)の"5, 4, 4, 7"が正解でしょうか?実はこれも間違いです。

map(Integer::toString)でコンパイルエラーになるため正解は(4)のコンパイルエラーです。

Puzzler1.java:10: エラー: インタフェース Stream<T>のメソッド mapは指定された型に適用できません。
            .map(Integer::toString)
            ^
  期待値: Function<? super Integer,? extends R>
  検出値: Integer::toString
  理由: 型変数Rを推論できません
    (引数の不一致: メソッド参照が無効です。toStringの参照はあいまいです
        Integerのメソッド toString(int)とIntegerのメソッド toString()の両方が一致します)
  R,Tが型変数の場合:
    メソッド <R>map(Function<? super T,? extends R>)で宣言されているRはObjectを拡張します
    インタフェース Streamで宣言されているTはObjectを拡張します
エラー1

なぜこのようなエラーが発生するかというとmapは(? super Integer) -> (? extends R)を受け取ります。そこにInteger::toStringを指定しているのですがこのInteger::toStringのシグネチャに問題があります。
Integer::toStringはオーバーロードされていて複数のシグネチャがありますが関連する物は次の2つです。

public String toString() // Integer::toString : Integer -> String
public static String toString(int) // Integer::toString : int -> String

intはIntegerと互換性があるためこの2つが候補として考えられてしまい一意に定めることができなくなってしまうためコンパイルエラーになってしまいます。

この場合の最も簡単な解決策はInteger::toStringの代わりにObject::toStringを使用することです。

        String s = Stream.of("Hello", "Fizz", "Buzz", "JavaSE8")
            .map(String::length)
            .map(Integer::toString)
            .collect(Collectors.toStringJoiner(", "))
            .toString();

その他にもラムダ式で書くなどの解決策があります。

        String s = Stream.of("Hello", "Fizz", "Buzz", "JavaSE8")
            .map(String::length)
            .map(n -> n.toString())
            .collect(Collectors.toStringJoiner(", "))
            .toString();
        String s = Stream.of("Hello", "Fizz", "Buzz", "JavaSE8")
            .mapToInt(String::length)
            .mapToObj(Integer::toString)
            .collect(Collectors.toStringJoiner(", "))
            .toString();

この場合、Integer::toStringを使用していますが問題ありません。
何故ならばmapToIntでIntStreamになっておりmapToObjの引数はint -> ? extends Stringになり同様に「int -> String」と「Integer -> String」の両方が候補として考えられますがターゲット型の方でプリミティブ型が使用されている場合はプリミティブ型のものが優先して使用されるので「int -> String」のInteger::toStringに一意に決まるためコンパイルエラーになりません。

第二問

import java.util.stream.*;

public class Puzzler2
{
    public static void main(String[] args)
    {
        String s = IntStream.range(1, 10)
            .filter(_ -> _ % 2 == 0)
            .mapToObj(Integer::toString)
            .collect(Collectors.toStringJoiner(", "))
            .toString();
        System.out.println(s);
    }
}

まず、IntStream.range(a, b)はaからbを範囲としてその間にある値のIntStreamを作成します。この時bは含まれずaからb-1までの値のIntStreamになります。
そのため10が含まれてしまっている(1)は間違っていることがわかります。

次に_ -> _ % 2 == 0について真になる要素のみにする処理を行なっており、_ % 2 == 0が真になるのは偶数のときです。なので奇数が出力されている(3)は誤りとなります。

では(2)が正解でしょうか。実は、これもコンパイルエラーの(4)が正解になります。
なぜならラムダ式の引数に対して_(アンダーバー単体)を使用することが出来なくなっているためです。これは他のラムダ式がある言語で_が特別な意味を持っており混乱を避けるためです。ちなみにそれ以外の場所での_は問題ありませんが警告となります。

第三問

import java.util.*;
import java.util.stream.*;

public class Puzzler3
{
    public static void main(String[] args)
    {
        List<Integer> primes = new ArrayList<Integer>();
        prime(2, 13, primes);
        String s = primes.stream()
            .map(Object::toString)
            .collect(Collectors.toStringJoiner(", "))
            .toString();

        System.out.println(s);
    }

    public static void prime(int start, int end, List<Integer> primes)
    {
        // TODO: error check
        prime(IntStream.range(start, end), primes);
    }

    public static void prime(IntStream s, List<Integer> l)
    {
        OptionalInt firstOpt = s.findFirst();

        if (firstOpt.isPresent())
        {
            int first = firstOpt.getAsInt();

            l.add(first);
            prime(s.filter(n -> n % first != 0), l);
        }
    }
}

素数を求めようとしています。
例のごとくIntStream.rangeを使用しているので答えに13が入ってしまっている(2)は誤りです。となると正常に実行できるケースは(1)のみになりますが正解は(4)の実行時エラーです。

このプログラムを実行すると次のような例外が発生します。

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon
	at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:200)
	at java.util.stream.IntPipeline.<init>(IntPipeline.java:91)
	at java.util.stream.IntPipeline$StatelessOp.<init>(IntPipeline.java:590)
	at java.util.stream.IntPipeline$9.<init>(IntPipeline.java:324)
	at java.util.stream.IntPipeline.filter(IntPipeline.java:323)
	at Puzzler3.prime(Puzzler3.java:34)
	at Puzzler3.prime(Puzzler3.java:22)
	at Puzzler3.main(Puzzler3.java:10)

この例外が発生する理由はStreamは常に一本道でなければいけないのですが、findFirstによって既に終了しているのに更に分岐して処理を続けようとしたためです。

図で表すと次のようになりますが二行目のfilterの所で分岐が発生してしまい例外が発生してしまいます。*1

s--* findFirst
 |---filter(×)---* findFirst
 |---filter(×)---* findFirst
 |---filter(×)---* findFirst
    ...

第四問

import java.util.stream.*;

public class Puzzler4
{
    public static String toString(IntStream is)
    {
        return is.mapToObj(Integer::toString)
            .collect(Collectors.toStringJoiner(", "))
            .toString();
    }

    public static void main(String[] args)
    {
        IntStream is = IntStream.range(1, 10);

        IntStream even = is.filter(n -> n % 2 == 0);
        IntStream odd = is.filter(n -> n % 2 != 0);

        System.out.println(toString(even));
        System.out.println(toString(odd));
    }
}

第四問も第三問と同様の理由により実行時エラーが発生するため(5)が正解となります。

is---filter = even
  |---filter(×) = odd

Streamが常に一本道でなければいけないという原則は簡単に破れてしまうので注意が必要です。

*1:*は即時処理オペレーション