instanceofとは、「変数が指しているオブジェクトが、指定した型として扱えるか」を true / false で答える演算子です。 obj instanceof String のように書き、true のときだけ安全にキャストできます。Java 16 からは obj instanceof String s と書くだけで判定とキャストを一度に済ませる「パターンマッチング」が使え、Java 21 では switch 文や record の分解にも広がりました。

この記事では、同じ題材(図形の面積計算)を「従来のキャスト書き」「Java 16 のパターンマッチング」「Java 21 の record パターン」「Java 21 の switch パターン」の4通りで書き比べ、すべて OpenJDK 21.0.10 で動かした実行結果を載せています。null を渡したときの結果、パターン変数が使える範囲、&& と || での違い、無関係な型を書いたときのコンパイルエラーも実際に再現しました。コードはすべて1ファイルで完結しており、java ファイル名.java でそのまま試せます。

instanceofの早見表

まずは「書き方」と「結果」をまとめた早見表です。迷ったらこの表だけ見れば用が足ります。

書き方の早見表(バージョン別)

やりたいこと 書き方 使えるバージョン
型を判定するだけ obj instanceof String すべて
判定してキャスト(従来) if (obj instanceof String) { String s = (String) obj; } すべて
判定と同時に変数へ入れる if (obj instanceof String s) { ... } Java 16 以降
判定+追加条件 if (obj instanceof String s && s.length() > 3) Java 16 以降
否定して早期 return if (!(obj instanceof String s)) return; // この後で s が使える Java 16 以降
record を分解して取り出す if (obj instanceof Point(int x, int y)) Java 21 以降
switch で型ごとに分岐 switch (obj) { case String s -> ...; default -> ...; } Java 21 以降
switch で型+条件 case String s when s.isEmpty() -> ... Java 21 以降
switch で null を受ける case null -> ... Java 21 以降
使わない部分を _ で省く obj instanceof Point(int x, _) Java 22 以降

結果の早見表(Java 21 で確認)

式 結果 補足
null instanceof String false null は必ず false。例外にもならない
new Dog() instanceof Animal(Dog は Animal の子) true 親クラスでも true
new Dog() instanceof Puppy(Puppy は Dog の子) false 子クラスには当てはまらない
new Dog() instanceof Object true null 以外は必ず true
Integer の変数 instanceof String コンパイルエラー 無関係な型は実行前に検出される
obj instanceof String s || s.isEmpty() コンパイルエラー || の右側では s が使えない
obj instanceof List<String>(obj は Object) コンパイルエラー 型引数は実行時に確認できない。List<?> と書く
int の変数 instanceof Integer コンパイルエラー 左側は参照型だけ(Java 21 時点)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java instanceofの使い方とパターンマッチング早見表」

instanceofの基本|型を判定して true / false を得る

構文と読み方

変数 instanceof 型名

「左の変数が指すオブジェクトは、右の型として扱えますか?」という質問だと考えると読みやすくなります。扱える(=その型か、その子クラス、またはそのインターフェースを実装したクラス)なら true です。

親子関係のあるクラスと配列・コレクション・ラッパークラスで、結果を一通り確かめます。

import java.util.*;

public class Hier {
    static class Animal {}
    static class Dog extends Animal {}
    static class Puppy extends Dog {}
    interface Runner {}

    public static void main(String[] args) {
        Animal a = new Dog();
        System.out.println("Dog instanceof Animal : " + (a instanceof Animal));
        System.out.println("Dog instanceof Dog    : " + (a instanceof Dog));
        System.out.println("Dog instanceof Puppy  : " + (a instanceof Puppy));
        System.out.println("Dog instanceof Object : " + (a instanceof Object));
        System.out.println("Dog instanceof Runner : " + (a instanceof Runner));
        System.out.println("getClass()==Animal    : " + (a.getClass() == Animal.class));
        System.out.println("getClass()==Dog       : " + (a.getClass() == Dog.class));
        System.out.println("Animal.class.isInstance(a): " + Animal.class.isInstance(a));

        Object arr = new int[]{1, 2};
        System.out.println("int[] instanceof int[]   : " + (arr instanceof int[]));
        System.out.println("int[] instanceof Object[]: " + (arr instanceof Object[]));
        Object strs = new String[]{"a"};
        System.out.println("String[] instanceof Object[]: " + (strs instanceof Object[]));

        Object list = new ArrayList<String>(List.of("a"));
        System.out.println("ArrayList instanceof List<?>: " + (list instanceof List<?>));
        if (list instanceof List<?> l && !l.isEmpty() && l.get(0) instanceof String first) {
            System.out.println("先頭要素: " + first);
        }

        Object n = 42;
        System.out.println("42 instanceof Integer: " + (n instanceof Integer));
        System.out.println("42 instanceof Number : " + (n instanceof Number));
        System.out.println("42 instanceof Long   : " + (n instanceof Long));
    }
}

