Switch over type in java

73,217

Solution 1

Here is an approach that does not deal with class names at all, and dispatches as fast as a switch statement does: make a hash map to map Class<T> objects to class-specific handlers, and use the map instead of a switch:

// Declare an interface for your polymorphic handlers to implement.
// There will be only anonymous implementations of this interface.
private interface Handler {
    void handle(Object o);
}
// Make a map that translates a Class object to a Handler
private static final Map<Class,Handler> dispatch = new HashMap<Class,Handler>();
// Populate the map in a static initializer
static {
    dispatch.put(A.class, new Handler() {
        public void handle(Object o) {
            handleA((A)o);
        }
    });
    dispatch.put(B.class, new Handler() {
        public void handle(Object o) {
            handleB((B)o);
        }
    });
    dispatch.put(C.class, new Handler() {
        public void handle(Object o) {
            handleC((C)o);
        }
    });
}
// This object performs the dispatch by looking up a handler,
// and calling it if it's available
private static void handle(Object o) {
    Handler h = dispatch.get(o.getClass());
    if (h == null) {
        // Throw an exception: unknown type
    }
    h.handle(o); // <<== Here is the magic
}

Solution 2

Using java 8 lambdas you can get to something like this:

Collection col = Arrays.asList(1,2,3);
switchType(col, 
       caze(Collection.class, c->System.out.println(c.size())),
       caze(ArrayBlockingQueue.class, bq->System.out.println(bq.remainingCapacity())),
       caze(Queue.class, q->System.out.println(q.poll())),
       caze(String.class, s->System.out.println(s.substring(0))),
       caze(ArrayList.class, al->System.out.println(al.get(0)))
);

In order to do that you should define the following static methods:

public static <T> void switchType(Object o, Consumer... a) {
    for (Consumer consumer : a)
        consumer.accept(o);
}

public static <T> Consumer caze(Class<T> cls, Consumer<T> c) {
    return obj -> Optional.of(obj).filter(cls::isInstance).map(cls::cast).ifPresent(c);
}    

Solution 3

Java had a draft to support this which released in Java SE 17. See here. The syntax looks like this

switch (obj) {
    case Integer i: handleI(i); break;
    case Byte b:    handleB(b); break;
    case Long l:    handleL(l); break;
    case Double d:  handleD(d); break;
    case String s:  handleS(s); break
    default:        handle(obj);
}

Solution 4

You was very close to the solution with enums. It hasn't compiled because your enum missed constructor and coversion method to map enum from String. Actually you could solve it even without String, i.e. without calling getCanonicalName at all:

public enum Classes {
  // changed enum constants a bit to avoid confusing with target class names
  ClsA (A.class),
  ClsB (B.class),
  ClsC (C.class),
  UNKNOWN(null);
  private final Class<?> targetClass;
  Classes(Class<?> targetClass) {
    this.targetClass = targetClass;
  }
  public static Classes fromClass(Class<?> cls) {
    for(Classes c : values()) {
      if(c.targetClass == cls)
         return c;
    }
    return UNKNOWN;
  }
}

switch (Classes.fromClass(o.getClass())) {
case ClsA:
  handleA((A)o);
  break;
case ClsB:
  handleB((B)o);
  break;
case ClsC:
  handleC((C)o);
  break;
default:
  handleUnknown(o);
  break;
}

if you get significant count of known classes, consider using map instead of iterating in Classes.fromClass, e.g.:

public enum Classes {
  ClsA(A.class),
  ClsB(B.class),
  // etc...
  UNKNWON(null);

  // need a wrapper class to avoid compilation problem
  // with referring static enum field within an initializer 
  private static class Holder {
    public static final IdentityHashMap<Class<?>, Classes> map = new IdentityHashMap<>();
  }
  Classes(Class<?> targetClass) {
    Holder.map.put(targetClass, this);
  }
  public static Classes fromClass(Class<?> cls) {
    Classes c = Holder.map.get(cls);
    return c != null ? c : UNKNOWN;
  }
}

Solution 5

The instanceof operator is a simple approach, when you don't own the classes. An instanceof expression is true when the object is the given class or a subclass.

You mention that you don't own the classes. The owner could introduce subclasses in a subsequent release. Say the owner introduces APlus as a subclass of A. An instance of APlus is an A. Code that works on an A should also work on an APlus. If you use instanceof, your code would continue to work -- without effort from you. If you use class names, it would fail -- without notice from your compiler.

If you're repeatedly switching on the same object, you might find it useful to wrap the object once in a wrapper class that implements an interface. Thereafter, you can simply call methods on the interface -- with no if, switch, or map.

