Java#例外

導入

Throwable は Java 言語のすべての例外のルート クラスであり、2 つの直接サブクラス Error と Exception を派生します。エラーは、アプリケーション自体が克服および回復できない深刻な問題を表しており、エラーが発生すると、スレッドまたは仮想マシンさえも終了します。例外は、プログラムが克服および回復できる問題を表し、処理タイミングに応じてコンパイル時例外と実行時例外に分けられます。

コンパイル時例外は修復できる例外です。コードのコンパイル中に、Java プログラムはコンパイル時例外を明示的に処理する必要があり、そうでない場合はコンパイルできません。ランタイム例外は通常、ソフトウェア開発者による考慮不足によって引き起こされる問題です。ソフトウェアユーザーこの問題を克服して復元することはできませんが、ソフトウェア システムはこの問題の下でも動作し続ける可能性があり、深刻な場合にはソフトウェア システムが停止します。

画像の説明を追加してください

例外処理

例外処理計画

Java の例外処理には、例外をキャッチして処理する方法と、例外をスローする方法の 2 つのオプションがあります。

コンパイル時の例外処理には 2 つのオプションがあり、現在のメソッドが例外の処理方法を知っていれば、それをキャッチします。現在のメソッドがそれを処理する方法を知らない場合は、メソッドを定義するときに例外をスローするように宣言します。

実行時例外はコードの実行中にのみ検出されるため、コンパイル中に捕捉する必要はありません。例えば、除数が0だったり、配列の添え字が範囲外だったり、頻繁に発生するため対処が面倒であり、表示宣言やキャプチャはプログラムの可読性や動作効率に大きな影響を与えます。したがって、仮想マシンによって自動的に検出され、スローされます。もちろん、キャプチャ処理をアクティブに表示することもできます。

例外関連のキーワード

Java の例外メカニズムは主に、try、catch、finally、throw、throws の 5 つのキーワードに依存しています。一般に、try、catch、finally は例外をキャッチするために組み合わせて使用​​されます。throw と throw は、例外をスローするために単独で使用されます。

      // try catch finally
       try {
    
    
             // 可能触发异常的代码
        } catch (XXXException e) {
    
     // XXXException  :代表异常类型 
            // 这里进行处理异常
        } finally {
    
    
           //这里进行资源释放
       }

try の直後に中括弧で展開されたコードブロックを try ブロックと呼び、例外が発生する可能性のあるコードは try ブロック内に配置されます。catch の後に例外タイプとコード ブロックを定義します。try ブロック内のコードのセクションが例外をトリガーし、catch で定義された例外タイプと一致する場合、catch ブロックの処理ロジックに従います。

try コード ブロックの後に複数の catch コード ブロックを続けて、さまざまな種類の例外をキャッチできます。Java 仮想マシンは、例外ハンドラーを上から下まで照合します。したがって、前の catch コード ブロックによってキャプチャされた例外タイプは、次の例外タイプをカバーできません。そうでない場合、コンパイラはエラーを報告します。

finally ブロックと catch ブロックの後、finally ブロックは try ブロックで開かれた物理リソースをリサイクルするために使用され、例外メカニズムにより、finally ブロックは常に実行されます。


    //throws 抛出异常,方法签名处抛出。
    private static void test() throws XXXException {
    
    }
    
    //throw 作为语句使用,代码中直接抛出一个异常。 throw new XXXException();
    private static void getCode(String type) {
    
    
       if (type == null) throw new IllegalArgumentException("参数不能为空");
    } 

throws キーワードは主に、メソッドによってスローされる可能性のある例外を宣言するためにメソッド シグネチャで使用されます。throw は、実際の例外をスローするために使用されます。 throw は、特定の例外オブジェクトをスローするための別個のステートメントとして使用できます。

例外処理クリ

(1) コンパイル時例外

    public static void main(String[] args) {
    
    
        File file = new File("F://a.txt");
        if (!file.exists()) {
    
    
            file.createNewFile(); // 这段代码直接运行,这里编译不通过,报编译时异常。
        }
    }

