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 등을 제어 할 수 있습니다.

몇 줄로 할 수있는 코드 줄을 많이 사용하고 싶지 않습니다. (때로는 Integer.class를 IntegerStuff의 인스턴스로, BigDecimal.class를 BigDecimalStuff의 인스턴스로 매핑하는 HashMap을 만듭니다.하지만 오늘은 더 간단한 것을 원합니다.)

다음과 같이 간단한 것을 원합니다.

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
lexicore

25
방문자 패턴은 대상 클래스 (예 : Integer)에 메소드를 추가해야합니다. JavaScript에서는 쉽고 Java에서는 어렵습니다. 대상 클래스를 디자인 할 때 우수한 패턴; 예전 클래스에 새로운 트릭을 가르치려고 할 때 그렇게 쉽지는 않습니다.
Mark Lutton

4
@lexicore : 주석의 마크 다운이 제한됩니다. [text](link)댓글에 링크를 게시하는 데 사용 합니다.
BalusC

2
"하지만 Java는 그렇게 작동하지 않습니다." 아마도 나는 오해하고 있지만 Java는 메서드 오버로딩을 지원합니다 (정적 메서드에서도) 괜찮습니다 ... 위의 메서드에 반환 유형이 누락되어 있기 때문입니다.
Powerlord

4
@Powerlord 과부하 해상도는 정적 으로 컴파일시 .
Aleksandr Dubinsky 2015 년

답변:


55

Steve Yegge의 Amazon 블로그 "when polymorphism fails" 에서이 항목에 관심이있을 수 있습니다 . 본질적으로 그는 다형성이 해결하는 것보다 더 많은 문제를 야기하는 이와 같은 경우를 다루고 있습니다.

문제는 다형성을 사용하려면 각 '스위칭'클래스의 "처리"부분의 논리를 만들어야한다는 것입니다.이 경우에는 정수 등이 있습니다. 분명히 이것은 실용적이지 않습니다. 때로는 논리적으로 코드를 넣을 올바른 장소가 아닙니다. 그는 '인스턴스 오브'접근 방식을 여러 가지 악 중 적은 것으로 권장합니다.

악취가 나는 코드를 작성해야하는 모든 경우와 마찬가지로 악취가 누출되지 않도록 하나의 메서드 (또는 최대 하나의 클래스)로 단추를 유지하십시오.


22
다형성은 실패하지 않습니다. 오히려 Steve Yegge는 .NET의 완벽한 대체물 인 방문자 패턴을 발명하지 못했습니다 instanceof.
Rotsor

12
방문자가 여기서 어떻게 도움을 주는지 모르겠습니다. 요점은 NewMonster에 대한 OpinionatedElf의 응답이 NewMonster가 아니라 OpinionatedElf로 인코딩되어야한다는 것입니다.
DJClayworth 2011-06-07

2
이 예의 요점은 OpinionatedElf가 사용 가능한 데이터에서 몬스터를 좋아하는지 싫어하는지를 알 수 없다는 것입니다. 몬스터가 속한 클래스를 알아야합니다. 그것은 instanceof 또는 괴물이 OpinionatedElf가 그것을 좋아하는지 어떤 식 으로든 알아야합니다. 방문자는 그것에 대해 이해하지 못합니다.
DJClayworth 2011 년

2
@DJClayworth Visitor pattern Monster 클래스에 메소드를 추가하여이를 우회합니다. 기본적으로 "Hello, I am an Orc. 나에 대해 어떻게 생각하세요?"와 같이 객체를 소개하는 책임이 있습니다. 의견이있는 엘프는 이러한 "인사말"을 기반으로 몬스터를 판단 할 수 있으며, 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 2011-06-09

2
어떤 경우에 Visitor가 적용되지 않는 이유는 @Chris Knight의 답변을 참조하십시오.
James P.

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여기에서 나쁜 냄새 가 아니라고 생각 합니다. 특히 그것이 한 가지 방법에 국한되어 행복하게 사용한다면 (아마도 위의 내 제안보다). 당신이 말했듯이, 그것은 매우 읽기 쉽고 형식이 안전하며 유지 관리가 가능합니다. 항상 그렇듯이 간단하게 유지하십시오.


