web-dev-qa-db-ja.com

コンストラクターに例外をスローさせるのは良い習慣ですか?

コンストラクターに例外をスローさせるのは良い習慣ですか?たとえば、クラスPersonがあり、その唯一の属性としてageがあります。今、私はクラスを提供します

class Person{
  int age;
  Person(int age) throws Exception{
   if (age<0)
       throw new Exception("invalid age");
   this.age = age;
  }

  public void setAge(int age) throws Exception{
  if (age<0)
       throw new Exception("invalid age");
   this.age = age;
  }
}
145
ako

コンストラクターで例外をスローすることは悪い習慣ではありません。実際、コンストラクターが問題があることを示すのはonly合理的な方法です。例えばパラメータが無効であること。

また、checked例外をスローしても問題ないと思います1、チェック済み例外が1)宣言され、2)報告している問題に固有であると仮定します。

ただし、Java.lang.Exceptionを明示的に宣言またはスローすることは、ほとんど常に悪い習慣です。

発生した例外条件に一致する例外クラスを選択する必要があります。 Exceptionをスローすると、呼び出し元がこの例外を、宣言されている可能性のあるその他の例外と区別することは困難です。これによりエラー回復が困難になり、呼び出し元が例外を伝達することを選択した場合、問題はただ広がります。


1-一部の人々は同意しないかもしれませんが、IMOはこのケースと例外をスローするケースの間に違いはありません。標準のチェック済みのアドバイスと未チェックのアドバイスは、両方のケースに等しく適用されます。


誰かがassertを使用して引数をチェックすることを提案しました。これに伴う問題は、assertアサーションのチェックをJVMコマンドライン設定を介してオンおよびオフにできることです。アサーションを使用して内部不変式をチェックすることは問題ありませんが、javadocで指定された引数チェックを実装するためにアサーションを使用することはお勧めできません...アサーションチェックが有効な場合、メソッドは仕様を厳密に実装するだけだからです。

assertの2番目の問題は、アサーションが失敗した場合、AssertionErrorがスローされ、それが悪い考えであるということですErrorおよびそのサブタイプのいずれかをキャッチしようとします。

153
Stephen C

私は常にコンストラクターでチェック例外をスローすることは悪い習慣であるか、少なくとも避けるべきものだと考えてきました。

この理由は、これを実行できないからです。

private SomeObject foo = new SomeObject();

代わりにこれを行う必要があります:

private SomeObject foo;
public MyObject() {
    try {
        foo = new SomeObject()
    } Catch(PointlessCheckedException e) {
       throw new RuntimeException("ahhg",e);
    }
}

私が構築している時点で SomeObject パラメータが何であるかを知っているので、なぜtry catchでラップする必要があるのですか?ああ、あなたは言いますが、動的パラメーターからオブジェクトを構築している場合、それらが有効かどうかわかりません。さて、コンストラクターに渡す前にパラメーターを検証できます。それは良い習慣です。パラメーターが有効かどうかだけが心配な場合は、IllegalArgumentExceptionを使用できます。

したがって、チェック例外をスローする代わりに

public SomeObject(final String param) {
    if (param==null) throw new NullPointerException("please stop");
    if (param.length()==0) throw new IllegalArgumentException("no really, please stop");
}

もちろん、チェック例外をスローするのが妥当な場合もあります

public SomeObject() {
    if (todayIsWednesday) throw new YouKnowYouCannotDoThisOnAWednesday();
}

しかし、それはどのくらいの頻度でしょうか?

26
Richard

別の答えはこちら で述べたように、Javaのガイドライン7-3 セキュアコーディングガイドライン で、非最終のコンストラクタで例外をスローしますクラスは潜在的な攻撃ベクトルを開きます。

ガイドライン7-3/OBJECT-3:非最終クラスの部分的に初期化されたインスタンスに対する防御非最終クラスのコンストラクターが例外をスローすると、攻撃者はそのクラスの部分的に初期化されたインスタンスへのアクセスを試みることができます。コンストラクターが正常に完了するまで、非最終クラスが完全に使用不可のままであることを確認してください。

