CLOVER🍀

That was when it all began.

isInstanceOf、asInstanceOfをnullに対して適用すると?

Nettyのコードを写経したりといった、Java関連のコードを置き換えている時にいつも気になっていたので。
*コメントいただいたので、もう少し内容を見直しました

Scalaでは、Javaでいうinstanceof演算子とキャストがそれぞれAny#isInstanceOf、Any#asInstanceOfというメソッド形式に置き換えられています。よって、nullに対して適用したりすると、危なさそうなのですが実際コケたところを見たことがありません。

これを、ちゃんと見てみることにしました。

まずは動作確認。以下のようなコードを用意します。
instanceof_test.scala

val msg: Any = "Hello World"
println(msg.isInstanceOf[String])
println(msg.asInstanceOf[String])

実行してみます。

$ scala instanceof_test.scala 
true
Hello World

では、これをこう変えてみます。

//val msg: Any = "Hello World"
val msg: Any = null
println(msg.isInstanceOf[String])
println(msg.asInstanceOf[String])

実行してみます。

$ scala instanceof_test.scala 
false
null

…普通に動きましたね。まあ予想通りです。

これをちゃんと見てみましょう。まず、Anyのソースコードから。

src/library-aux/scala/Any.scalaから抜粋

/*                     __                                               *\
**     ________ ___   / /  ___     Scala API                            **
**    / __/ __// _ | / /  / _ |    (c) 2002-2010, LAMP/EPFL             **
**  __\ \/ /__/ __ |/ /__/ __ |    http://scala-lang.org/               **
** /____/\___/_/ |_/____/_/ | |                                         **
**                          |/                                          **
\*                                                                      */

package scala

/** Class `Any` is the root of the Scala class hierarchy.  Every class in a Scala
 *  execution environment inherits directly or indirectly from this class.
 */
abstract class Any {
  〜省略〜

  /** Test whether the dynamic type of the receiver object is `T0`.
   *
   *  Note that the result of the test is modulo Scala's erasure semantics.
   *  Therefore the expression `1.isInstanceOf[String]` will return `false`, while the
   *  expression `List(1).isInstanceOf[List[String]]` will return `true`.
   *  In the latter example, because the type argument is erased as part of compilation it is
   *  not possible to check whether the contents of the list are of the specified type.
   *
   *  @return `true` if the receiver object is an instance of erasure of type `T0`; `false` otherwise.
   */
  def isInstanceOf[T0]: Boolean = sys.error("isInstanceOf")

  /** Cast the receiver object to be of type `T0`.
   *
   *  Note that the success of a cast at runtime is modulo Scala's erasure semantics.
   *  Therefore the expression `1.asInstanceOf[String]` will throw a `ClassCastException` at
   *  runtime, while the expression `List(1).asInstanceOf[List[String]]` will not.
   *  In the latter example, because the type argument is erased as part of compilation it is
   *  not possible to check whether the contents of the list are of the requested type.
   *
   *  @throws ClassCastException if the receiver object is not an instance of the erasure of type `T0`.
   *  @return the receiver object.
   */
  def asInstanceOf[T0]: T0 = sys.error("asInstanceOf")
}

とまあ、sys.errorに渡してすぐエラーになるようになっています。しかも、final宣言されていない割には、ScalaAPIを見ると両方のメソッドともfinalと書かれているので何かトリックがあるんだろうと…。まあ、これが実際に使われているわけではないんでしょうね。

こうなったらJDで逆コンパイルだ!ということで、以下のようなソースを用意。

object Sample {
  def main(args: Array[String]): Unit = {
    val msg: Any = null
    println("isInstanceOf => %s".format(msg.isInstanceOf[String]))
    println("asInstanceOf => %s".format(msg.asInstanceOf[String]))
  }
}

これをfscにかけた後に、JDを適用してみます。このコードだとクラスファイルが2つできます。
まずはどうでもいい方から。

import scala.reflect.ScalaSignature;

@ScalaSignature(bytes="\006\0015:Q!\001\002\t\006\025\taaU1na2,'\"A\002\002\017q*W\016\035;z}\r\001\001C\001\004\b\033\005\021a!\002\005\003\021\013I!AB*b[BdWmE\002\b\025I\001\"a\003\t\016\0031Q!!\004\b\002\t1\fgn\032\006\002\037\005!!.\031<b\023\t\tBB\001\004PE*,7\r\036\t\003'Yi\021\001\006\006\002+\005)1oY1mC&\021q\003\006\002\f'\016\fG.Y(cU\026\034G\017C\003\032\017\021\005!$\001\004=S:LGO\020\013\002\013!)Ad\002C\001;\005!Q.Y5o)\tq\022\005\005\002\024?%\021\001\005\006\002\005+:LG\017C\003#7\001\0071%\001\003be\036\034\bcA\n%M%\021Q\005\006\002\006\003J\024\030-\037\t\003O)r!a\005\025\n\005%\"\022A\002)sK\022,g-\003\002,Y\t11\013\036:j]\036T!!\013\013")
public final class Sample
{
  public static final void main(String[] paramArrayOfString)
  {
    Sample..MODULE$.main(paramArrayOfString);
  }
}