実行結果(java Hier.java、OpenJDK 21.0.10):

Dog instanceof Animal : true
Dog instanceof Dog    : true
Dog instanceof Puppy  : false
Dog instanceof Object : true
Dog instanceof Runner : false
getClass()==Animal    : false
getClass()==Dog       : true
Animal.class.isInstance(a): true
int[] instanceof int[]   : true
int[] instanceof Object[]: false
String[] instanceof Object[]: true
ArrayList instanceof List<?>: true
先頭要素: a
42 instanceof Integer: true
42 instanceof Number : true
42 instanceof Long   : false

ここから読み取れるポイントは次のとおりです。

  • 変数の宣言型(Animal)ではなく、実際に入っているオブジェクト(Dog)で判定される。
  • 親クラス・実装しているインターフェースに対しても true になる。「ちょうどそのクラスか」を知りたいときは getClass() == Dog.class を使う。
  • int[] は Object[] ではない(要素が基本型のため)。String[] は Object[] として true。
  • Object に入れた 42 は Integer に自動で包まれる(オートボクシング)ので、Integer と Number には true、Long には false。

instanceof・getClass・isInstance の違い

方法 子クラスのとき null のとき 型を実行時に決められるか 向いている場面
obj instanceof Dog true false 決められない(コードに直接書く) ふだんの型判定
obj.getClass() == Dog.class false NullPointerException 決められる 「ちょうどそのクラス」だけを通したいとき
Dog.class.isInstance(obj) true false 決められる(Class 型の変数で渡せる) 型を引数や設定で受け取るとき

同じ題材で書き比べ|従来のキャストからswitchパターンまで

ここがこの記事の中心です。「円・長方形・正方形の面積を求める」という同じ処理を、4通りの書き方で並べます。

public class Compare {
    sealed interface Shape permits Circle, Rect, Square {}
    record Circle(double radius) implements Shape {}
    record Rect(double width, double height) implements Shape {}
    record Square(double side) implements Shape {}

    // 1. 従来の書き方(全バージョン):判定 → キャスト → 取り出し
    static double areaClassic(Object obj) {
        if (obj instanceof Circle) {
            Circle c = (Circle) obj;
            return Math.PI * c.radius() * c.radius();
        } else if (obj instanceof Rect) {
            Rect r = (Rect) obj;
            return r.width() * r.height();
        } else if (obj instanceof Square) {
            Square s = (Square) obj;
            return s.side() * s.side();
        }
        throw new IllegalArgumentException("未対応: " + obj);
    }

    // 2. Java 16〜:instanceof のパターンマッチング(型パターン)
    static double areaPattern(Object obj) {
        if (obj instanceof Circle c) {
            return Math.PI * c.radius() * c.radius();
        } else if (obj instanceof Rect r) {
            return r.width() * r.height();
        } else if (obj instanceof Square s) {
            return s.side() * s.side();
        }
        throw new IllegalArgumentException("未対応: " + obj);
    }

    // 3. Java 21〜:instanceof の record パターン(分解まで一度に)
    static double areaRecordPattern(Object obj) {
        if (obj instanceof Circle(double radius)) {
            return Math.PI * radius * radius;
        } else if (obj instanceof Rect(double w, double h)) {
            return w * h;
        } else if (obj instanceof Square(var side)) {
            return side * side;
        }
        throw new IllegalArgumentException("未対応: " + obj);
    }

    // 4. Java 21〜:switch のパターンマッチング(sealed なので default 不要)
    static double areaSwitch(Shape shape) {
        return switch (shape) {
            case Circle c -> Math.PI * c.radius() * c.radius();
            case Rect r   -> r.width() * r.height();
            case Square s -> s.side() * s.side();
        };
    }

