Java'da instanceof'tan kaçınmak


102

"Eşgörünüm" işlemleri zincirine sahip olmak "kod kokusu" olarak kabul edilir. Standart cevap "polimorfizm kullan" dır. Bu durumda bunu nasıl yaparım?

Bir temel sınıfın birkaç alt sınıfı vardır; hiçbiri benim kontrolüm altında değil. Integer, Double, BigDecimal vb Java sınıflarında da benzer bir durum söz konusudur.

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 üzerinde kontrole sahibim ve benzeri.

Birkaç satırın işe yarayacağı çok sayıda kod satırı kullanmak istemiyorum. (Bazen bir IntegerStuff örneğine Integer.class, BigDecimalStuff örneğine BigDecimal.class gibi bir HashMap eşlemesi yaparım. Ama bugün daha basit bir şey istiyorum.)

Bunun kadar basit bir şey istiyorum:

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

Ancak Java bu şekilde çalışmıyor.

Biçimlendirirken statik yöntemler kullanmak istiyorum. Biçimlendirdiğim şeyler bileşik, burada bir Thing1 bir Thing2s dizisi içerebilir ve bir Thing2 bir Thing1 dizisi içerebilir. Biçimlendiricilerimi şu şekilde uygularken bir sorun yaşadım:

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);
  }
}

Evet, HashMap'in ve biraz daha fazla kodun da bunu düzeltebileceğini biliyorum. Ancak "instanceof", kıyaslandığında çok okunabilir ve sürdürülebilir görünüyor. Basit ama kokmayan bir şey var mı?

Not 5/10/2010 eklendi:

Görünüşe göre gelecekte yeni alt sınıflar eklenecek ve mevcut kodumun bunları zarif bir şekilde ele alması gerekecek. Sınıfta HashMap bu durumda çalışmayacaktır çünkü Sınıf bulunmayacaktır. En özelden başlayıp en genel olanla biten if ifadelerinden oluşan bir zincir, sonuçta muhtemelen en iyisidir:

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
Bir [ziyaretçi kalıbı] [1] öneririm. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore

25
Ziyaretçi kalıbı, hedef sınıfa (örneğin Tamsayı) bir yöntem eklemeyi gerektirir - JavaScript'te kolay, Java'da zor. Hedef sınıfları tasarlarken mükemmel desen; eski bir Sınıfa yeni numaralar öğretmeye çalışırken o kadar kolay değil.
Mark Lutton

4
@lexicore: yorumlarda markdown sınırlıdır. [text](link)Yorumlarda bağlantı göndermek için kullanın .
BalusC

2
"Ancak Java bu şekilde çalışmıyor." Belki bir şeyleri yanlış anlıyorum, ancak Java, yöntem aşırı yüklemesini destekliyor (statik yöntemlerde bile) gayet iyi ... sadece yukarıdaki yöntemleriniz geri dönüş türünü eksik.
Powerlord

4
@Powerlord Aşırı yük çözümlemesi, derleme sırasında statiktir .
Aleksandr Dubinsky

Yanıtlar:


55

Steve Yegge'nin Amazon blogundaki şu giriş ilginizi çekebilir: "çok biçimlilik başarısız olduğunda" . Esasen, polimorfizmin çözdüğünden daha fazla soruna neden olduğu bu gibi durumları ele alıyor.

Sorun şu ki, polimorfizmi kullanmak için her bir "anahtarlama" sınıfının "tutamaç" mantığını - bu durumda Tamsayı vb. Açıkçası bu pratik değil. Bazen kodu yerleştirmek için mantıksal olarak doğru yer bile değildir. Birkaç kötülüğün daha azı olarak 'örnek' yaklaşımını tavsiye ediyor.

Kokulu kod yazmaya zorlandığınız tüm durumlarda olduğu gibi, kokunun dışarı sızmaması için tek bir yöntemde (veya en fazla bir sınıfta) düğmeli tutun.


22
Çok biçimlilik başarısız olmaz. Bunun yerine Steve Yegge, yerine mükemmel bir alternatif olan Ziyaretçi modelini icat etmekte başarısız oluyor instanceof.
Rotsor

12
Ziyaretçinin burada nasıl yardımcı olduğunu anlamıyorum. Mesele şu ki, OpinionatedElf'in NewMonster'a verdiği yanıt NewMonster'da değil, OpinionatedElf'de kodlanmalıdır.
DJClayworth

