Уникнення instanceof в Java


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 може містити масив Thing2s, а Thing2 може містити масив Thing1s. У мене виникла проблема, коли я реалізував такі формати:

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" здається настільки читабельним і доцільним у порівнянні. Чи є щось просте, але не смердюче?

Примітка додана 10.10.2010:

Виявляється, нові підкласи, ймовірно, будуть додані в майбутньому, і мій існуючий код доведеться грамотно обробляти їх. У цьому випадку 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
lexicore

25
Шаблон відвідувача вимагає додавання методу до цільового класу (наприклад, Integer) - простий у JavaScript, жорсткий у Java. Відмінна модель при проектуванні цільових класів; не так просто, коли намагаєтесь навчити старий клас нових хитрощів.
Марк Люттон

4
@lexicore: відмітка в коментарях обмежена. Використовуйте [text](link)для розміщення посилань у коментарях.
BalusC

2
"Але Java просто не працює таким чином." Можливо, я нерозумію речі, але Java підтримує перевантаження методу (навіть на статичні методи) просто чудово ... просто у ваших методах вище відсутній тип повернення.
Powerlord

4
@ Роздільна здатність перевантажень статична на час компіляції .
Олександр Дубінський

Відповіді:


55

Вас може зацікавити цей запис із блогу Амазонки Стіва Йегге: "коли поліморфізм провалюється" . По суті, він займається подібними випадками, коли поліморфізм викликає більше клопоту, ніж вирішує.

Проблема полягає в тому, що для використання поліморфізму вам доведеться зробити логіку "обробляти" частиною кожного класу "комутація" - тобто Integer тощо. Зрозуміло, що це не практично. Іноді це навіть не логічно правильне місце для введення коду. Він рекомендує підхід "instanceof" як менший з кількох злих.

Як і у всіх випадках, коли вас змушують писати смердючий код, тримайте його застебнутим одним методом (або максимум одним класом), щоб запах не просочувався.


22
Поліморфізм не провалюється. Швидше за все, Стів Йегге не винайшов шаблон відвідувачів, який є ідеальною заміною instanceof.
Ротсор

12
Я не бачу, як відвідувач допомагає тут. Справа в тому, що відповідь OpinionatedElf на NewMonster повинна кодуватися не в NewMonster, а в OpinionatedElf.
DJClayworth

2
Суть прикладу полягає в тому, що OpinionatedElf не може визначити з доступних даних, чи подобається йому чи не подобається Чудовисько. Він повинен знати, до якого класу належить Монстр. Для цього потрібен або екземпляр, або Монстр має певним чином знати, чи подобається Думаючому Ельфу. Відвідувач цього не обходить.
DJClayworth

2
@DJClayworth відвідувачів шаблон робить обійти це шляхом додавання методу до 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); } }, достатній для кожного огляду монстра раз і назавжди.
Ротсор

2
Дивіться відповідь @Chris Knight з причини, чому відвідувач не може бути застосований в деяких випадках.
Джеймс П.

20

Як підкреслено в коментарях, модель відвідувачів буде хорошим вибором. Але без прямого контролю над ціллю / акцептом / відвідувачем ви не можете реалізувати цю схему. Ось один із способів використання відвідувача може все-таки використовуватись, навіть якщо ви не маєте прямого контролю над підкласами, використовуючи обгортки (беручи за приклад Integer):

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, що тут поганий запах, особливо якщо він обмежений одним методом, і я б із задоволенням використовував його (напевно, над моєю власною пропозицією вище). Як ви кажете, його досить читабельно, безпечно і зручно в експлуатації. Як завжди, нехай це буде просто.


Так, "Форматер", "Композит", "Різні типи" всі шви вказують у напрямку відвідувача.
Томас Ахле

3
як визначити, яку обгортку ви збираєтеся використовувати? через if instanceof розгалуження?
швидкий зуб