public interface IWrapper {
    public void handle();
    public String describe();
}

public AWrapper implements IWrapper { ... }
public BWrapper implements IWrapper { ... }
public CWrapper implements IWrapper { ... }
public UnknownWrapper implements IWrapper { ... }

IWrapper wrap( Object o ) {
    if ( o instanceof A ) return new AWrapper((A) o);
    else if ( o instanceof B ) return new BWrapper((B) o);
    else if ( o instanceof C ) return new CWrapper((C) o);
    else return new UnknownWrapper(o);
}

Even in the guaranteed absence of subclasses, avoid specifying class names as literal strings in switch cases. This allows errors that the compiler will not find, which may cost you debugging time.

Share:
73,217
ewok
Author by

ewok

Software engineer in the Greater Boston Area. Primary areas of expertise include Java, Python, web-dev, and general OOP, though I have dabbled in many other technologies.

Updated on February 18, 2021

Comments

  • ewok
    ewok over 3 years

    Before I start, I know there are a bunch of answers to this question that suggest alternate approaches. I'm looking for assistance to this particular approach as to whether it is possible, and if not, similar approaches that might work.

    I have a method that takes a superclass and calls a method based on the type of the passed object. for instance:

    public void handle(Object o){
      if (o instanceof A)
        handleA((A)o);
      else if (o instanceof B)
        handleB((B)o);
      else if (o instanceof C)
        handleC((C)o);
      else 
        handleUnknown(o);
    

    I can't modify the subtypes to override a handle() method, as this answer would suggest, because I don't own the classes. So the instanceof method is all I have.

    I'd like to use a switch statement instead of if/else, simply because it's much neater. I know you can only switch on primitives and Strings, so I'm switching over the class name:

    switch(o.getClass().getCanonicalName()){
    case "my.package.A":
      handleA((A)o);
      break;
    case "my.package.B":
      handleB((B)o);
      break;
    case "my.package.C":
      handleC((C)o);
      break;
    default:
      handleUnknown(o);
      break;
    }
    

    The catch here is that the canonical names are VERY long (like 12 subpackages), and I can't call ClassName.class.getCanonicalName() in the case statement because Java doesn't allow that. So my next solution was an Enum. This is where I hit my problem.

    I'd like my code to look something like this:

    public enum Classes {
      A (A.getCanonicalName()),
      B (B.getCanonicalName()),
      C (C.getCanonicalName());
    }
    
    switch (o.getClass().getCanonicalName()){
    case Classes.A:
      handleA((A)o);
      break;
    case Classes.B:
      handleB((B)o);
      break;
    case Classes.C:
      handleC((C)o);
      break;
    default:
      handleUnknown(o);
      break;
    }
    

    But this doesn't compile. I'm not sure why. I'd like some approach that allows me to switch over the type without having to type out the entire canonical name. If I do that, I might as well just use if/else and instanceof.

    NOTE There are a couple of types that have the same name (inner classes), so getSimpleName() is out.

  • Andy Thomas
    Andy Thomas about 9 years
    What happens if this is fed a subclass of A? The OP's instanceof would have called handleA().
  • Sergey Kalinichenko
    Sergey Kalinichenko about 9 years
    @AndyThomas The same thing as in OP's switch statement :)
  • Jesper
    Jesper about 9 years
    With Java 8 lambda's or method references you can make this a lot more succinct (no need for all the anonymous Handler subclasses).
  • Sergey Kalinichenko
    Sergey Kalinichenko about 9 years
    @Jesper That's definitely true. I'm not sure if OP is allowed to use Java 8, though.
  • Al-Mothafar
    Al-Mothafar over 7 years
    Worked, nice, but how to break I mean if first match called then get out of Consumer
  • Joel Shemtov
    Joel Shemtov about 7 years
    Most elegant solution!
  • 2Big2BeSmall
    2Big2BeSmall almost 7 years
    @eitan This is great thinking, but how can I use return for each case?
  • babay
    babay almost 7 years
    this adds a lot of everhead
  • eitan
    eitan almost 7 years
    @Francy, you can use Functions instead of Consumers.
  • MasterJoe
    MasterJoe over 4 years
    The limitation of this approach is that we cannot find the usages of a particular class from this switch easily. IDEs usually have a feature to find usages of a class, not string. For strings, we have to use the search text option. That can be cumbersome and we might miss some usages.
  • zappee
    zappee about 2 years
    This is so elegant solution, but only available from Java SE 17 (released in September 2021).