1. 概要

例外は、アプリケーションの通常のフローからエラー処理コードを分離します。オブジェクトのインスタンス化中に例外をスローすることは珍しくありません。

この記事では、コンストラクターで例外をスローすることに関するすべての詳細を調べます。

2. コンストラクターで例外をスローする

コンストラクターは、オブジェクトを作成するために呼び出される特殊なタイプのメソッドです。 次のセクションでは、例外をスローする方法、スローする例外、およびコンストラクターで例外をスローする理由について説明します。

2.1. どのように?

コンストラクターで例外をスローすることは、他のメソッドでスローすることと同じです。 引数なしのコンストラクターを使用してAnimalクラスを作成することから始めましょう。

public Animal() throws InstantiationException {
    throw new InstantiationException("Cannot be instantiated");
}

ここでは、 InstantiationException をスローしています。これは、チェック済みの例外です。

2.2. どれ?

あらゆるタイプの例外をスローすることは許可されていますが、いくつかのベストプラクティスを確立しましょう。

まず、「java.lang.Exception」をスローしたくありません。 これは、呼び出し元がどのような種類の例外を識別して処理できない可能性があるためです。

次に、呼び出し元が強制的に処理する必要がある場合は、チェックされた例外をスローする必要があります。

第3に、呼び出し元が例外から回復できない場合は、チェックされていない例外をスローする必要があります。

これらのプラクティスは、メソッドとコンストラクターの両方に等しく適用できることに注意することが重要です。

2.3. なんで?

このセクションでは、コンストラクターで例外をスローする理由を理解しましょう。

引数の検証は、コンストラクターで例外をスローするための一般的なユースケースです。コンストラクターは、主に変数の値を割り当てるために使用されます。 コンストラクターに渡された引数が無効な場合、例外をスローできます。 簡単な例を考えてみましょう。

public Animal(String id, int age) {
    if (id == null)
        throw new NullPointerException("Id cannot be null");
    if (age < 0)
        throw new IllegalArgumentException("Age cannot be negative");
}

上記の例では、オブジェクトを初期化する前に引数の検証を実行しています。 これは、有効なオブジェクトのみを作成していることを確認するのに役立ちます。

ここで、Animalオブジェクトに渡されるidnullの場合、null以外の引数に対してNullPointerExceptionをスローできます。 age の負の値など、無効な場合は、IllegalArgumentExceptionをスローできます。

セキュリティチェックは、コンストラクターで例外をスローするためのもう1つの一般的なユースケースです。一部のオブジェクトは、作成中にセキュリティチェックが必要です。 コンストラクターが安全でない、または機密性の高い操作を実行した場合、例外をスローできます。

Animalクラスがユーザー入力ファイルから属性をロードしていると考えてみましょう。

public Animal(File file) throws SecurityException, IOException {
    if (file.isAbsolute()) {
        throw new SecurityException("Traversal attempt");
    }
    if (!file.getCanonicalPath()
        .equals(file.getAbsolutePath())) {
        throw new SecurityException("Traversal attempt");
    }
}

上記の例では、パストラバーサル攻撃を防止しました。 これは、絶対パスとディレクトリトラバーサルを許可しないことによって実現されます。 たとえば、ファイル「a/../b.txt」について考えてみます。 ここでは、正規パスと絶対パスが異なり、ディレクトリトラバーサル攻撃の可能性があります。

3. コンストラクターで継承された例外

それでは、コンストラクターでのスーパークラス例外の処理について説明しましょう。

Animalクラスを拡張する子クラスBirdを作成しましょう。

public class Bird extends Animal {
    public Bird() throws ReflectiveOperationException {
        super();
    }
    public Bird(String id, int age) {
        super(id, age);
    }
}

super()はコンストラクターの最初の行である必要があるため、 try-catch ブロックを挿入して、スーパークラスによってスローされたチェック済み例外を処理することはできません。

親クラスAnimalはチェックされた例外InstantiationExceptionをスローするため、Birdコンストラクターで例外を処理することはできません。 代わりに、同じ例外またはその親例外を伝播できます。

メソッドのオーバーライドに関する例外処理のルールが異なることに注意することが重要です。 メソッドのオーバーライドでは、スーパークラスメソッドが例外を宣言する場合、サブクラスのオーバーライドされたメソッドは、同じ、サブクラスの例外、または例外なしを宣言できますが、親の例外を宣言することはできません。

一方、チェックされていない例外を宣言する必要はなく、サブクラスコンストラクター内で処理することもできません。

4. セキュリティ上の懸念

コンストラクターで例外をスローすると、オブジェクトが部分的に初期化される可能性があります。 Java Secure CodingGuidelines のガイドライン7.3で説明されているように、非ファイナルクラスの部分的に初期化されたオブジェクトは、ファイナライザー攻撃として知られるセキュリティ上の懸念に陥りがちです。

つまり、ファイナライザー攻撃は、部分的に初期化されたオブジェクトをサブクラス化し、その finalize()メソッドをオーバーライドすることによって引き起こされ、そのサブクラスの新しいインスタンスを作成しようとします。 これにより、サブクラスのコンストラクター内で実行されるセキュリティチェックがバイパスされる可能性があります。

finalize()メソッドをオーバーライドし、 final とマークすると、この攻撃を防ぐことができます。

ただし、 finalize()メソッドはJava 9で非推奨になっているため、このタイプの攻撃は防止されています。

5. 結論

このチュートリアルでは、コンストラクターで例外をスローする方法と、関連する利点およびセキュリティ上の懸念について学習しました。 また、コンストラクターで例外をスローするためのいくつかのベストプラクティスを確認しました。

いつものように、このチュートリアルで使用されているソースコードは、GitHubからで入手できます。