コンパイル時例外は、コードのコンパイル中にエラーを報告します (コンパイル中に報告されるエラーは、必ずしもコンパイル時例外であるとは限りません)。このような例外は、コーディング中に手動でキャッチまたはスローする必要があります~

/**
 * Create by SunnyDay on 2022/04/21 18:26
 */
public class ExceptionDemo {
    
    
    public static void main(String[] args) throws IOException {
    
    
        tryCatch();
        throwsException(); // 这里选择继续抛出给main
    }

    /**
     * 捕获异常栗子
     */
    private static void tryCatch() {
    
    
        try {
    
    
            File file = new File("F://a.txt");
            if (!file.exists()) {
    
    
                file.createNewFile();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            System.out.println("finally 块");
        }

    }

    /**
     * 方法签名处抛出异常栗子。
     * 注意若是此方法被其他方法A调用,那么A需要捕获或者抛出处理。
     */
    private static void throwsException() throws IOException {
    
    
        File file = new File("F://a.txt");
        if (!file.exists()) {
    
    
            file.createNewFile();
        }
    }
}

(2) 実行時例外

    private static void runtimeException(String name) {
    
    
        name.length(); //name 为空时java.lang.NullPointerException.直接crash。
    }

ランタイム例外は通常、開発者のコ​​ードの考慮不足によって発生します。通常、積極的にキャッチしたりスローしたりする必要はありません。ただし、必要に応じて、次のように積極的に捕捉して処理できます。