2
Örneğin, OpinionatedElf'in mevcut verilerden Monster'ı sevip sevmediğini söyleyememesidir. Canavarın hangi sınıfa ait olduğunu bilmek zorundadır. Bu ya bir örneğini gerektirir ya da Canavar bir şekilde OpinionatedElf'in beğenip beğenmediğini bilmek zorundadır. Ziyaretçi bunu aşamaz.
DJClayworth

2
@DJClayworth Ziyaretçi kalıbı , sorumluluğu temelde nesneyi tanıtmak olan, "Merhaba, ben bir Ork'um. Benim hakkımda ne düşünüyorsun?" Gibi bir yöntem ekleyerek bunu aşıyor Monster. Fikir sahibi elf daha sonra canavarları benzer bir kodla bu "selamlamalara" dayanarak yargılayabilir bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. Canavara özgü tek kod, class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }her canavar incelemesi için bir kez ve herkes için yeterli olacaktır .
Rotsor

2
Ziyaretçinin bazı durumlarda uygulanamama nedeni için @Chris Knight'ın cevabına bakınız.
James P.

20

Yorumlarda vurgulandığı gibi, ziyaretçi düzeni iyi bir seçim olacaktır. Ancak hedef / alıcı / ziyaret eden kişi üzerinde doğrudan kontrol olmadan bu modeli uygulayamazsınız. Sarmalayıcıları kullanarak alt sınıflar üzerinde doğrudan kontrolünüz olmasa bile, ziyaretçi kalıbının muhtemelen burada kullanılmasının bir yolu şudur (örnek olarak Tamsayı alarak):

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);
    }
}

Elbette, son bir dersi paketlemek kendi başına bir koku olarak kabul edilebilir, ancak alt sınıflarınıza iyi bir uyum olabilir. Şahsen, instanceofburada o kadar kötü bir koku olduğunu düşünmüyorum , özellikle tek bir yöntemle sınırlıysa ve onu mutlu bir şekilde kullanırsam (muhtemelen yukarıdaki kendi önerim üzerine). Sizin de söylediğiniz gibi, oldukça okunabilir, güvenilir ve bakımı yapılabilir. Her zaman olduğu gibi, basit tutun.


Evet, "Biçimlendirici", "Bileşik", "Farklı türler" tüm dikişler ziyaretçinin yönünü işaret eder.
Thomas Ahle

3
hangi sargıyı kullanacağınızı nasıl belirlersiniz? bir if dallanma örneği aracılığıyla?
hızlı diş

2
@Fasttooth'un işaret ettiği gibi, bu çözüm yalnızca sorunu değiştirir. instanceofDoğru handle()yöntemi aramak için kullanmak yerine artık onu doğru XWrapperkurucu olarak kullanmak zorunda kalacaksınız ...
Matthias

16

Bir büyük yerine, işlediğiniz iförnekleri bir haritaya koyabilirsiniz (anahtar: sınıf, değer: işleyici).

Anahtarla arama dönerse null, eşleşen bir işleyici bulmaya çalışan özel bir işleyici yöntemi çağırın (örneğin isInstance(), haritadaki her anahtarı çağırarak ).

Bir işleyici bulunduğunda, onu yeni anahtarın altına kaydedin.

Bu, genel durumu hızlı ve basit hale getirir ve kalıtımla ilgilenmenize olanak tanır.


+1 Bu yaklaşımı, XML şemalarından veya düzinelerce nesne türünün olduğu mesajlaşma sisteminden üretilen kodu işlerken, koduma esasen tür güvenli olmayan bir şekilde teslim ederken kullandım.
DNA

13

Yansımayı kullanabilirsiniz:

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...
}

Alt sınıfları ve belirli arabirimleri uygulayan sınıfları genel olarak ele almak için fikri genişletebilirsiniz.


35
Bunun operatör örneğinden daha fazla koktuğunu iddia ediyorum. Yine de çalışmalı.
Tim Büthe