JDK 6以降、サブクラス化可能なクラスの構築は、Objectコンストラクターが完了する前に例外をスローすることで防止できます。これを行うには、this()またはsuper()の呼び出しで評価される式でチェックを実行します。

    // non-final Java.lang.ClassLoader
    public abstract class ClassLoader {
        protected ClassLoader() {
            this(securityManagerCheck());
        }
        private ClassLoader(Void ignored) {
            // ... continue initialization ...
        }
        private static Void securityManagerCheck() {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkCreateClassLoader();
            }
            return null;
        }
    }

古いリリースとの互換性のために、潜在的なソリューションには初期化されたフラグの使用が含まれます。正常に戻る前に、フラグをコンストラクターの最後の操作として設定します。機密操作へのゲートウェイを提供するすべてのメソッドは、先に進む前にまずフラグを調べる必要があります。

    public abstract class ClassLoader {

        private volatile boolean initialized;

        protected ClassLoader() {
            // permission needed to create ClassLoader
            securityManagerCheck();
            init();

            // Last action of constructor.
            this.initialized = true;
        }
        protected final Class defineClass(...) {
            checkInitialized();

            // regular logic follows
            ...
        }

        private void checkInitialized() {
            if (!initialized) {
                throw new SecurityException(
                    "NonFinal not initialized"
                );
            }
        }
    }

さらに、そのようなクラスのセキュリティに敏感な使用は、初期化フラグの状態をチェックする必要があります。 ClassLoader構築の場合、親クラスローダーが初期化されていることを確認する必要があります。

非ファイナルクラスの部分的に初期化されたインスタンスには、ファイナライザー攻撃を介してアクセスできます。攻撃者は、サブクラスの保護されたfinalizeメソッドをオーバーライドし、そのサブクラスの新しいインスタンスを作成しようとします。この試行は失敗します(上記の例では、ClassLoaderのコンストラクターのSecurityManagerチェックはセキュリティ例外をスローします)が、攻撃者は例外を無視し、仮想マシンが部分的に初期化されたオブジェクトでファイナライズを実行するのを待ちます。それが発生すると、悪意のあるファイナライズメソッドの実装が呼び出され、攻撃者はこれにアクセスでき、ファイナライズされるオブジェクトへの参照を取得できます。オブジェクトは部分的にのみ初期化されますが、攻撃者はそのオブジェクトに対してメソッドを呼び出すことができるため、SecurityManagerチェックを回避できます。初期化されたフラグは、部分的に初期化されたオブジェクトへのアクセスを防止しませんが、そのオブジェクトのメソッドが攻撃者にとって有用なことを行うことを防止します。

初期化されたフラグの使用は、安全ですが面倒です。オブジェクトの初期化が正常に完了するまで、パブリックの非最終クラスのすべてのフィールドに安全な値(nullなど)が含まれていることを確認するだけで、セキュリティに依存しないクラスで適切な代替を表すことができます。

より堅牢で、より冗長なアプローチは、「実装へのポインター」(または「ピンプ」)を使用することです。クラスのコアは、インターフェイスクラス転送メソッド呼び出しで非パブリッククラスに移動します。完全に初期化される前にクラスを使用しようとすると、NullPointerExceptionが発生します。このアプローチは、クローンおよびデシリアライゼーション攻撃に対処するのにも適しています。

    public abstract class ClassLoader {

        private final ClassLoaderImpl impl;

        protected ClassLoader() {
            this.impl = new ClassLoaderImpl();
        }
        protected final Class defineClass(...) {
            return impl.defineClass(...);
        }
    }

    /* pp */ class ClassLoaderImpl {
        /* pp */ ClassLoaderImpl() {
            // permission needed to create ClassLoader
            securityManagerCheck();
            init();
        }

        /* pp */ Class defineClass(...) {
            // regular logic follows
            ...
        }
    }
23
Hazok

チェック済み例外をスローする必要はありません。これはプログラムの制御内のバグであるため、未チェックの例外をスローする必要があります。 Java言語で既に提供されている、IllegalArgumentExceptionIllegalStateException、またはNullPointerExceptionなどの未チェックの例外のいずれかを使用します。