    /**
     * 不过一般不会建议采取捕获处理的方式,完全可通过name的判空处理。
     */
    private static void runtimeException(String name) {
    
    
        // 捕获,出现异常也不会导致crash,不影响try catch 块之外的逻辑。
        try {
    
    
            name.length(); //java.lang.NullPointerException
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

(3) アクセス異常情報

catch ブロック内の例外オブジェクトに関する関連情報にアクセスする必要がある場合は、catch ブロックの後の例外パラメーターにアクセスすることで情報を取得できます。JVM が例外オブジェクトを処理するために catch ブロックを呼び出すことを決定すると、catch ブロックの後の例外パラメータに例外オブジェクトが割り当てられます。このとき、このパラメータを使用して例外関連の情報を取得できます。一般的に使用される方法は次のとおりです。

  • getMessage(): 例外の詳細な説明文字列を返します。
  • printStackTrace(): 例外トレーススタック情報を標準エラー出力に出力します。
  • printStackTrace(PrintStream s): 例外トレース スタック情報を指定された出力ストリームに出力します。
  • getStackTrace(): 例外のトレーススタック情報を返します。
注意点

2つ3つアノマリーを経験したように見えますが、実はアノマリー関連はまだまだたくさんあるんですよ~

(1) プログラム コード ブロックが try ブロック内にあるかどうか、または catch ブロック内のコードであっても、コード ブロックの実行時に例外が発生する限り、システムは常に例外オブジェクトを自動的に生成します。プログラムでこのコードの catch ブロックが定義されていない場合、Java ランタイム環境は例外を処理するための catch ブロックを見つけることができず、プログラムは異常終了します。

(2) 例外がトリガーされると、仮想マシンは対応する例外を生成し、catch で定義された例外エントリを上から下まで走査して、一致する例外エントリを見つけます。catch で例外エントリを定義する場合は、例外エントリは展開または無関係のみ可能であるという原則に従う必要があります。そうしないと、コンパイルが失敗します。

(3) Java 7 より前では、各 catch ブロックは 1 種類の例外のみをキャッチできましたが、Java 7 以降では、1 つの catch ブロックで複数の種類の例外をキャッチできるようになりました。複数の例外をキャッチする場合の注意点:

  • 複数の種類の例外をキャッチする場合は、複数の例外の種類を縦線「|」で区切ります。

  • 複数のタイプの例外をキャッチする場合、例外変数には暗黙的な最終変更が含まれるため、プログラムは例外変数を再割り当てできません。

    private static void mutipleException(){
    
    
        // 多异常捕获,如下catch块可以捕获处理2种异常。
        try {
    
    
        
        }catch (ArrayIndexOutOfBoundsException|NumberFormatException e){
    
    
            e = new IllegalArgumentException("") // 编译报错,多异常不能重新赋值。
        }
        
        // 单个异常捕获
        try {
    
    
            
        }catch (Exception e){
    
    
            e = new IllegalArgumentException(""); // 编译通过,单个异常可以赋值。
        }
    }

(4) 通常の状況では、finally ブロック内でメソッドを終了させる return 文や throw 文などを使用しないでください。finally ブロック内で return 文や throw 文を使用すると、finally ブロック内で return 文や throw 文が発生します。 try ブロックと catch ブロック。ステートメントは無効です。

(5) try および catch の return ステートメント、例外トリガー、およびメソッドの終了を引き起こすその他のケースは、finally コード ブロックの実行には影響しません。


    public static void main(String[] args) {
    
    
        System.out.println("test return value:"+test());
    }
    
    private static int test() {
    
    
        try {
    
    
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
    
    
            return 0;
        } finally {
    
    
            System.out.println("finally");
        }
        System.out.println("test finish");
        return 1;
    }
log:

finally
test return value:0

では、なぜメソッドによって最終的に戻り値が 1 ではなく 0 として出力されるのでしょうか? 実際の工程はこんな感じですよ~

まず、try ブロックが ArithmeticException をトリガーするまでコードが実行され、その後 catch ブロックがそれをキャプチャして処理します。ただし、例外メカニズムにはそのような原則があります。catch でリターンまたは例外が発生した場合、関数は終了する可能性があります。 、その後、finally コードが最初に実行される必要があり、ブロック内のコードは catch の throw または return ポイントに戻ります。最後に、catch return ステートメント メソッドが実行されて終了します。後続のコードは実行されなくなります。

コードを変更して検証することもできます。次の catch コード ブロックが実行された後、try catchfinally~ 以外のコードが引き続き実行されます。

    private static int test() {
    
    
        try {
    
    
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
    
    
            System.out.println("catch");
        } finally {
    
    
            System.out.println("finally");
        }
        System.out.println("test finish");
        return 1;
    }
    
log:

catch
finally
test finish
test return value:1   

栗を与えてより統合し、徹底的に理解しましょうえむ〜次のメソッドの戻り値は何ですか?

    private static int test() {
    
    
        try {
    
    
            int a = 10 / 0; //  ArithmeticException: / by zero
            return 1;
        } catch (Exception e) {
    
    
            return 2;
        } finally {
    
        
            return 3;
        }
    }

try の int a までコードを実行すると ArithmeticException が発生しますが、このとき例外ハンドラでキャッチされ、catch で 2 が返されます。Javaの例外実行メカニズムによるものこのとき、finally の return3 が最初に実行されます。最後に、ここで return ステートメントが偶然出現し、メソッドは正常に終了します。

最終的にリソース終了コードのみを処理し、ここに戻り 3 がない場合、このメソッドの戻り値は 2~ になります。

(6) try ブロックまたは catch ブロック内で仮想マシンを終了するメソッドが呼び出されていない限り、try ブロックまたは catch ブロック内でどのようなコードが実行され、どのような状況が発生しても、常に例外処理のfinally ブロックが実行されます。

    private static int test() {
    
    
        try {
    
    
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
    
    
            System.out.println("catch");
            System.exit(0);
        } finally {
    
    
            System.out.println("finally");
        }
        return 0;
    }
log:

catch
Process finished with exit code 0

上と同様に、最初に ArithmeticException 例外がトリガーされ、この時点で catch コード ブロックに到達し、print ステートメントが実行された後、System.exit(0) が実行されて JVM を直接終了します。

(7) try catch 最終的に実行メカニズムが存在する異常損失場合

    /**
     * try中捕获异常A,catch中又触发异常B,这时finally执行完后系统只会抛出异常B。
     * 这种case也可以看做try catch的弊端,丢失了try中的异常。
     * */
    private static void test() {
    
    
        try {
    
    
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
    
    
            String a = null;
            a.length(); // finally 执行完毕后这里最终由系统抛出NullPointerException
        } finally {
    
    
            System.out.println("finally");
        }
    }
    /**
     * try中捕获异常A,catch中又触发异常B,这时finally执行又触发异常C系统只会抛出异常C。
     * 这种case也可以看做try catch的弊端,丢失了try,catch中的异常。
     */
    private static void test() {
    
    
        try {
    
    
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
    
    
            String a = null;
            a.length(); // NullPointerException
        } finally {
    
    
           Integer.parseInt("aaa"); //代码执行到这里只会抛出NumberFormatException。上述两异常忽略。
           System.out.println("finally");
        }
    }

Java 7 の抑制された例外と構文シュガー

try catch の例外が失われることは以前に学びましたが、この問題を解決するために Java7 では抑制された例外が導入されました。この新機能により、開発者は 1 つの例外を別の例外に付加することができます。したがって、スローされる例外には複数の例外情報が伴う可能性があります。

Java 7 は、バイトコード レベルで抑制された例外を自動的に使用するために、try-with-resources と呼ばれる糖衣構文を特別に構築しました。もちろん、この構文シュガーの主な目的は、抑制された例外を使用することではなく、リソースのオープンとクローズを効率化することです。Java 7 より前では、開いているリソースの場合、通常または異常な実行条件下でリソースを確実に閉じることができるように、finally コード ブロックを定義する必要がありました。このアプローチではコードが肥大化しすぎます~

Java 7 の try-with-resources 構文のシュガーにより、try catchfinally コードが大幅に簡素化されます。プログラムは、try キーワードの後に​​ AutoCloseable インターフェイスを実装するクラスを宣言してインスタンス化することができ、コンパイラは対応する close 操作を自動的に追加します。複数の AutoCloseable インスタンスを宣言する場合、コンパイルされたバイトコードは、try catchfinally の手書きコードのコンパイル結果と似ています。手動コードと比較して、try-with-resources では、元の例外が「消える」ことを防ぐために、抑制された例外関数も使用されます。

(1) リソースを自動的にクローズする

システムは、AutoCloseable インターフェースを実装するいくつかのクラスを提供します。try-with-resources 構文のシュガーを直接使用すると、面倒なクローズ作業を行うために、finally を使用する必要がなくなりました。

    public static void main(String[] args) throws Exception {
    
    

        /**
         * 1、try()中进行变量定义(创建、赋值),类必须实现了AutoCloseable接口(或者是AutoCloseable实现类)。
         * 2、try后的代码块中可进行逻辑的操作。
         * 3、自动关闭资源的try语句相当于包含了隐式的finally块,执行了close回调,因此这个try语句可以既没有catch块,
         *    也没有finally块。
         * 4、注意AutoCloseable#close()方法抛出了Exception
         * */
        try (
                BufferedReader br = new BufferedReader(new FileReader("F://a.txt"));
                PrintStream pr = new PrintStream(new FileOutputStream("F://b.txt"))
        ) {
    
    
            br.readLine();
            pr.write("emmm".getBytes());
        }
    }

BufferedReader と PrintStream はどちらも AutoCloseable インターフェイスを間接的に実装しているため、try ステートメントで宣言して初期化すると、try ステートメントによって自動的に閉じられます。もちろん、クラスをカスタマイズしてインターフェイスを実装し、インターフェイスでリソース処理を実装することもできます。次に、例外キャプチャを確認します~

(2) 異常損失を回避する

/**
 * Create by SunnyDay on 2022/04/22 17:37
 */
public class Demo implements AutoCloseable {
    
    

    private String desc;

    public Demo(String name) {
    
    
        this.desc = name;
    }

    public static void main(String[] args) throws Exception {
    
    
        try (
                Demo demo1 = new Demo("1");
                Demo demo2 = new Demo("2")) {
    
    

                int a = 10/0; // 执行代码 触发异常
        }
    }


    @Override
    public void close() throws Exception {
    
    
        // 这里直接抛出一个异常,验证 finally中触发了异常工作。
        throw new IllegalArgumentException();
    }
}

log: 打印所有的异常信息

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Demo.main(Demo.java:21)
	Suppressed: java.lang.IllegalArgumentException
		at Demo.close(Demo.java:29)
		at Demo.main(Demo.java:22)
	Suppressed: java.lang.IllegalArgumentException
		at Demo.close(Demo.java:29)
		at Demo.main(Demo.java:22)

例外実施原則

例外インスタンスの構築にはコストがかかります。これは、例外インスタンスを構築するときに、Java 仮想マシンが例外のスタック トレースを生成する必要があるためです。この操作は、現在のスレッドの Java スタック フレームに 1 つずつアクセスし、スタック フレームが指すメソッドの名前、メソッドのクラス名とファイル名、コード内の行などのさまざまなデバッグ情報を記録します。例外がトリガーされた場所。

スタック トレースを生成するとき、Java 仮想マシンはスタック フレームを満たす例外コンストラクターと Java メソッドを無視し、新しい例外の場所から直接カウントを開始します。さらに、Java 仮想マシンは、非表示としてマークされている Java メソッド スタック フレームを無視します。

例外インスタンスの構築には非常にコストがかかるため、例外インスタンスをキャッシュし、必要なときに直接スローできるでしょうか?

文法の観点からは、これは許可されています。ただし、この例外に対応するスタック トレースは、throw ステートメントの場所ではなく、新しい例外の場所です。したがって、このアプローチは開発者を誤解させて、間違った場所をターゲットにする可能性があります。実際には、新しい例外インスタンスをスローすることを選択することが多いのはこのためです。

jvm はどのように例外を実装しますか?

クラス ファイルがバイトコードにコンパイルされると、各メソッドには例外テーブルが伴います。各例外テーブルエントリ代表する例外ハンドラープロセッサは、from ポインター、to ポインター、ターゲット ポインター、およびキャプチャされた例外タイプで構成されます。これらのポインターの値は、バイトコードを見つけるために使用されるバイトコード インデックスです。

  • from と to は例外ハンドラの監視範囲、つまり try コードブロックで監視される範囲を表します。
  • target は例外ハンドラの開始位置、つまり catch の開始位置を表します。
  • 例外の種類は xxxException です。
/**
 * Create by SunnyDay on 2022/04/22 18:45
 */
public class Test {
    
    
    public static void main(String[] args) {
    
    
        // 异常条目1(try catch finally块就是一个异常处理器)
        try {
    
    // from
            File file = new File("F://a.txt");
            if (!file.exists()) {
    
    
                file.createNewFile();
            }
        } catch (IOException e) {
    
    //to(不包括to 可以这样记住范围“包左不包右”也即[from,to))。  target,这里也是异常处理器
                                // 开始位置
            e.printStackTrace();
        } finally {
    
    
            System.out.println("finally1");
        }

        // 异常条目2
        try {
    
    // from
             int a =  1/0;
        } catch (Exception e) {
    
    // to ,target
            e.printStackTrace();
        } finally {
    
    
            System.out.println("finally2");
        }

    }
}
//javap 命令 查看class文件:javap -c -l Test.class  main方法中生成的异常表如下:

    Exception table:
       from    to  target type
           0    22    33   Class java/io/IOException // 异常条目1
           0    22    49   any
          33    38    49   any
          60    64    75   Class java/lang/Exception // 异常条目2
          60    64    91   any
          75    80    91   any

プログラムが例外をトリガーすると、Java 仮想マシンはスローされる例外インスタンスを生成し、例外テーブル内のすべてのエントリを上から下まで走査します。例外をトリガーしたバイトコードのインデックス値が例外テーブル エントリの監視範囲内にある場合、Java 仮想マシンはスローされる例外がエントリがキャッチしようとしている例外と一致するかどうかを判断します。一致するものがあれば、Java 仮想マシンは、エントリのターゲット ポインタが指すバイトコードに制御フローを転送します。

すべての例外テーブル エントリを調べても、Java 仮想マシンが依然として例外ハンドラと一致しない場合、現在のメソッドに対応する Java スタック フレームをポップアップし、呼び出し側で上記の操作を繰り返します。最悪の場合、Java 仮想マシンは、現在のスレッドの Java スタック上のすべてのメソッドの例外テーブルを走査する必要があります。最後に例外をスローします。

finally コード ブロックのコンパイルはさらに複雑で、現在のバージョンの Java コンパイラは、finally コード ブロックの内容をコピーし、try-catch コード ブロックのすべての通常実行パスと異常実行パスの出口に配置します。

ここに画像の説明を挿入します
例外実行パスの場合、Java コンパイラは 1 つ以上の例外テーブル エントリを生成し、try-catch コード ブロック全体を監視して、あらゆる種類の例外をキャッチします。これらの例外テーブル エントリのターゲット ポインタは、finally コード ブロックの別のコピーを指します。そして、この Finally コード ブロックの最後で、Java コンパイラはキャッチした例外を再スローします。

catch ブロックが例外をキャッチし、別の例外をトリガーした場合、最終的にどの例外がキャッチされ、再スローされますか? 答えは後者です。これは、元の例外が無視されることを意味し、コードのデバッグに非常に悪影響を及ぼします。

キャッチされない例外ハンドラ

導入

Java では、スレッドが明示的に処理せずに例外をスローした場合、JVM は処理対象のスレッド オブジェクトの UncaughtExceptionHandler に例外イベントを報告します。スレッドが UncaughtExceptionHandler を設定していない場合は、例外スタック情報がターミナルに出力されます。デフォルトでは、プログラムは直接クラッシュします。したがって、スレッドが予期せずクラッシュしたときに何らかの処理を実行したい場合は、UncaughtExceptionHandler を実装することでニーズを満たすことができます。

/**
 * Create by SunnyDay on 2022/04/24 11:07
 */
public class CrashHandler implements Thread.UncaughtExceptionHandler {
    
    

    private volatile static CrashHandler INSTANCE;

    private CrashHandler() {
    
    
    }

    public static CrashHandler getINSTANCE() {
    
    
        if (INSTANCE == null) {
    
    
            synchronized (CrashHandler.class) {
    
    
                if (INSTANCE == null) {
    
    
                    INSTANCE = new CrashHandler();
                }
            }
        }
        return INSTANCE;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
    
    
        printInfo(t, e);
        collectDeviceInfo();
        saveCatchInfo2File(e);
    }

    private void printInfo(Thread t, Throwable e) {
    
    
        System.out.println("异常线程:" + t.getName() + " 异常信息:" + e.getMessage());
    }

    private void collectDeviceInfo() {
    
    
        System.out.println("收集用户设备信息");
    }

    private void saveCatchInfo2File(Throwable ex) {
    
    
        System.out.println("异常信息保存到文件");
    }
}


/**
 * Create by SunnyDay on 2022/04/24 11:06
 */
public class Test {
    
    
    public static void main(String[] args) {
    
    
        getException();
        new Thread(() -> createAnException1(), "工作线程1").start();
        new Thread(() -> createAnException2(), "工作线程2").start();
    }

    /**
     * 捕获所有线程未捕获异常。
     */
    private static void getException() {
    
    
        Thread.setDefaultUncaughtExceptionHandler(CrashHandler.getINSTANCE());
    }

    /**
     * 模拟一个异常 "除0异常"
     */
    private static void createAnException1() {
    
    
        int a = 10 / 0;
    }

    /**
     * 模拟一个异常 "NumberFormatException"
     */
    private static void createAnException2() {
    
    
        Integer.parseInt("sss");
    }
}

上記の Test#main メソッドを実行すると、最終的に CrashHandler#uncaughtException ログが次のように表示されます。

异常线程:工作线程1异常信息:/ by zero
收集用户设备信息
异常信息保存到文件

异常线程:工作线程2异常信息:For input string: "sss"
收集用户设备信息
异常信息保存到文件
ソースコード分析

上記の例では Thread.setDefaultUncaughtExceptionHandler() が使用されていますが、実際、Thread にも setUncaughtExceptionHandler メソッドがあります。まず結論を出してからソースコードを見てみましょう~

  • Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh): デフォルトのグローバル例外ハンドラーを設定するために使用されます。つまり、このハンドラーをすべてのスレッドに設定します。
  • Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler eh): 特定のスレッドのキャッチされなかった例外を処理するために、指定されたスレッドに設定します。
class Thread implements Runnable {
    
    
...
    /* The group of this thread */
    private ThreadGroup group;
    
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
    
    
        void uncaughtException(Thread t, Throwable e);
    }
    
