kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

ScalaとKotlin(と昔のJava)のジェネリクスが壊れている理由

表題の通りです。とりあえず、Kotlin版とScala版のコード貼ります。

gist.github.com

gist.github.com

これらのコードでは、両方とも明示的なダウンキャストやその他の抜け穴を使っていないので、実行しても決してClassCastExceptionが起きてはいけないのですが、実際に実行するとClassCastExceptionが起きてしまいます。

さて、これはコンパイラの実装のバグでもあり、言語仕様のバグでもあるのですが、ちょっと理由を説明してみたいと思います。

class B extends A with Comparable[B] {
  def compareTo(b: B): Int = 0
}

上記のコードをコンパイルすると、下記のコードが生成されます。ここで、int compareTo(java.lang.Object)というメソッドが 定義されているのが味噌です。

Compiled from "C.scala"
public class B extends A implements java.lang.Comparable<B> {
  public int compareTo(B);
    Code:
       0: iconst_0
       1: ireturn

  public int compareTo(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #2                  // class B
       5: invokevirtual #19                 // Method compareTo:(LB;)I
       8: ireturn

  public B();
    Code:
       0: aload_0
       1: invokespecial #25                 // Method A."<init>":()V
       4: return
}

このような、ジェネリクスをうまく動かすためにコンパイラが自動生成するメソッドのことをブリッジメソッド と呼んだりしますが、このブリッジメソッドが曲者です。このブリッジメソッドでは単に引数をB型にキャストして呼び出すだけですが、これはComparable<B>型として取り扱われた型に対して正しくcompareToメソッドを呼び出すために必要なものです。このブリッジメソッド、本来は内部実装であって直接呼び出せてしまうとまずい代物なのですが、ブリッジメソッドと同じシグニチャのメソッドをスーパークラスで定義してやると、このブリッジメソッドをユーザーが呼び出せてしまい、その結果、ClassCastExceptionが起こるのでした。

実はこの問題、Java Genericsの(実装 and/or 仕様)バグとして以前から知られており、

Java Generics Unsound? | A Concurrent Affair

等で言及されています。結果、Java Genericsではこのようなブリッジメソッドをユーザが呼び出し可能になるような穴はふさがれたのですが、ジェネリクスJavaと類似の方式でコンパイルするScalaやKotlinはこの仕様バグをそのまま引き継いでしまったのでした。

なお、Kotlinのissue trackerには既に報告

https://youtrack.jetbrains.com/issue/KT-13712

してあります。