2
Як зазначає @fasttooth, це рішення лише змінює проблему. Замість того, instanceofщоб використовувати правильний handle()метод, вам доведеться використовувати його викликом правильного XWrapperконструктора ...
Маттіас

15

Замість величезних if, ви можете помістити екземпляри, якими ви керуєте, у карту (ключ: клас, значення: обробник).

Якщо пошук за ключем повертається null, зателефонуйте до спеціального методу обробника, який намагається знайти відповідний обробник (наприклад, за допомогою натискання isInstance()кожної клавіші на карті).

Коли обробник знайдений, зареєструйте його під новим ключем.

Це робить загальний випадок швидким і простим і дозволяє обробляти спадщину.


+1 Я використовував цей підхід при обробці коду, сформованого із XML-схем або системи обміну повідомленнями, де є десятки типів об'єктів, переданих моєму коду по суті нетиповим способом.
ДНК

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. Треба хоч працювати.
Тім Бют

5
@Tim Büthe: Принаймні, вам не доведеться мати справу зі зростаючою if then elseланцюжком, щоб додати, видалити чи змінити обробники. Код менш крихкий до змін. Тож я б сказав, що з цієї причини він перевершує instanceofпідхід. У всякому разі, я просто хотів дати дійсну альтернативу.
Йордао

1
Це, по суті, як динамічна мова впоралася б із ситуацією, набравши качок
ДНК

@DNA: чи не були б це багатометоди ?
Йордао

1
Чому ви повторюєте всі методи замість того, щоб використовувати getMethod(String name, Class<?>... parameterTypes)? Або ж я замінив би ==з isAssignableFromдля перевірки типу параметра.
Олександр Дубінський

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

а потім аналогічно для інших ваших обробників. Тоді це випадок з'єднання StuffHandlers у порядку (найбільш специфічному для найменш конкретного, з остаточним "резервним" обробником), і ваш код деспетчера справедливий firstHandler.handle(o);.

(Альтернатива полягає в тому, щоб замість використання ланцюга просто мати List<StuffHandler>свій клас диспетчера, і він перебирає список, поки не handle()повернеться true).


9

Я думаю, що найкращим рішенням є HashMap з класом як ключем та Handler як цінністю. Зауважимо, що рішення на основі HashMap працює з постійною алгоритмічною складністю θ (1), тоді як пахнуча ланцюг if-instanceof-else працює в лінійній алгоритмічній складності O (N), де N - кількість посилань у ланцюзі if-instanceof-else (тобто кількість різних класів, якими потрібно обробити). Таким чином, продуктивність рішення, заснованого на HashMap, асимптотично вище N разів, ніж продуктивність ланцюгового рішення if-instanceof-else. Вважайте, що вам потрібно по-різному обробляти нащадків класу 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);
        }
    }
}

Детальніше про використання змінних типу Class на Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


для невеликої кількості випадків (ймовірно, вище кількості цих класів для будь-якого реального прикладу), якщо-else перевершить карту, окрім того, що взагалі не використовує купа пам’яті
idelvall


0

Я вирішив цю проблему за допомогою reflection(приблизно 15 років тому в еру до Generics).

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

Я визначив один загальний клас (абстрактний базовий клас). Я визначив багато конкретних реалізацій базового класу. Кожен конкретний клас буде завантажений з параметром className. Ця назва класу визначена як частина конфігурації.

Базовий клас визначає загальний стан для всіх конкретних класів, а конкретні класи модифікують стан, змінюючи абстрактні правила, визначені в базовому класі.

Тоді я не знаю назви цього механізму, який був відомий як reflection.

У цій статті перераховано трохи більше альтернатив : Mapі enumкрім рефлексії.


Просто цікаво, чому не зробити ? GenericClassinterface
Ztyx

У мене була спільна поведінка та поведінка за замовчуванням, яку доводиться ділити між багатьма спорідненими об'єктами
Равіндра, бабу,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.