    // 单独线程的UncaughtExceptionHandler 对象
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
    // 全局的UncaughtExceptionHandler 对象
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    //设置全局的UncaughtExceptionHandler 对象
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
    
    
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
    
    
            sm.checkPermission(
                new RuntimePermission("setDefaultUncaughtExceptionHandler")
                    );
        }

         defaultUncaughtExceptionHandler = eh;
     }

    public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
    
    
        return defaultUncaughtExceptionHandler;
    }

    //为单独线程指定UncaughtExceptionHandler 对象
    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
    
    
        checkAccess();
        uncaughtExceptionHandler = eh;
    }

    /**
      uncaughtExceptionHandler对象为null则返回group。ThreadGroup 类对象在Thread构造中初始化。

      这里不妨猜测下ThreadGroup必定实现了UncaughtExceptionHandler接口。
     */
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
    
    
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
...
}

この時点で、setDefaultUncaughtExceptionHandler と setUncaughtExceptionHandler の違いが理解できました。スレッドがクラッシュしたときに例外がスローされる順序は、最初に Thread の getUncaughtExceptionHandler を呼び出して、UncaughtExceptionHandler オブジェクトに値があるかどうかを確認することです。値がある場合は、直接処理します。ない場合は、ThreadGroup の論理処理を呼び出します~

