Javaでのinstanceofの回避


102

「instanceof」操作のチェーンを持つことは「コードのにおい」と見なされます。標準的な答えは「多態性の使用」です。この場合、どうすればよいですか?

基本クラスにはいくつかのサブクラスがあります。それらのどれも私の制御下にありません。同様の状況は、JavaクラスInteger、Double、BigDecimalなどです。

if (obj instanceof Integer) {NumberStuff.handle((Integer)obj);}
else if (obj instanceof BigDecimal) {BigDecimalStuff.handle((BigDecimal)obj);}
else if (obj instanceof Double) {DoubleStuff.handle((Double)obj);}

NumberStuffなどを制御できます。

数行で十分なコード行を使いたくありません。(時々、HashMapマッピングInteger.classをIntegerStuffのインスタンスに、BigDecimal.classをBigDecimalStuffのインスタンスに、などを作成します。しかし、今日はもっと単純なものが必要です。)

私はこれと同じくらい簡単なものが欲しい:

public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }

しかし、Javaはそのようには機能しません。

フォーマット時に静的メソッドを使用したいのですが。私がフォーマットしているものは複合であり、Thing1にはThing2の配列を含めることができ、Thing2にはThing1の配列を含めることができます。次のようにフォーマッタを実装すると問題が発生しました。

class Thing1Formatter {
  private static Thing2Formatter thing2Formatter = new Thing2Formatter();
  public format(Thing thing) {
      thing2Formatter.format(thing.innerThing2);
  }
}
class Thing2Formatter {
  private static Thing1Formatter thing1Formatter = new Thing1Formatter();
  public format(Thing2 thing) {
      thing1Formatter.format(thing.innerThing1);
  }
}

はい、私はHashMapを知っています。もう少しコードで修正することもできます。しかし、「instanceof」は、比較すると非常に読みやすく、保守しやすいようです。シンプルだが臭くないものはありますか?

2010年5月10日に追加されたメモ:

新しいサブクラスはおそらく将来追加される予定であり、既存のコードはそれらを適切に処理する必要があることがわかります。その場合、クラスが見つからないため、クラスのHashMapは機能しません。結局のところ、最も具体的で始まり、最も一般的なもので終わるifステートメントのチェーンがおそらく最良です。

if (obj instanceof SubClass1) {
    // Handle all the methods and properties of SubClass1
} else if (obj instanceof SubClass2) {
    // Handle all the methods and properties of SubClass2
} else if (obj instanceof Interface3) {
    // Unknown class but it implements Interface3
    // so handle those methods and properties
} else if (obj instanceof Interface4) {
    // likewise.  May want to also handle case of
    // object that implements both interfaces.
} else {
    // New (unknown) subclass; do what I can with the base class
}

4
[訪問者パターン] [1]をお勧めします。[1]:en.wikipedia.org/wiki/Visitor_pattern
10

25
Visitorパターンでは、ターゲットクラス(たとえばInteger)にメソッドを追加する必要があります-JavaScriptでは簡単、Javaでは難しいです。ターゲットクラスを設計する際の優れたパターン。古いクラスに新しいトリックを教えようとするときはそれほど簡単ではありません。
Mark Lutton、2010年

4
@lexicore:コメントのマークダウンは制限されています。[text](link)コメントにリンクを投稿するために使用します。
BalusC 2010年

2
「しかし、Javaはそのようには機能しません。」多分私は誤解しているかもしれませんが、Javaは(静的メソッドであっても)メソッドのオーバーロードをうまくサポートしています...上記のメソッドに戻り値の型が欠けているだけです。
Powerlord

4
@Powerlordオーバーロードの解決は、コンパイル時に静的です。
Aleksandr Dubinsky、2015年

回答:


55

Steve YeggeのAmazonブログのこの記事「多態性が失敗したとき」に興味があるかもしれません。基本的に彼は、ポリモーフィズムが解決する以上の問題を引き起こす場合、このようなケースに対処しています。

問題は、ポリモーフィズムを使用するには、各「スイッチング」クラスの「ハンドル」部分のロジックを作成する必要があることです。つまり、この場合は整数などです。明らかにこれは実用的ではありません。コードを配置するのに論理的に適切な場所ではない場合もあります。彼は「instanceof」アプローチをいくつかの悪の少ない方として推奨しています。

臭いコードを書くことを余儀なくされるすべての場合と同様に、臭いが漏れないように、1つのメソッド(または多くても1つのクラス)でボタンを押し続けてください。