예, "포맷터", "복합", "다른 유형"은 방문자의 방향을 가리키는 모든 이음새입니다.
Thomas Ahle 2012-01-03

3
사용할 래퍼를 어떻게 결정합니까? if instanceof 분기를 통해?
fast tooth

2
@fasttooth가 지적했듯이이 솔루션은 문제를 이동시킬뿐입니다. instanceof올바른 handle()메서드 를 호출하는 대신 사용 하는 대신 올바른 XWrapper생성자 호출을 사용해야합니다 .
Matthias

15

huge 대신 if처리하는 인스턴스를 맵 (key : class, value : handler)에 넣을 수 있습니다.

키별 조회가를 반환 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 연산자보다 훨씬 더 냄새가 난다고 주장합니다. 그래도 작동합니다.
Tim Büthe

5
@Tim Büthe : 적어도 if then else핸들러를 추가, 제거 또는 수정하기 위해 성장하는 체인 을 다룰 필요는 없습니다 . 코드는 변경에 덜 취약합니다. 그래서 저는 이것이 instanceof접근 방식 보다 우월하다고 말하고 싶습니다 . 어쨌든 유효한 대안을 제시하고 싶었습니다.
Jordão

1
이것은 본질적으로 동적 언어가 오리 타이핑을
DNA

@DNA : 다중 방법이 아닐까요?
Jordão

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

다른 핸들러에 대해서도 비슷합니다. 그런 다음 StuffHandlers를 순서대로 묶는 경우 (최종 '대체'핸들러를 사용하여 가장 구체적이거나 가장 구체적이지 않음), 발송자 코드는 다음과 같습니다.firstHandler.handle(o); .

(대안은 체인을 사용하는 대신 List<StuffHandler>디스패처 클래스에 a를 가지고 handle()true를 반환 할 때까지 목록을 반복하는 것 입니다).


9

가장 좋은 해결책은 Class를 키로, Handler를 값으로 사용하는 HashMap이라고 생각합니다. HashMap 기반 솔루션은 일정한 알고리즘 복잡성 θ (1)에서 실행되는 반면 if-instanceof-else의 냄새 체인은 선형 알고리즘 복잡성 O (N)에서 실행됩니다. 여기서 N은 if-instanceof-else 체인의 링크 수입니다. (즉, 처리 할 다른 클래스의 수). 따라서 HashMap 기반 솔루션의 성능은 if-instanceof-else 체인 솔루션의 성능보다 점근 적으로 N 배 높습니다. Message1, Message2 등 Message 클래스의 다른 하위 항목을 다르게 처리해야한다는 점을 고려하십시오. 다음은 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에서 클래스 유형의 변수 사용에 대한 추가 정보 : http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


적은 수의 경우 (실제 예의 경우 해당 클래스 수보다 높음)의 경우 if-else는 힙 메모리를 전혀 사용하지 않는 것 외에도 맵을 능가합니다
idelvall


0

나는이 문제를 reflection(제네릭 이전 시대에 약 15 년 전) 사용하여 해결했습니다 .

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

하나의 일반 클래스 (추상 기본 클래스)를 정의했습니다. 기본 클래스의 많은 구체적인 구현을 정의했습니다. 각 구체적인 클래스는 매개 변수로 className과 함께로드됩니다. 이 클래스 이름은 구성의 일부로 정의됩니다.

기본 클래스는 모든 구체적인 클래스에 걸쳐 공통 상태를 정의하고 구체적인 클래스는 기본 클래스에 정의 된 추상 규칙을 재정 의하여 상태를 수정합니다.

그 당시에는이 메커니즘의 이름을 알지 못합니다 reflection.

몇 가지 더 대안이에 나와있는 기사 : Mapenum반사 그렇다.


그냥 궁금해서, 왜하지 않았다 GenericClassinterface?
Ztyx

나는 일반적인 상태와 많은 관련 개체간에 공유되어야하는 기본 동작, 한
라빈 드라 BABU를
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.