おそらく、コードを実行すると、 NullPointerException ClassCastException あるいはさらに悪いことに何らかのエラーが発生するという状況を経験したことがあると思います。その後、デバッグ、分析、グーグル検索などの長いプロセスが続きます。例外はそのままで素晴らしいものです。例外は問題の性質と問題が発生した場所を示します。記憶をリフレッシュしてもう少し詳しく知りたい場合は、次の記事を参照してください:例外: チェック済み、未チェック、カスタム

ただし、独自の例外を作成する必要がある場合もあります。たとえば、何らかの理由で利用できないリモート サービスからの情報をコードで要求する必要があるとします。あるいは、誰かが銀行カードの申請書に記入し、偶然かどうかに関係なく、すでにシステム内の別のユーザーに関連付けられている電話番号を提供したとします。

もちろん、ここでの正しい動作は顧客の要件とシステムのアーキテクチャによって異なりますが、電話番号がすでに使用されているかどうかを確認し、使用されている場合は例外をスローするという任務を負っていると仮定します。

例外を作成しましょう。


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

次に、チェックを実行するときにそれを使用します。


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

例を単純化するために、いくつかのハードコードされた電話番号を使用してデータベースを表します。最後に、例外を使用してみましょう。


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

次に、Shift+F10を押します(IDEA を使用している場合)。つまり、プロジェクトを実行します。コンソールには次のように表示されます。

例外.CreditCardIssue
例外.PhoneNumberAlreadyExistsException: 指定された電話番号はすでに別の顧客によって使用されています。
例外.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11) で

あなたを見て!独自の例外を作成し、少しテストしました。この達成おめでとうございます! コードがどのように機能するかをよりよく理解するために、コードを少し試してみることをお勧めします。

別のチェックを追加します。たとえば、電話番号に文字が含まれているかどうかをチェックします。ご存知かと思いますが、米国では電話番号を覚えやすくするために文字がよく使用されます (例: 1-800-MY-APPLE)。チェックにより、電話番号に数字のみが含まれていることを確認できます。

さて、チェック例外を作成しました。すべて問題なく、良いことでしょうが、...

プログラミング コミュニティは、チェック例外を支持する派と反対する派の 2 つの陣営に分かれています。双方とも強い主張を展開している。どちらにも一流の開発者が含まれており、Bruce Eckel はチェック例外を批判し、James Gosling はチェック例外を擁護しています。この問題は永久に解決されないようだ。そうは言っても、チェック例外を使用する主な欠点を見てみましょう。

チェック例外の主な欠点は、チェック例外を処理する必要があることです。ここでは 2 つのオプションがあります。try-catch を使用してその場で処理するか、同じ例外を多くの場所で使用する場合はthrowsを使用して例外をスローし、最上位クラスで処理するかのいずれかです。

また、最終的に「ボイラープレート」コード、つまり、多くのスペースを占有するが、それほど面倒な作業は行わないコードが作成される可能性があります。

問題は、多くの例外が処理されるかなり大規模なアプリケーションで発生します。トップレベルのメソッドのスローリストは簡単に 12 個の例外を含むように大きくなる可能性があります。

public OurCoolClass() は FirstException、SecondException、ThirdException、ApplicationNameException をスローします。

開発者は通常これを好まず、代わりにトリックを選択します。すべてのチェック例外が共通の祖先であるApplicationNameExceptionを継承するようにします。ここで、ハンドラーでその (チェック済み!) 例外もキャッチする必要があります。


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

ここで別の問題に直面します。最後のcatchブロックでは何をすべきでしょうか? 上記で、予期されるすべての状況はすでに処理されているため、この時点でApplicationNameException は「例外: 理解できないエラーが発生しました」ということ以上の意味はありません。これを次のように処理します。


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

そして結局、何が起こったのかは分かりません。

しかし、このようにすべての例外を一度にスローすることはできないでしょうか?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

はい、できます。しかし、「例外をスローする」ということは何を意味するのでしょうか? 何かが壊れているということ。理由を理解するには、すべてを上から下まで調査し、長時間デバッガーを使いこなす必要があります。

