14 instanceofおよびswitchによる安全なキャスト

instanceof演算子を使用して、ある型から別の型へのキャストが安全かどうかを実行時に確認できます。キャストまたは変換中に情報が失われたり、例外がスローされたりしない場合、キャストまたは変換は安全です。instanceof演算子を使用して、2つの参照型または2つのプリミティブ型の間でキャストが安全かどうかをテストできます。また、変換の安全性は、switch文および式の動作に影響します。

ノート:

instanceof演算子を使用して2つのプリミティブ型の間でキャストが安全かどうかをテストする機能は、プレビュー機能です。プレビュー機能は、設計、仕様および実装が完了したが、永続的でない機能です。プレビュー機能は、将来のJava SEリリースで、異なる形式で存在することもあれば、まったく存在しないこともあります。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。

詳細は、JEP 455: パターン、instanceofおよびswitchでのプリミティブ型(プレビュー)を参照してください。

次の例では、instanceofを型比較演算子として使用して、Shape sCircleかどうかをテストします。Circleである場合、Shape sCircleにキャストすることは安全です。つまり、出力できる半径が間違いなく存在します:

public interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
//...

Shape s = new Rectangle(4,3);
if (s instanceof Circle) {
    Circle c = (Circle)s;
    System.out.println("Radius: " + c.radius());
}

Javaランタイムは、参照型を別の参照型にキャストできない場合、ClassCastExceptionをスローします。s instanceof Circle式をこの例から削除した場合、Javaランタイムは、RectangleであるShapeCircleにキャストしようとしたときに、例外をスローします:

Shape s = new Rectangle(4,3);

// ClassCastException: class Rectangle cannot be cast
// to class Circle
Circle c = (Circle)s;

System.out.println("Radius: " + c.radius());

プリミティブ型間のキャストの場合、JavaランタイムがClassCastExceptionをスローすることはありません。値を変換した結果(型に互換性がある場合)、値の全体的な大きさと精度および範囲に関する情報が失われる可能性があります。次の例では、intbyteにキャストします:

int i = 2345323;
byte b = (byte)i;
System.out.println(b);

次のように出力されます。

107

2345323から107のように不適切に、または意図せず変換された値、あるいは2.05fから2のようにわずかに変換された値は、プログラム全体に静かに伝播して、見つけにくいbugの原因になる可能性があります。

プリミティブ型のintbyteは範囲が異なるため、値をキャストする前にそれをテストできます:

if (i >= -128 && i <= 127) {
    System.out.println((byte)i);
}

ただし、2つのプリミティブ型間のキャストが安全かどうかをより明確かつ簡潔にテストする方法は、instanceofを型比較演算子として使用することです:

if (i instanceof byte) {
    System.out.println((byte)i);
}

この例では、依然として値ibyteにキャストする必要があります。instanceof型比較演算子は、ibyteかどうかをチェックするのみです。実際のキャストは実行しません。

instanceofをパターン一致演算子として使用する方法もありますが、その方法には、テストが成功した場合にキャストが実行されるという追加の利点があります:

if (i instanceof byte b) {
    System.out.println(b);
}

変換の安全性とswitch

2つのswitch文を含む次の例について考えてみます。コンパイラは、最初のswitch文に対してエラーを生成します:

float f = 100.01f;

switch (f) {
    // error: the switch statement does not cover
    // all possible input values
    case int i ->
        System.out.println(i + " as an int ");
}

switch (f) {
    case double d ->
        System.out.println(d + " as a double");
}

最初のswitch文は、int値のみを処理するため、網羅的ではありません。caseラベルcase int iは、switch文のセレクタ式のfloat値をint値に変換します。この変換は安全ではありません。つまり、値の全体的な大きさと精度および範囲に関する情報が失われる可能性があります。このswitch文が許可されている場合、100.01fから100のように不適切に、または意図せず変換された値は、プログラム全体に伝播して、見つけにくいbugの原因になる可能性があります。このような状況が発生するのを防ぐために、コンパイラはエラーを生成します。

2番目のswitch文は網羅的です。ラベルdouble dは、セレクタ式のfloatdoubleに変換します。これは無条件に正確な変換です。つまり、最初の型から2番目の型への変換によって値に関する情報が失われたり、例外がスローされたりしないことが保証されているとコンパイル時にわかっています。無条件に正確な変換の例としては、byteからintへの変換、byteからByteへの変換、intからlongへの変換、StringからObjectへの変換などがあります。

プリミティブ型間の変換が無条件に正確であるかどうかは、パターンが他のパターンよりどのように優先されるかにも影響します。(「パターン・ラベルの優位性」を参照してください。)次の例では、コンパイラは型パターンbyte bに関するエラーを生成します:

switch (f) {
    case int i ->
        System.out.println(i + " as an int");
    // error: this case label is dominated by a preceding case label
    case byte b ->
        System.out.println(b + " as a byte");
    default ->
        System.out.println(f + " as a float");
}

型パターンint iは、byteからintへの変換が無条件に正確であるため、型パターンbyte bより優先されます。