22
ポリモーフィズムは失敗しません。むしろ、Steve YeggeはVisitorパターンの発明に失敗しましたinstanceof。これはの完全な代替品です。
Rotsor、

12
訪問者がここでどのように役立つかわかりません。ポイントは、NewMonsterに対するOpinionatedElfの応答はNewMonsterではなくOpinionatedElfでエンコードする必要があるということです。
DJClayworth

2
例の要点は、OpinionatedElfは、モンスターが好きか嫌いかを利用可能なデータから判別できないことです。モンスターがどのクラスに属しているかを知る必要があります。それには、instanceofが必要です。または、モンスターがOpinionatedElfが好きかどうかを何らかの方法で知る必要があります。訪問者はそれを回避しません。
DJクレイワース

2
@DJClayworth VisitorパターンMonsterクラスにメソッドを追加することでそれを回避ます。その責任は、基本的にオブジェクトを導入することです。たとえば、「こんにちは、私はオークです。私についてどう思いますか?」意見のあるエルフは、これらの「挨拶」に基づいて、と同様のコードでモンスターを判断できbool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }ます。唯一のモンスター固有のコードは、class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }すべてのモンスターの検査に十分なだけです。
Rotsor

2
ビジターが適用できない場合がある理由については、@ Chris Knightの回答をご覧ください。
James P.

20

コメントで強調されているように、訪問者パターンは良い選択です。しかし、ターゲット/アクセプター/訪問者を直接制御しないと、そのパターンを実装できません。ここでは、ラッパーを使用してサブクラスを直接制御していなくても(例としてIntegerを使用)、ビジターパターンを使用できる可能性がある1つの方法を示します。

public class IntegerWrapper {
    private Integer integer;
    public IntegerWrapper(Integer anInteger){
        integer = anInteger;
    }
    //Access the integer directly such as
    public Integer getInteger() { return integer; }
    //or method passthrough...
    public int intValue() { return integer.intValue(); }
    //then implement your visitor:
    public void accept(NumericVisitor visitor) {
        visitor.visit(this);
    }
}

もちろん、最後のクラスをラップすることは、それ自体の匂いと見なされるかもしれませんが、サブクラスに適しているかもしれません。個人的にはinstanceof、特にここが1つの方法に限定されており、私がそれを喜んで使用する場合(おそらく上記の私の提案よりも)、それほど悪臭はないと思います。あなたが言うように、それは非常に読みやすく、タイプセーフで保守可能です。いつものように、シンプルにしてください。


はい、「フォーマッタ」、「コンポジット」、「異なるタイプ」のすべての縫い目は、訪問者の方向を指しています。
Thomas Ahle、2012年

3
どのラッパーを使用するかをどのように決定しますか?if instanceofブランチを通じて?
速い歯

2
@fasttoothが指摘するように、このソリューションは問題をシフトさせるだけです。instanceof適切なhandle()メソッドを呼び出すために使用する代わりに、適切なXWrapperコンストラクタを呼び出すためにそれを使用する必要があります...
Matthias

15

hugeの代わりに、if処理するインスタンスをマップに配置できます(key:クラス、value:ハンドラー)。

キーによる検索がを返す場合、null一致するハンドラーを検索しようとする特別なハンドラーメソッドを呼び出します(たとえばisInstance()、マップ内のすべてのキーを呼び出すことによって)。

ハンドラが見つかったら、新しいキーの下にハンドラを登録します。

これにより、一般的なケースが高速かつシンプルになり、継承を処理できるようになります。


+1数十のオブジェクトタイプが存在するXMLスキーマまたはメッセージングシステムから生成されたコードを処理するときに、このアプローチを使用して、本質的にタイプセーフでない方法でコードに渡されました。
DNA

13

リフレクションを使用できます。