「例外飲み込み」と呼ばれることもある構造に遭遇することもあります。


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

ここで説明のために追加することはあまりありません。コードによってすべてが明確になります。むしろ、コードによってすべてが不明瞭になります。

もちろん、これは実際のコードでは見られないと言うかもしれません。さて、 java.netパッケージのURLクラスの中身 (コード) を覗いてみましょう。知りたい方はフォローしてください!

URLクラスの構成要素の 1 つを次に示します。


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

ご覧のとおり、興味深いチェック例外MalformedURLExceptionがあります。これがスローされる可能性がある場合は次のとおりです (引用します):
「プロトコルが指定されていない場合、不明なプロトコルが見つかった場合、仕様が null の場合、または解析された URL が関連付けられたプロトコルの特定の構文に準拠していない場合。」

あれは:

  1. プロトコルが指定されていない場合。
  2. 不明なプロトコルが見つかりました。
  3. 仕様はnullです。
  4. URL は、関連するプロトコルの特定の構文に準拠していません。

URLオブジェクトを作成するメソッドを作成しましょう。


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

IDE でこれらの行を記述すると (私は IDEA でコーディングしていますが、これは Eclipse や NetBeans でも機能します)、次のように表示されます。

これは、例外をスローするか、コードをtry-catchブロックでラップする必要があることを意味します。現時点では、何が起こっているかを視覚化するために 2 番目のオプションを選択することをお勧めします。


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

ご覧のとおり、コードはすでにかなり冗長になっています。そしてそれについては上で触れました。これは、未チェック例外を使用する最も明白な理由の 1 つです。

Java でRuntimeExceptionを拡張することで、未チェック例外を作成できます。

未チェック例外は、 ErrorクラスまたはRuntimeExceptionクラスから継承されます。多くのプログラマーは、これらの例外はプログラムの実行中に回復が期待できないエラーを表すため、プログラムで処理できると考えています。

未チェック例外が発生する場合、通常はコードの使い方が間違っていること、null または無効な引数を渡していることが原因です。

さて、コードを書いてみましょう:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

さまざまな目的のために複数のコンストラクターを作成したことに注意してください。これにより、例外にさらに多くの機能を与えることができます。たとえば、例外が発生したときにエラー コードが表示されるようにすることができます。まず、エラー コードを表す列挙型を作成しましょう。


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

次に、例外クラスに別のコンストラクターを追加しましょう。


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

そして、フィールドを追加することを忘れないでください (ほとんど忘れていました)。


private Integer errorCode;
    

そしてもちろん、このコードを取得するメソッドは次のとおりです。


public Integer getErrorCode() {
   return errorCode;
}
    

クラス全体を確認して比較できるようにしてみましょう。

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

タダ!例外は完了しました。ご覧のとおり、ここでは特に複雑なことは何もありません。実際に動作を確認してみましょう:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

小さなアプリケーションを実行すると、コンソールに次のようなものが表示されます。

次に、追加した追加機能を活用してみましょう。前のコードに少し追加します。


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

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

オブジェクトを操作するのと同じ方法で例外を操作できます。もちろん、Java のすべてがオブジェクトであることはすでにご存知だと思います。

そして、私たちがやったことを見てください。まず、メソッドを変更しました。メソッドはスローされず、入力パラメーターに応じて単純に例外を作成するようになりました。次に、switch-caseステートメントを使用して、目的のエラー コードとメッセージを含む例外を生成します。そしてメインメソッドでは、作成された例外を取得し、エラーコードを取得してスローします。

これを実行して、コンソールに何が表示されるかを確認してみましょう。

見てください。例外から取得したエラー コードを出力してから、例外自体をスローしました。さらに、例外がスローされた場所を正確に追跡することもできます。必要に応じて、すべての関連情報をメッセージに追加したり、追加のエラー コードを作成したり、例外に新しい機能を追加したりできます。

さて、それについてどう思いますか?すべてうまくいったことを願っています!

