Javaの上位互換性を信じすぎてはいけないとか


クイズとか

いつものとおりクイズからスタートです。
以下のプログラムを実行して、得られる結果を答えてください。

import java.util.stream.IntStream;
public class Peeker {

	public static void main(String[] args) {
		long i= IntStream
				.rangeClosed(1,3)
				.map(x->x*2)
				.peek(System.out::print)
				.count();
		System.out.println(i);
	}
}

ちなみにあまり見たことないかも知れませんので書いておきますと、peekは要素を渡すだけ、好きにしてね!という動きをします。
たぶんデバッグ用途以外で使うことはないです。てかJava APIリファレンスにもそう書かれてますし。

This method exists mainly to support debugging,

答えとか

答えは 「Javaのバージョンによる」 です。
なんだってー(
ちなみにJava8では
2463
Java9からは
3
となります。

何が起こっているのかとか

このロジック、終端処理がcount() になっていますよね。
つまり、要素数を数えているだけなのです。
そして、mapは要素の中身を変えているだけで、要素数を変更しません
peekも同様です。
よって、最終結果であるcount()を出すのにmapもpeekも無駄な処理ということになります。
Java9からは、この無駄な処理が無視されるようになったようです。
実際、

		long i= IntStream
				.rangeClosed(1,3)
				.map(x->x*2)
				.filter(x->true)
				.peek(System.out::print)
				.count();
		System.out.println(i);

とかしてあげると、無視できなくなって
2463
という結果になります。

と、記事書いた後で気づいたのですが、この話は実はQiitaで既出でした。
JavaのStreamで変な挙動だなぁと思った話(2つ)
「count()と中間操作」参照

犯人は誰だとか

それではこの最適化は誰が行っているのでしょうか。
実は、私はclassファイル生成時に無駄な処理をカットするものだと思い込んでいました。
ところが、試しにjavap -c でアセンブルコード吐いてみたら

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: iconst_3
       2: invokestatic  #16                 // InterfaceMethod java/util/stream/IntStream.rangeClosed:(II)Ljava/util/stream/IntStream;
       5: invokedynamic #22,  0             // InvokeDynamic #0:applyAsInt:()Ljava/util/function/IntUnaryOperator;
      10: invokeinterface #26,  2           // InterfaceMethod java/util/stream/IntStream.map:(Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream;
      15: getstatic     #30                 // Field java/lang/System.out:Ljava/io/PrintStream;
      18: dup
      19: invokevirtual #36                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      22: pop
      23: invokedynamic #40,  0             // InvokeDynamic #1:accept:(Ljava/io/PrintStream;)Ljava/util/function/IntConsumer;
      28: invokeinterface #44,  2           // InterfaceMethod java/util/stream/IntStream.peek:(Ljava/util/function/IntConsumer;)Ljava/util/stream/IntStream;
      33: invokeinterface #48,  1           // InterfaceMethod java/util/stream/IntStream.count:()J
      38: lstore_1
      39: getstatic     #30                 // Field java/lang/System.out:Ljava/io/PrintStream;
      42: lload_1
      43: invokevirtual #52                 // Method java/io/PrintStream.println:(J)V
      46: return
(略)

おおお?
Java8でもJava9でも、書いたコードそのままにコンパイルされてますね!
実際にJava8でコンパイルしたclassファイルをJava8で実行すると
2463
となりますが、Java9で実行すると
3
という結果が返ってきます。
つまりjavacが最適化したわけではないのです。
そうすると犯人は奴しかありえません。
JIT(Just In Time)コンパイラ
お前かーーっ!

真面目な話とか

ちょっと真面目な話をしておきますと、これ、実はかなり怖いことなんです。
最初に書いたとおり、peekなんてデバッグくらいにしか使わない(使っちゃいけない)のですが、開発環境がJava8で、実行環境はサポート対象のJava最新版とか、そういうケースがあるんですよ。
それで不具合の調査のために、とりあえずpeek入れて中身出力しとけや、みたいな乱暴なコードを無理矢理追加していたりすると、テスト環境ではデータが入っているのに本番環境ではデータが入っていない!みたいな恐ろしい勘違いを生むことが考えられます。

//ヤバいコードの例
//long maxLength = list.length;
//list<Sting>の中身を表示するように変更
long maxLength = list.stream().peek(System.out::println).count();

最後にとか

JavaはGUI系に手を出さない限り、上位互換性が極めて高い言語というのは、おおよそ納得してもらえるかと思います。
だからといって大丈夫と安心していると思わぬ落とし穴にハマります。
バージョンが上がったら必ず動作確認しましょう。