public final class Handler {
  public static void handle(Object o) {
    try {
      Method handler = Handler.class.getMethod("handle", o.getClass());
      handler.invoke(null, o);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public static void handle(Integer num) { /* ... */ }
  public static void handle(BigDecimal num) { /* ... */ }
  // to handle new types, just add more handle methods...
}

アイデアを拡張して、特定のインターフェースを実装するサブクラスおよびクラスを一般的に処理することができます。


34
これは、instanceof演算子よりもさらににおいがすることを主張します。しかしうまくいくはずです。
TimBüthe11年

5
@TimBüthe:if then elseハンドラーを追加、削除、または変更するために、少なくとも成長するチェーンに対処する必要はありません。コードは変更に対して脆弱ではありません。だから私はこの理由からそれはinstanceofアプローチよりも優れていると思います。とにかく、私は有効な代替案を提供したかっただけです。
ジョルダン

1
これは基本的に、動的言語がダックタイピング
DNA

@DNA:それはマルチメソッドではないでしょうか?
ジョルダン

1
なぜ使用する代わりにすべてのメソッドを反復するのgetMethod(String name, Class<?>... parameterTypes)ですか?さもないと私は代わる==isAssignableFromパラメータの型チェックのために。
Aleksandr Dubinsky、2015年

9

責任の連鎖パターンを検討することができます。最初の例では、次のようになります。

public abstract class StuffHandler {
   private StuffHandler next;

   public final boolean handle(Object o) {
      boolean handled = doHandle(o);
      if (handled) { return true; }
      else if (next == null) { return false; }
      else { return next.handle(o); }
   }

   public void setNext(StuffHandler next) { this.next = next; }

   protected abstract boolean doHandle(Object o);
}

public class IntegerHandler extends StuffHandler {
   @Override
   protected boolean doHandle(Object o) {
      if (!o instanceof Integer) {
         return false;
      }
      NumberHandler.handle((Integer) o);
      return true;
   }
}

そして、他のハンドラーについても同様です。次に、StuffHandlerを順番に(最も具体的なものから最も具体的でないものまで、最後の「フォールバック」ハンドラを使用して)文字列化する場合であり、デパッチャコードはだけfirstHandler.handle(o);です。

(代わりに、チェーンを使用するのではなくList<StuffHandler>、ディスパッチャークラスにを入れ、handle()trueが返されるまでリストをループさせます)。


9

最善の解決策は、クラスをキー、ハンドラを値とするHashMapだと思います。HashMapベースのソリューションは一定のアルゴリズムの複雑度θ(1)で実行されますが、if-instanceof-elseの嗅覚チェーンは線形アルゴリズムの複雑度O(N)で実行されます。ここで、Nはif-instanceof-elseチェーン内のリンクの数です。 (つまり、処理されるさまざまなクラスの数)。したがって、HashMapベースのソリューションのパフォーマンスは、if-instanceof-elseチェーンソリューションのパフォーマンスよりも漸近的にN倍高くなります。Messageクラスの異なる子孫を異なる方法で処理する必要があることを考慮してください:Message1、Message2など。以下は、HashMapベースの処理のコードスニペットです。

public class YourClass {
    private class Handler {
        public void go(Message message) {
            // the default implementation just notifies that it doesn't handle the message
            System.out.println(
                "Possibly due to a typo, empty handler is set to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        }
    }
    private Map<Class<? extends Message>, Handler> messageHandling = 
        new HashMap<Class<? extends Message>, Handler>();

    // Constructor of your class is a place to initialize the message handling mechanism    
    public YourClass() {
        messageHandling.put(Message1.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message1
        } });
        messageHandling.put(Message2.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message2
        } });
        // etc. for Message3, etc.
    }

    // The method in which you receive a variable of base class Message, but you need to
    //   handle it in accordance to of what derived type that instance is
    public handleMessage(Message message) {
        Handler handler = messageHandling.get(message.getClass());
        if (handler == null) {
            System.out.println(
                "Don't know how to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        } else {
            handler.go(message);
        }
    }
}

JavaのClassタイプの変数の使用に関する詳細情報:http : //docs.oracle.com/javase/tutorial/reflect/class/classNew.html


少数の場合(おそらく実際の例ではこれらのクラスの数よりも多い)、if-elseはヒープメモリをまったく使用しないだけでなく、マップよりも優れています
idelvall


0

私はこの問題を使用して解決しましたreflection(前Generics時代の約15年前)。

GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();

ジェネリッククラス(抽象基本クラス)を1つ定義しました。基本クラスの多くの具体的な実装を定義しました。各具象クラスは、classNameをパラメーターとしてロードされます。このクラス名は、構成の一部として定義されます。

基本クラスはすべての具象クラスに共通の状態を定義し、具象クラスは基本クラスで定義された抽象ルールをオーバーライドすることによって状態を変更します。

現時点では、このメカニズムの名前はわかりません reflection

さらにいくつかの選択肢が、この中に列挙されている記事Mapenum反射から離れて。


気になるだけなのにGenericClassinterfaceどうして?
Ztyx

私は共通の状態とデフォルトの動作をしていたため、多くの関連オブジェクト間で共有する必要があります
Ravindra babu
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.