    // 5. Java 21〜:switch + record パターン + when ガード
    static String describe(Shape shape) {
        return switch (shape) {
            case Circle(double r) when r >= 10 -> "大きい円 半径=" + r;
            case Circle(double r)              -> "円 半径=" + r;
            case Rect(double w, double h) when w == h -> "正方形に近い長方形 " + w;
            case Rect(double w, double h)      -> "長方形 " + w + "x" + h;
            case Square(double side)           -> "正方形 " + side;
        };
    }

    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Rect(2, 3), new Square(4), new Circle(10) };
        for (Shape s : shapes) {
            System.out.printf("%-34s classic=%.2f pattern=%.2f record=%.2f switch=%.2f%n",
                s, areaClassic(s), areaPattern(s), areaRecordPattern(s), areaSwitch(s));
        }
        for (Shape s : shapes) {
            System.out.println(describe(s));
        }
    }
}

実行結果(java Compare.java、OpenJDK 21.0.10):

Circle[radius=1.0]                 classic=3.14 pattern=3.14 record=3.14 switch=3.14
Rect[width=2.0, height=3.0]        classic=6.00 pattern=6.00 record=6.00 switch=6.00
Square[side=4.0]                   classic=16.00 pattern=16.00 record=16.00 switch=16.00
Circle[radius=10.0]                classic=314.16 pattern=314.16 record=314.16 switch=314.16
円 半径=1.0
長方形 2.0x3.0
正方形 4.0
大きい円 半径=10.0

4通りとも同じ結果になります。違いは「書く量」と「コンパイラが助けてくれる範囲」です。

書き方 1分岐あたりの行数 型名を書く回数 分岐の書きもれをコンパイラが検出 使えるバージョン
1. 従来のキャスト 3行(判定・キャスト・処理) 3回(判定・変数の型・キャスト) しない すべて
2. 型パターン 2行 1回 しない Java 16 以降
3. record パターン 2行(アクセサ呼び出しも不要) 1回 しない Java 21 以降
4. switch パターン 1行 1回 する(sealed と組み合わせた場合) Java 21 以降

1. 従来の書き方|判定してからキャストする

if (obj instanceof Circle) { Circle c = (Circle) obj; ... } は、すべてのバージョンで動く基本形です。Circle という型名を3回書くことになり、判定とキャストで別の型を書いてしまうと、実行時に ClassCastException になります。判定とキャストを必ずセットで書くのがこの形の約束ごとです。

2. Java 16以降|判定と同時に変数へ入れる(型パターン)

obj instanceof Circle c の Circle c の部分を型パターン、c をパターン変数と呼びます。判定が true のときだけ、キャスト済みの値が c に入ります。型名を書くのは1回だけなので、書き間違える場所が減ります。

3. Java 21以降|recordの中身まで一度に取り出す(recordパターン)

if (obj instanceof Rect(double w, double h)) {
    return w * h;   // r.width() と書かずに済む
}

record(Java 16 で入ったデータ用のクラス)であれば、型の判定と同時に中身を変数へ分解できます。Square(var side) のように var で型を推論させることもできます。入れ子の record もまとめて分解できます(後述)。

4. Java 21以降|switchで型ごとに分岐する

分岐が3つ以上あるなら switch が読みやすくなります。sealed interface(実装できるクラスを限定したインターフェース)と組み合わせると、default を書かなくても「全種類を扱っている」ことをコンパイラが確認してくれます。 試しに Square の case を消すと、次のコンパイルエラーになります。