一般に、例外はかなり広範囲にわたるトピックであり、明確なものではありません。それをめぐってはさらに多くの論争が起こるだろう。たとえば、例外をチェックできるのは Java だけです。最も人気のある言語の中で、それらを使用している言語を見たことがありません。

Bruce Eckel は著書『Thinking in Java』の第 12 章で例外について非常に詳しく書いています。ぜひ読んでください。Horstmann の「Core Java」の第 1 巻もご覧ください。第 7 章にも興味深い内容がたくさんあります。

簡単な要約

  1. すべてをログに書き込みます。スローされた例外にメッセージを記録します。これは通常、デバッグに非常に役立ち、何が起こったのかを理解できるようになります。catchブロックを空のままにしないでください。空にしないと、例外が単に「飲み込まれる」だけになり、問題を突き止めるのに役立つ情報が得られなくなります。

  2. 例外に関して言えば、すべてを一度にキャッチするのは悪い習慣です (私の同僚が言ったように、「それはポケモンではありません。Java です」)。そのため、 catch (Exception e) 、または最悪の場合は catch (Throwable t) を避けください

  3. できるだけ早く例外をスローします。これは Java プログラミングの良い習慣です。Spring のようなフレームワークを研究すると、それらが「フェイルファスト」原則に従っていることがわかります。つまり、エラーを迅速に発見できるようにするために、できるだけ早く「失敗」します。もちろん、これには一定の不都合が伴います。ただし、このアプローチは、より堅牢なコードを作成するのに役立ちます。

  4. コードの他の部分を呼び出すときは、特定の例外をキャッチすることが最善です。呼び出されたコードが複数の例外をスローする場合、それらの例外の親クラスのみをキャッチするのは不適切なプログラミング方法です。たとえば、FileNotFoundExceptionIOExceptionをスローするコードを呼び出すとします。このモジュールを呼び出すコードでは、 Exception をキャッチする1 つのcatchではなく、各例外をキャッチする 2 つの catch ブロックを作成することをお勧めします。

  5. ユーザーやデバッグのために効果的に例外を処理できる場合にのみ、例外をキャッチします。

  6. 独自の例外を遠慮なく作成してください。もちろん、Java にはあらゆる場面に対応する既製のものがたくさんありますが、それでも独自の「車輪」を発明する必要がある場合もあります。ただし、これを行う理由を明確に理解し、標準の例外セットに必要なものがまだ含まれていないことを確認する必要があります。

  7. 独自の例外クラスを作成するときは、名前に注意してください。おそらく、クラス、変数、メソッド、パッケージに正しく名前を付けることが非常に重要であることはすでにご存知でしょう。例外も例外ではありません!:) 常にExceptionという単語で終わり、例外の名前はそれが表すエラーの種類を明確に伝える必要があります。たとえば、FileNotFoundException

  8. 例外を文書化します。例外用に @throws Javadoc タグを記述することをお勧めします。これは、コードがあらゆる種類のインターフェイスを提供する場合に特に役立ちます。また、後で自分のコードを理解するのも容易になるでしょう。MalformedURLExceptionの内容をどのように判断できるでしょうか? Javadocから!確かに、ドキュメントを書くという考えはあまり魅力的ではありませんが、信じてください、6 か月後に自分のコードに戻ったとき、自分自身に感謝するでしょう。

  9. リソースを解放し、 try-with-resources構造を無視しないでください。

  10. 全体的な概要は次のとおりです。例外は賢明に使用してください。例外のスローは、リソースの点でかなり「高価な」操作です。多くの場合、例外のスローを回避し、代わりに、単純で「低コスト」の if- elseを使用して、操作が成功したかどうかを示すブール変数を返す方が簡単な場合があります。

    また、アプリケーション ロジックを例外に結び付けたくなるかもしれませんが、これは明らかにすべきではありません。記事の冒頭で述べたように、例外は予期されない例外的な状況に対するものであり、それを防ぐためのさまざまなツールがあります。特に、NullPointerExceptionを防ぐためのOptionalや、read()メソッドがスローする可能性のあるIOExceptionを防ぐためのScanner.hasNextなどがあります。