続いて、中身の方へ。

import scala.Predef.;
import scala.ScalaObject;
import scala.collection.immutable.StringLike;
import scala.runtime.BoxesRunTime;

public final class Sample$
  implements ScalaObject
{
  public static final  MODULE$;

  static
  {
    new ();
  }

  public void main(String[] args)
  {
    null; Object msg = null;
    Predef..MODULE$.println(Predef..MODULE$.augmentString("isInstanceOf => %s").format(Predef..MODULE$.genericWrapArray(new Object[] { BoxesRunTime.boxToBoolean(msg instanceof String) })));
    Predef..MODULE$.println(Predef..MODULE$.augmentString("asInstanceOf => %s").format(Predef..MODULE$.genericWrapArray(new Object[] { (String)msg })));
  }

  private Sample$()
  {
    MODULE$ = this;
  }
}

よーく見ると、isInstanceOfが

msg instanceof String

となっていて、asInstanceOfが

(String)msg

となっていますね。

追記
<ご指摘いただいた内容を、Scalaの仕様書を含めて追記します>

こんな挙動をするため、これらのメソッドはnullそのものに対しても起動することができます。

println(null.isInstanceOf[String])  // => false
println(null.asInstanceOf[String])  // => null

この辺りの話は、Scalaの仕様書の75ページ目「6.3 The Null Value」に記載があります。
http://www.scala-lang.org/docu/files/ScalaReference.pdf

ここで、nullオブジェクトに対して適用した場合は、isInstanceOfは常にfalseを返し、asInstanceOfは適用先がAnyRefならばnullに、そうでなければNullPointerExceptionをスローすると書いてあります。

Javaで、以下のようなコードを実行するとNullPointerが発生するのと似たような感じなんでしょうね、仕様的には。

public class CastTest {
    public static void main(String[] args) {
        try {
            Integer integer = null;
            int i = (int)integer;  // NullPointerException
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            Integer integer = null;
            if (integer == 0) {  // NullPointerException
                System.out.println("Hello World");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

んで、コメントいただいている内容ですと、AnyValに対するこれらの挙動が怪しいということでしたので、確認してみましょう。

val i: Any = null
println(i.isInstanceOf[Int])  // => false
println(i.asInstanceOf[Int])  // => 0

確かに、null.asInstanceOf[Int]が0になりました…。

なので、以下のコードはキャストするかどうかで結果が変わります。

println(i.asInstanceOf[Int] == 0)  // => true
println(i == 0)  // => false

こういう目で見ると、ちょっと微妙な気持ちになります。

さて、このあたりはいったいどうなっているかというと??ということで、今度はこんなコードを用意。

object Sample {
  def main(args: Array[String]): Unit = {
    val i: Any = null
    println(i.isInstanceOf[Int])
    println(i.asInstanceOf[Int])

    println(i.asInstanceOf[Int] == 0)
    println(i == 0)
  }
}

このコンパイル結果を、JDで見てみます。今回は、isInstanceOf、asInstanceOfの結果を抜粋します。printlnの部分は省略しています。

    null; Object i = null;
    i instanceof Integer;
    BoxesRunTime.unboxToInt(i);

    BoxesRunTime.unboxToInt(i) == 0;
    BoxesRunTime.equals(i, BoxesRunTime.boxToInteger(0));

というわけで、BoxesRunTime#unboxToIntの結果、こういうことになってそうですね。

んじゃあ、BoxesRunTime#unboxToIntの実装は?
src/library/scala/runtime/BoxesRunTime.javaより抜粋

/*                     __                                               *\
**     ________ ___   / /  ___     Scala API                            **
**    / __/ __// _ | / /  / _ |    (c) 2006-2011, LAMP/EPFL             **
**  __\ \/ /__/ __ |/ /__/ __ |    http://scala-lang.org/               **
** /____/\___/_/ |_/____/_/ | |                                         **
**                          |/                                          **
\*                                                                      */



package scala.runtime;

import java.io.*;
import scala.math.ScalaNumber;

/** An object (static class) that defines methods used for creating,
  * reverting, and calculating with, boxed values. There are four classes
  * of methods in this object:
  *   - Convenience boxing methods which call the static valueOf method
  *     on the boxed class, thus utilizing the JVM boxing cache.
  *   - Convenience unboxing methods returning default value on null.
  *   - The generalised comparison method to be used when an object may
  *     be a boxed value.
  *   - Standard value operators for boxed number and quasi-number values.
  *
  * @author  Gilles Dubochet
  * @author  Martin Odersky
  * @contributor Stepan Koltsov
  * @version 2.0 */
public final class BoxesRunTime
{
    〜省略〜
    public static int unboxToInt(Object i) {
        return i == null ? 0 : ((java.lang.Integer)i).intValue();
    }
    〜省略〜

そりゃあ、0になりますよね…。