ErrExhaust.java:6: エラー: switch式がすべての可能な入力値をカバーしていません
        return switch (s) {
               ^

(英語表示では the switch expression does not cover all possible input values。)新しい図形を追加したときに、対応が必要な場所をコンパイラが教えてくれるわけです。

nullを渡すとどうなるか|instanceofはfalse、switchは例外

instanceof は、左側が null なら相手の型が何であっても false を返します。例外にはなりません。

public class NullCheck {
    record Point(int x, int y) {}

    public static void main(String[] args) {
        Object obj = null;
        String str = null;
        System.out.println("null instanceof String      : " + (obj instanceof String));
        System.out.println("String変数(null) instanceof : " + (str instanceof String));
        System.out.println("null instanceof String s    : " + (obj instanceof String s));
        System.out.println("null instanceof Point(..)   : " + (obj instanceof Point(int x, int y)));
        System.out.println("null instanceof Object      : " + (obj instanceof Object));

        // nullチェックを別に書かなくてよい
        if (obj instanceof String s && !s.isEmpty()) {
            System.out.println("文字列あり: " + s);
        } else {
            System.out.println("文字列ではない、または null");
        }

        // キャストは null でも通る(例外にならない)
        String casted = (String) obj;
        System.out.println("(String) null → " + casted);

        // switch は case null がないと NullPointerException
        try {
            String r = switch (obj) {
                case String s -> "String";
                default -> "その他";
            };
            System.out.println(r);
        } catch (NullPointerException e) {
            System.out.println("switch(null) → " + e.getClass().getName());
        }
        String r2 = switch (obj) {
            case null -> "nullです";
            case String s -> "String";
            default -> "その他";
        };
        System.out.println("case null あり → " + r2);
    }
}

実行結果:

null instanceof String      : false
String変数(null) instanceof : false
null instanceof String s    : false
null instanceof Point(..)   : false
null instanceof Object      : false
文字列ではない、または null
(String) null → null
switch(null) → java.lang.NullPointerException
case null あり → nullです

このため、obj != null && obj instanceof String の obj != null は省略できます。 一方で switch は null を受け取ると例外になるので、null が来る可能性があるなら case null -> を1行足します。case null, default -> とまとめて書くこともできます。

パターン変数が使える範囲|「必ずマッチした」場所だけ

パターン変数のスコープ(使える範囲)は、ふつうの変数のように { } で決まるのではなく、「ここに来たなら必ずマッチしている」とコンパイラが判断できる場所で決まります。これをフロースコープと呼びます。

public class Scope {
    static String early(Object obj) {
        // 否定して早期 return すると、if の「後ろ」で s が使える
        if (!(obj instanceof String s)) {
            return "文字列ではない";
        }
        return "長さ=" + s.length();
    }

    public static void main(String[] args) {
        Object obj = "hello";

        // && の右側では使える
        if (obj instanceof String s && s.length() > 3) {
            System.out.println("1: " + s.toUpperCase());
        }
        // 三項演算子でも使える
        int len = obj instanceof String s ? s.length() : -1;
        System.out.println("2: " + len);
        // 早期 return
        System.out.println("3: " + early(obj));
        System.out.println("4: " + early(123));
        // while の条件でも使える
        Object cur = "abc";
        while (cur instanceof String s && !s.isEmpty()) {
            System.out.println("5: " + s);
            cur = s.substring(1);
        }
        // パターン変数は final ではない(再代入できる)
        if (obj instanceof String s) {
            s = s + "!";
            System.out.println("6: " + s);
        }
    }
}

実行結果:

1: HELLO
2: 5
3: 長さ=5
4: 文字列ではない
5: abc
5: bc
5: c
6: hello!
場所 パターン変数 s 理由
if (obj instanceof String s) { ここ } 使える true のときだけ入る
if (...) { } else { ここ } 使えない false のときに入る
obj instanceof String s && ここ 使える 左が true のときだけ右が評価される
obj instanceof String s || ここ 使えない 左が false のときに右が評価される
obj instanceof String s ? ここ : 使えない ? の直後だけ使える true 側だけ
if (!(obj instanceof String s)) { return; } の後ろ 使える return しなかった=マッチしている
if (!(obj instanceof String s)) { ここ } 使えない マッチしなかった側
if 文を抜けた後(早期 return なし) 使えない マッチしたか決まらない

「否定して早期 return すると、その後ろで変数が使える」のは、入力チェックを先頭にまとめる書き方と相性がよく、ネストを浅く保てます。なお、Java 16 の正式版からパターン変数は final ではなくなり、再代入もできます(6 の結果)。

コンパイルエラーになる書き方と直し方

実際に javac 21.0.10 でコンパイルして出たメッセージを載せます。日本語表示(javac -J-Duser.language=ja)と英語表示の両方を確認しました。

エラーメッセージ早見表

書き方 日本語のメッセージ 英語のメッセージ
obj instanceof String s || s.length() > 3 シンボルを見つけられません(変数 s) cannot find symbol
if の外で s を使う シンボルを見つけられません(変数 s) cannot find symbol
Integer の変数 instanceof String 不適合な型: IntegerをStringに変換できません incompatible types: Integer cannot be converted to String
Object の変数 instanceof List<String> ObjectをList<String>に安全にキャストできません Object cannot be safely cast to List<String>
int の変数 instanceof Integer 予期しない型(期待値: 参照、検出値: int) unexpected type(required: reference, found: int)
同じ名前の変数が既にある 変数 sはすでにメソッド main(String[])で定義されています variable s is already defined in method main(String[])
switch で親の型を先に書く 先行するcaseラベルが、このcaseラベルよりも優位です this case label is dominated by a preceding case label
switch の分岐が足りない switch式がすべての可能な入力値をカバーしていません the switch expression does not cover all possible input values

||(または)の右側ではパターン変数が使えない

public class ErrOr {
    public static void main(String[] args) {
        Object obj = "hello";
        if (obj instanceof String s || s.length() > 3) {
            System.out.println(s);
        }
    }
}
ErrOr.java:4: エラー: シンボルを見つけられません
        if (obj instanceof String s || s.length() > 3) {
                                       ^
  シンボル:   変数 s
  場所: クラス ErrOr
ErrOr.java:5: エラー: シンボルを見つけられません
            System.out.println(s);
                               ^
  シンボル:   変数 s
  場所: クラス ErrOr
エラー2個

|| の右側は「左が false だったとき」に評価されます。つまり s に値が入っていない状況で使おうとしているので、コンパイラが止めてくれます。if の中身(5行目)も、「String ではなかったが右の条件で true になった」場合がありうるため s は使えません。

直し方:「String で、かつ長さが3より大きい」なら && にします。「String ではない、または空文字なら除外」としたいときは、否定形にまとめると s が使えます。

// 直した後:&& なら右側で使える
if (obj instanceof String s && s.length() > 3) { ... }

// 「String でない、または空」を除外したい場合
if (!(obj instanceof String s) || s.isEmpty()) {
    return;
}
// ここでは s が使える(空でない String)

2つ目の形では、|| の右側が評価されるのは「左が false=String だった」ときなので、s が使えます。「どちらに進んだときにマッチが確定しているか」で決まると覚えておくと判断しやすくなります。

if の外でパターン変数を使う

ErrScope.java:7: エラー: シンボルを見つけられません
        System.out.println(s.length());
                           ^
  シンボル:   変数 s
  場所: クラス ErrScope
エラー1個

if ブロックを抜けた後は、マッチしたかどうかが決まらないので使えません。直し方は、処理を if の中に入れるか、前の章の「否定して早期 return」の形にすることです。

無関係な型を書くとコンパイルエラー

Integer num = 42;
if (num instanceof String) { ... }      // 4行目
if (num instanceof String s) { ... }    // 7行目
ErrUnrelated.java:4: エラー: 不適合な型: IntegerをStringに変換できません:
        if (num instanceof String) {
            ^
ErrUnrelated.java:7: エラー: 不適合な型: IntegerをStringに変換できません:
        if (num instanceof String s) {
            ^
エラー2個

Integer 型の変数に String が入ることはありえません。キャストできない組み合わせは instanceof でも書けない、というルールです。「常に false」ではなくコンパイルエラーになるので、書き間違いに早く気づけます。

直し方:変数の型を見直します。いろいろな型が入る想定なら、変数を Object(または共通の親クラス・インターフェース)で宣言します。

Object value = 42;
if (value instanceof String s) { ... }   // コンパイルできる(結果は false)

なお、相手がインターフェースの場合は少し事情が変わります。

interface Runner {}
static class Animal {}
static final class Cat {}

Animal a = new Animal();
System.out.println(a instanceof Runner);   // OK:Animal の子クラスが Runner を実装しているかもしれない
Cat c = new Cat();
System.out.println(c instanceof Runner);   // エラー:final で子クラスを作れないので可能性がない
ErrFinalSealed.java:9: エラー: 不適合な型: CatをRunnerに変換できません:
        System.out.println(c instanceof Runner);   // エラー:final なので可能性がない
                           ^
エラー1個

ジェネリクスの型引数は判定できない

Object obj = new ArrayList<String>();
if (obj instanceof List<String> list) { ... }
ErrGeneric.java:5: エラー: ObjectをList<String>に安全にキャストできません

Java のジェネリクスは、コンパイル後に <String> の情報が消える仕組み(型消去)です。実行時には「List かどうか」しかわからないため、Object から List<String> への判定は書けません。

直し方:List<?> で判定し、要素は取り出してから個別に判定します。

if (obj instanceof List<?> l && !l.isEmpty() && l.get(0) instanceof String first) {
    System.out.println("先頭要素: " + first);
}

元の変数の型から型引数が確定できる場合は、そのまま書けます。次のコードは Java 21 でコンパイル・実行できました。

Collection<String> c = new ArrayList<String>();
if (c instanceof List<String> list) {          // Collection<String> → List<String> は安全
    System.out.println("List<String>: " + list);
}

基本型(int など)は左側に書けない

ErrPrim.java:4: エラー: 予期しない型
        if (i instanceof Integer) {
            ^
  期待値: 参照
  検出値:    int

(8行目の o instanceof int n も同じ「予期しない型」で、期待値は「クラスまたは配列」と表示されました。)

Java 21 では、instanceof の左側は参照型、右側はクラス・インターフェース・配列型に限られます。基本型をパターンで扱う機能は JEP 507(Primitive Types in Patterns, instanceof, and switch)として Java 25 で3回目のプレビューになっていることを OpenJDK の JEP ページで確認しました。プレビュー機能のため、この記事では動かしていません(未実行)。

「必ず当てはまる型」を書いたとき(Java 16〜20 と 21 の違い)

String str = "abc";
if (str instanceof CharSequence cs) {   // String は必ず CharSequence
    System.out.println("CharSequence: " + cs);
}

Java 21 ではコンパイル・実行できます。同じコードを javac --release 17 や --release 20 でコンパイルすると、次のエラーになりました。

OkGeneric.java:9: error: unconditional patterns in instanceof are not supported in -source 17
  (use -source 21 or higher to enable unconditional patterns in instanceof)

Java 16〜20 では「必ずマッチするパターン」はエラー、Java 21 からは許可という違いです。Java 17 の環境で パターン付き instanceof がエラーになったときは、左側の変数の型が、右側の型の子クラスになっていないかを確認してみてください。

switch での順番エラー

return switch (o) {
    case CharSequence cs -> "CharSequence";
    case String s -> "String";     // ここに到達することがない
    default -> "other";
};
ErrDominate.java:5: エラー: 先行するcaseラベルが、このcaseラベルよりも優位です

switch は上から順に調べるので、親の型を先に書くと、子の型の case には決して届きません。直し方は「具体的な型(子)を先、広い型(親)を後」に並べ替えることです。if-else の連鎖では同じ順番でもエラーにならず、そのまま動いてしまうため、switch にするとこの点もコンパイラに任せられます。

recordパターンの応用|入れ子の分解とジェネリクス

public class Nested {
    record Point(int x, int y) {}
    record Line(Point from, Point to) {}
    record Box<T>(T value) {}

    public static void main(String[] args) {
        Object obj = new Line(new Point(0, 0), new Point(3, 4));

        // 入れ子の record を一度に分解
        if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
            double len = Math.hypot(x2 - x1, y2 - y1);
            System.out.println("長さ = " + len);
        }
        // 中身が null の場合、入れ子のパターンにはマッチしない
        Object half = new Line(new Point(0, 0), null);
        System.out.println("to が null: " + (half instanceof Line(Point p1, Point(var x, var y))));
        System.out.println("型パターンなら: " + (half instanceof Line(Point p1, Point p2)));

        // ジェネリックな record
        Object box = new Box<>("本");
        if (box instanceof Box<?>(String s)) {
            System.out.println("Box の中身は文字列: " + s);
        }
    }
}

実行結果:

長さ = 5.0
to が null: false
型パターンなら: true
Box の中身は文字列: 本
  • 入れ子の record は、コンストラクタと同じ形で書けば一度に分解できる。
  • 入れ子の record パターン(Point(var x, var y))は、その部分が null だとマッチしない。
  • 一方、record パターンの中に置いた型パターン(Point p2)は、コンポーネントの型と同じなら null でもマッチし、p2 に null が入る。分解した変数を使う前に、null になりうるかを意識しておくと安心です。

Java 22 からは、使わない部分を _(アンダースコア)で省略できます(JEP 456)。Java 21 で obj instanceof P(int x, _) をコンパイルすると unnamed variables are a preview feature and are disabled by default. というメッセージになり、21 ではプレビュー扱いであることを確認しました。Java 22 以降での動作はこの記事では未実行です。

よく使う応用パターン

equals メソッドを短く書く

import java.util.Objects;

public class EqualsDemo {
    static class Member {
        private final String id;
        private final String name;
        Member(String id, String name) { this.id = id; this.name = name; }

        @Override
        public boolean equals(Object o) {
            // null でも別の型でも false。キャストも不要
            return o instanceof Member other
                && id.equals(other.id)
                && name.equals(other.name);
        }
        @Override
        public int hashCode() { return Objects.hash(id, name); }
    }

    public static void main(String[] args) {
        Member a = new Member("M001", "佐藤");
        System.out.println(a.equals(new Member("M001", "佐藤")));
        System.out.println(a.equals(new Member("M002", "鈴木")));
        System.out.println(a.equals("M001"));
        System.out.println(a.equals(null));
    }
}

実行結果:

true
false
false
false

従来は「null チェック → 型チェック → キャスト → 比較」と4段階で書いていた equals が、o instanceof Member other && ... の1式にまとまります。null が false になる性質がそのまま活きる例です。

Object で受け取った値を型ごとに整形する

import java.util.List;

public class FormatDemo {
    static String format(Object value) {
        return switch (value) {
            case null -> "(なし)";
            case Integer i when i < 0 -> "マイナス " + (-i);
            case Integer i -> "整数 " + i;
            case String s when s.isBlank() -> "(空文字)";
            case String s -> "文字列 " + s;
            case List<?> l -> "リスト " + l.size() + "件";
            default -> "その他 " + value.getClass().getSimpleName();
        };
    }

    public static void main(String[] args) {
        Object[] values = { 5, -3, "備品A", "  ", List.of(1, 2, 3), 2.5, null };
        for (Object v : values) {
            System.out.println(format(v));
        }
    }
}

実行結果:

整数 5
マイナス 3
文字列 備品A
(空文字)
リスト 3件
その他 Double
(なし)

設定値や JSON を読み込んだ結果など、型がまちまちの値を受け取る入口で役立ちます。when のついた case を先、つかない case を後に並べるのがコツです。

instanceofを使うか、オーバーライドで書くかの判断表

型で分岐する方法は instanceof だけではありません。親クラスやインターフェースにメソッドを用意し、各クラスでオーバーライドする方法(ポリモーフィズム)もあります。

状況 おすすめ 理由
自分で作ったクラス群で、処理がそのクラスの責務 メソッドのオーバーライド 型を追加しても呼び出し側を変えずに済む
種類が固定で、処理を1か所にまとめて見たい sealed + switch パターン 書きもれをコンパイラが検出。処理が一覧できる
Object で受け取る(equals、汎用の受け口) instanceof パターン 型がわからない入口では判定が必要
変更できないクラス(ライブラリの型など) instanceof / switch パターン メソッドを足せないので外側で分岐する
分岐が1〜2個 instanceof パターン if のほうが短い
分岐が3個以上 switch パターン 並びが揃って読みやすい
Java 8〜11 で書く 従来のキャスト パターンマッチングは Java 16 から
flowchart TD
    A[型によって処理を変えたい] --> B{そのクラスを自分で変更できる?}
    B -- できない --> E{分岐は3個以上?}
    B -- できる --> C{処理はそのクラス自身の仕事?}
    C -- はい --> D[メソッドをオーバーライド]
    C -- いいえ --> E
    E -- はい --> F[sealed + switch パターン]
    E -- いいえ --> G[instanceof パターン]

バージョン別対応早見表

機能 書き方の例 正式に使えるバージョン 備考
instanceof による型判定 obj instanceof String すべて
instanceof のパターンマッチング(型パターン) obj instanceof String s Java 16(JEP 394) Java 14 でプレビューとして登場
必ずマッチする型パターン str instanceof CharSequence cs Java 21 Java 16〜20 ではコンパイルエラー
switch のパターンマッチング case String s -> Java 21(JEP 441) when ガード、case null も同時に利用可
record パターン obj instanceof Point(int x, int y) Java 21(JEP 440) 入れ子の分解、var も可
無名パターン _ obj instanceof Point(int x, _) Java 22(JEP 456) Java 21 ではプレビュー
基本型のパターン obj instanceof int i 未定(Java 25 時点で第3プレビュー、JEP 507) この記事では未実行

手元の javac 21 で --release を切り替えて確認した結果は次のとおりです。

コード --release 11 --release 16 --release 17 --release 21
o instanceof String s エラー OK OK OK
switch (o) { case String s -> ... } エラー エラー エラー OK
o instanceof P(int x, int y) エラー エラー エラー OK

Java 11 を指定したときのメッセージは次のとおりでした。

P16.java:1: エラー: instanceofでのパターンの一致は-source 11でサポートされていません

このメッセージが出たら、コードではなく JDK やビルド設定(--release、Maven の maven.compiler.release など)のバージョンを確認します。

従来の書き方からの書き換えチェックリスト

Java 16 以降へ移行するときに、機械的に書き換えられる形をまとめました。

直す前 直した後
if (o instanceof A) { A a = (A) o; ... } if (o instanceof A a) { ... }
if (o != null && o instanceof A) if (o instanceof A)
if (!(o instanceof A)) return false; A a = (A) o; if (!(o instanceof A a)) return false;
if (o instanceof A && ((A) o).size() > 0) if (o instanceof A a && a.size() > 0)
  • [ ] パターン変数の名前が、同じメソッド内の既存の変数と重なっていないか(重なると 変数 sはすでに…定義されています のエラー)
  • [ ] || でつないだ条件の右側でパターン変数を使っていないか
  • [ ] switch に null が渡る可能性があるなら case null を足したか
  • [ ] switch の case は「具体的な型 → 広い型」の順に並んでいるか
  • [ ] ジェネリクスは List<?> のようにワイルドカードで判定しているか

よくある質問

Q. instanceof は null を渡すと例外になりますか? A. なりません。左側が null のときは、右側の型が何であっても false を返します。そのため obj != null && obj instanceof String の null チェックは省略できます。パターンマッチング(obj instanceof String s)や record パターンでも同じく false です。switch に null を渡したときだけは NullPointerException になるので、case null を追加します。

Q. パターンマッチングは Java 何から使えますか?Java 8 や 11 では? A. obj instanceof String s の形は Java 16 から正式に使えます。switch のパターンマッチングと record パターンは Java 21 からです。Java 8・11 では従来の「判定してからキャスト」の書き方を使います。javac 21 で --release 11 を指定すると「instanceofでのパターンの一致は-source 11でサポートされていません」というエラーになることを確認しました。

Q. instanceof と getClass() での比較はどう違いますか? A. instanceof は子クラスのオブジェクトでも true になります。getClass() == Dog.class は「ちょうど Dog クラス」のときだけ true で、子クラスは false です。また getClass() は null に対して呼ぶと NullPointerException になります。ふだんの型判定は instanceof、厳密に同じクラスだけを通したいときは getClass() と使い分けます。

まとめ

  • instanceof は「その型として扱えるか」を true / false で返す。親クラスやインターフェースに対しても true になる。
  • null は必ず false。別途の null チェックは省略できる。switch に null を渡すときは case null を足す。
  • Java 16 以降は obj instanceof String s で判定とキャストを一度に。型名を書くのは1回で済む。
  • パターン変数は「必ずマッチした」場所でだけ使える。&& の右側は使える、|| の右側は使えない、否定して早期 return した後ろは使える。
  • 無関係な型(Integer と String など)やジェネリクスの型引数はコンパイルエラーになり、実行前に気づける。
  • Java 21 では record パターンで中身の分解、switch パターンで型ごとの分岐ができ、sealed と組み合わせると分岐の書きもれをコンパイラが検出する。
  • 自分で変更できるクラスならオーバーライド、型がわからない入口や変更できないクラスなら instanceof / switch、と使い分ける。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java instanceofの使い方とパターンマッチング早見表」

株式会社RJCでは、Javaの型判定やパターンマッチングの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?