ThreadGroup は、JDK の UncaughtExceptionHandler のデフォルト実装クラスです。スレッドの getDefaultUncaughtExceptionHandler() が内部で呼び出され、処理用のハンドラーが取得されますが、デフォルトのハンドラーで処理できない場合は、通常の例外処理が直接実行され、プログラムがクラッシュします~

//这个类实现了UncaughtExceptionHandler 接口。
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    
    

    private final ThreadGroup parent;
···
    public void uncaughtException(Thread t, Throwable e) {
    
    
        if (parent != null) {
    
    //ThreadGroup 无参构造中直接parent == null。这个值一般为null。
            parent.uncaughtException(t, e);
        } else {
    
    
        /**
           ThreadGroup 其实也是调用 Thread.getDefaultUncaughtExceptionHandler()来获取UncaughtExceptionHandler 对象的。

            当我们通过setDefaultUncaughtExceptionHandler设置过UncaughtExceptionHandler对象时则调用
            UncaughtExceptionHandler#uncaughtException
            当未设置过UncaughtExceptionHandler对象时打印异常信息,后续就是jvm的crash了~
         */
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
    
    
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
    
    
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
···
}

終わり

参考: Java 仮想マシンの詳細な解体

参考:UncaughtExceptionHandler関連の問題の分析

おすすめ

転載: blog.csdn.net/qq_38350635/article/details/124325091