Как избежать 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.05.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
Шаблон Visitor требует добавления метода к целевому классу (например, Integer) - это просто в JavaScript, сложно в Java. Отличный паттерн при проектировании целевых классов; не все так просто при попытке научить старый класс новым трюкам.
Марк Латтон,
4
@lexicore: уценка в комментариях ограничена. Используйте [text](link)для размещения ссылок в комментариях.
BalusC
2
«Но Java просто так не работает». Возможно, я что-то не понимаю, но Java поддерживает перегрузку методов (даже для статических методов) просто отлично ... просто в ваших методах выше отсутствует тип возвращаемого значения.
Powerlord
4
@Powerlord Разрешение перегрузки статично во время компиляции .
Александр Дубинский

Ответы:

55

Возможно, вас заинтересует эта запись из блога Стива Йегге на Amazon: «Когда полиморфизм терпит неудачу» . По сути, он занимается такими случаями, когда полиморфизм причиняет больше проблем, чем решает.

Проблема в том, что для использования полиморфизма вы должны сделать логику «обрабатывающей» части каждого «переключающего» класса - в данном случае Integer и т. Д. Ясно, что это непрактично. Иногда это даже не логически правильное место для размещения кода. Он рекомендует подход «instanceof» как меньшее из нескольких зол.

Как и во всех случаях, когда вы вынуждены писать неприятный код, держите его застегнутым в одном методе (или не более чем в одном классе), чтобы запах не просачивался.

DJClayworth
источник
22
Полиморфизм не подводит. Скорее Стив Йегге не смог изобрести паттерн Посетитель, который является идеальной заменой instanceof.
Rotsor
12
Я не понимаю, чем здесь помогает посетитель. Дело в том, что ответ OpinionatedElf на NewMonster должен быть закодирован не в NewMonster, а в OpinionatedElf.
DJClayworth 07
2
Суть примера в том, что OpinionatedElf не может сказать по имеющимся данным, нравится ему монстр или нет. Он должен знать, к какому классу принадлежит монстр. Для этого требуется либо instanceof, либо Monster должен каким-то образом знать, нравится ли это OpinionatedElf. Посетитель этого не оставляет.
DJClayworth 09
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 09
2
См. Ответ @Chris Knight о причине, по которой Visitor не может применяться в некоторых случаях.
Джеймс П.
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, что здесь неприятный запах, особенно если он ограничен одним методом, и я бы с радостью использовал его (вероятно, по моему собственному предложению выше). Как вы говорите, он вполне читаемый, безопасный и ремонтопригодный. Как всегда, будьте проще.

Крис Найт
источник
Да, «Formatter», «Composite», «Различные типы» - все швы должны указывать в направлении посетителя.
Thomas Ahle 03
3
как вы определяете, какую оболочку собираетесь использовать? через if instanceof ветвления?
быстрый зуб
2
Как указывает @fasttooth, это решение только решает проблему. Вместо того, instanceofчтобы использовать для вызова правильного handle()метода, теперь вам придется использовать его для вызова правильного XWrapperконструктора ...
Маттиас
16

Вместо огромного размера ifвы можете поместить обрабатываемые вами экземпляры на карту (ключ: класс, значение: обработчик).

Если поиск по ключу возвращается 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. Хотя должно работать.
Тим Бют
5
@ Тим Бют: По крайней мере, вам не нужно иметь дело с растущей 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).

Cowan
источник
9

Я думаю, что лучшим решением является HashMap с классом в качестве ключа и обработчиком в качестве значения. Обратите внимание, что решение на основе 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

Серж Рогач
источник
для небольшого количества случаев (вероятно, больше, чем количество этих классов для любого реального примера) if-else превзойдет карту, за исключением того, что вообще не использует память кучи
idelvall
0

Я решил эту проблему с помощью reflection(около 15 лет назад в эпоху до Generics).

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

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

Базовый класс определяет общее состояние для всех конкретных классов, а конкретные классы будут изменять состояние, переопределяя абстрактные правила, определенные в базовом классе.

В то время я не знал названия этого механизма, который был известен как reflection.

В этой статье перечислены еще несколько альтернатив : Mapи enumпомимо рефлексии.

Равиндра бабу
источник
Просто любопытно, почему не сделать ? GenericClassinterface
Ztyx
У меня было общее состояние и поведение по умолчанию, которое должно быть общим для многих связанных объектов
Равиндра бабу
0

Добавьте метод в BaseClass, который возвращает имя класса. И переопределите методы с определенным именем класса

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

Теперь используйте корпус переключателя следующим образом:

switch(obj.classType()){
    case SubClass1:
        // do subclass1 task
        break;
    case SubClass2:
        // do subclass2 task
        break;
}
Шахриар Мирадж
источник