セッターを取り除くこともできます。コンストラクタを介してageを開始する方法をすでに提供しました。インスタンス化したら更新する必要がありますか?そうでない場合は、セッターをスキップします。良いルール、必要以上に物事を公開しないでください。プライベートまたはデフォルトで開始し、finalでデータを保護します。誰もがPersonが適切に構築され、不変であることを知っています。安心して使用できます。

ほとんどの場合、これは本当に必要なものです:

class Person { 

  private final int age;   

  Person(int age) {    

    if (age < 0) 
       throw new IllegalArgumentException("age less than zero: " + age); 

    this.age = age;   
  }

  // setter removed
8
Spam Suppper

これは完全に有効です、私はいつもそれをします。パラメーターチェックの結果である場合は、通常IllegalArguemntExceptionを使用します。

この場合、アサーションはデプロイメントビルドでオフになっており、これを常に停止する必要があるため、アサーションを提案しませんが、アサーションをオンにしてグループがテストをすべて行い、行方不明になる可能性があると思う場合は有効です実行時のパラメータの問題は、実行時のクラッシュを引き起こす可能性が高い例外をスローするよりも許容されます。

また、呼び出し側がアサートをトラップするのはより困難であり、これは簡単です。

おそらく、呼び出し元が驚かないように、メソッドのjavadocsに「スロー」として理由とともにリストしたいでしょう。

7
Bill K

コンストラクターで例外をスローするのは悪い習慣だとは考えていません。クラスが設計されるとき、そのクラスの構造がどうあるべきかを念頭に置いて特定の考えを持っています。他の誰かが別のアイデアを持ち、そのアイデアを実行しようとする場合、エラーに応じてエラーを返し、エラーの内容についてユーザーにフィードバックする必要があります。あなたの場合、あなたは次のようなものを考慮するかもしれません

if (age < 0) throw new NegativeAgeException("The person you attempted " +
                       "to construct must be given a positive age.");

ここで、NegativeAgeExceptionは自分で作成した例外クラスであり、おそらくIndexOutOfBoundsExceptionなどの別の例外を拡張します。

コードのバグを発見しようとしているわけではないので、アサーションも正確に実行する方法ではないようです。ここでは、例外で終了することが絶対に正しいことだと思います。

4
ashays

例外をスローすることは悪い習慣です。コンストラクタを呼び出す人は例外をキャッチする必要があるため、これは悪い習慣です。

コンストラクター(または任意のメソッド)が例外をスローすることをお勧めします。一般に、IllegalArgumentExceptionはチェックされていないため、コンパイラーは強制的にキャッチしません。

呼び出し元にキャッチさせたい場合は、チェック済み例外(RuntimeExceptionではなくExceptionから拡張されたもの)をスローする必要があります。

4
TofuBeer

私はこれをクリーンでないと考えているため、コンストラクタで例外をスローすることはできません。私の意見にはいくつかの理由があります。

  1. リチャードが述べたように、簡単な方法でインスタンスを初期化することはできません。特にテストでは、初期化中にtry-catchで囲むだけでテスト全体のオブジェクトを構築するのは本当に面倒です。

  2. コンストラクターはロジックフリーでなければなりません。常に懸念の分離と単一責任の原則を目指しているため、コンストラクターにロジックをカプセル化する理由はまったくありません。コンストラクターの関心は「オブジェクトの構築」であるため、このアプローチに従う場合、例外処理をカプセル化しないでください。

  3. 悪いデザインの匂いがする。コンストラクターで例外処理を行うことを余儀なくされた場合、最初はクラスでデザイン詐欺があるかどうかを自問しています。場合によっては必要ですが、コンストラクタを可能な限りシンプルにするために、これをビルダーまたはファクトリーに外部委託します。

コンストラクターで例外処理を行う必要がある場合、なぜこのロジックをFactoryのBuilderに外注しないのでしょうか?さらに数行のコードになるかもしれませんが、例外処理のロジックをさらに外部委託することができ、コンストラクターに固執しないため、はるかに堅牢で適切な例外処理を自由に実装できます。論理。また、例外処理を適切に委任する場合、クライアントは構築ロジックについて何も知る必要がありません。

1
Vegaaaa