5
@Tim Büthe: İşleyicileri if then elseeklemek, çıkarmak veya değiştirmek için en azından büyüyen bir zincirle uğraşmak zorunda değilsiniz . Kod değişikliklere karşı daha az kırılgandır. Bu nedenle, instanceofyaklaşımdan daha üstün olduğunu söyleyebilirim . Her neyse, sadece geçerli bir alternatif vermek istedim.
Jordão

1
Bu, esasen dinamik bir dilin durumu ördek yazarak
DNA

@DNA: bu çoklu yöntemler olmaz mıydı ?
Jordão

1
Neden kullanmak yerine tüm yöntemleri yineliyorsunuz getMethod(String name, Class<?>... parameterTypes)? Yoksa ben yerini alacak ==olan isAssignableFromparametrenin tipi kontrol için.
Aleksandr Dubinsky

9

Sorumluluk Zinciri modelini düşünebilirsiniz . İlk örneğiniz için şöyle bir şey:

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;
   }
}

ve sonra benzer şekilde diğer işleyicileriniz için. O zaman bu, StuffHandlers'ı sırayla bir araya dizme durumudur (en az spesifik olana, son bir 'geri dönüş' işleyicisiyle) ve gönderici kodunuz sadece firstHandler.handle(o);.

(Alternatif olarak, bir zincir kullanmak yerine List<StuffHandler>, dağıtıcı sınıfınızda bir tane bulundurmak ve handle()doğru dönene kadar listeyi döngü halinde tutmaktır).


9

En iyi çözümün anahtar olarak Class ve değer olarak Handler ile HashMap olduğunu düşünüyorum. HashMap tabanlı çözümün sabit algoritmik karmaşıklıkta θ (1) çalıştığını, if-instanceof-else kokulu zincirinin doğrusal algoritmik karmaşıklıkta O (N) çalıştığını unutmayın; burada N, if-instanceof-else zincirindeki bağlantıların sayısıdır (yani ele alınacak farklı sınıfların sayısı). Dolayısıyla HashMap tabanlı çözümün performansı, if-instanceof-else zincir çözümünün performansından asimptotik olarak daha yüksek N kattır. Mesaj sınıfının farklı soyundan gelenleri farklı şekilde ele almanız gerektiğini düşünün: Mesaj1, Mesaj2, vb. HashMap tabanlı işleme için kod parçacığı aşağıdadır.

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'da Class türü değişkenlerin kullanımı hakkında daha fazla bilgi: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


az sayıdaki durum için (muhtemelen herhangi bir gerçek örnek için bu sınıfların sayısından daha yüksek) if-else, hiç yığın bellek kullanmamanın yanı sıra haritadan daha iyi performans gösterir
idelvall


0

Bu sorunu kullanarak reflectionçözdüm (Generics öncesi dönemde yaklaşık 15 yıl önce).

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

Bir Genel Sınıf (soyut Temel sınıf) tanımladım. Temel sınıfın birçok somut uygulamasını tanımladım. Her somut sınıf, parametre olarak className ile yüklenecektir. Bu sınıf adı, konfigürasyonun bir parçası olarak tanımlanır.

Temel sınıf, tüm somut sınıflarda ortak durumu tanımlar ve somut sınıflar, temel sınıfta tanımlanan soyut kuralları geçersiz kılarak durumu değiştirir.

O zaman, olarak bilinen bu mekanizmanın adını bilmiyorum reflection.

Bu makalede birkaç alternatif listelenmiştir : Mapve enumyansıtma dışında.


Sadece merak, neden yapmadığını GenericClassbir interface?
Ztyx

Bir çok ilgili nesnede paylaşılması gereken ortak
durumum

0

BaseClass'a sınıfın adını döndüren bir yöntem ekleyin. Ve belirli sınıf adıyla yöntemleri geçersiz kılın

public class BaseClass{
  // properties and methods
  public String classType(){
      return BaseClass.class.getSimpleName();
  }
}

public class SubClass1 extends BaseClass{
 // properties and methods
  @Override
  public String classType(){
      return SubClass1.class.getSimpleName();
  }
}

public class SubClass2 extends BaseClass{
 // properties and methods
  @Override
  public String classType(){
      return SubClass1.class.getSimpleName();
  }
}

Şimdi anahtar kutusunu aşağıdaki şekilde kullanın-

switch(obj.classType()){
    case SubClass1:
        // do subclass1 task
        break;
    case SubClass2:
        // do subclass2 task
        break;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.