It seems to me that such a switch statement would make a lot of sense, but it gives a compile error :
public void m(Class c) {
switch (c) {
case SubClassOfC1.class : //do stuff; break;
case SubClassOfC2.class : //do stuff; break;
}
}
However, classes are not supported to switch on. What is the reason why?
I am not trying to workaround instanceof
, it's really at the class level that I got some operations to perform, no instances there.
The compile error is around SubClassOfC1 & SubClassOfC2 : constant expression required.
Interestingly, all answers so far are basically saying “because the specification says so”, which is correct but not really satisfying. Before Java 7, String
s were not allowed and that was often treated like being carved in stone.
But technical obstacles shouldn’t drive language design. If there is no way to compile it to efficient code, it might still get compiled to the equivalent of if … else …
clauses, and still have a win on source code brevity. In the case of String
values, there is an efficient way. Just switch over the invariant hashcode and perform an equals
check on the match candidate. In fact, the specification does not mandate to use the hash code, it could be any invariant int
property, e.g. the length or the first character, but it’s value should be different for each String
.
Similarly, a switch
over Class
objects is possible. It’s hash code is not guaranteed to be the same, but each class has a constant name with a constant hash code. E.g. the following works:
public class ClassSwitch {
static final class Foo {}
static final class Bar {}
static final class Baz {}
public static void main(String... arg) {
Class<?> cl=Bar.class;
switch(cl.getSimpleName().hashCode()) {
case 70822:
if(cl==Foo.class) {
System.out.println("case Foo:");
}
break;
case 66547:
if(cl==Bar.class) {
System.out.println("case Baz:");
}
break;
case 66555:
if(cl==Baz.class) {
System.out.println("case Baz:");
}
break;
}
}
}
I used the simple name instead of the qualified name, so that this example code is package independent. But I think, the picture is clear. It is possible to implement efficient switch
statements for any kind of object which has a constant int
property that can be predicted at compile time. That said, there is also no reason not to support long
switches. There are plenty of ways to calculate a suitable int
from a long
…
So there are another important decisions to make.
Is this feature really a benefit? It looks like code smell—even the addition of String
support was controversial. It does not add new possibilities as you may do the same with if
-else
for a small number of classes or a HashMap<Class,SomeHandlerType>
for bigger numbers. And it doesn’t really look like something that is needed so often, that it is worth expanding the language specification, even if it is just a single sentence that has to be added.
These are the considerations that drive the language design, but it’s not that minds and balances cannot change. So I’m not saying that’s impossible that a future version gets this feature.
But well, looking at the quality of generated String
switch
code I’d rather code my switches
manually…
It's because we can only switch on Constant Expressions (§15.28) or Enum constants (§8.9.1).
From the JLS:
The type of the Expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type (§8.9), or a compile-time error occurs.
To imagine why this might be, think about the optimization that occurs when the Java compiler is attempting to compile a switch statement.
- It wants to absolutely, positively guarantee equality
- It wants to be able to maximize performance (branch prediction) for all the cases
- It wants to have an effective, consistent way of transforming the constant expressions into an integer lookup table (this is why
long
and float
and double
are not supported, but String
is).
Note that String
being supported in switch
statements was added only in Java 7. This is because the compiler uses a behind the scenes conversion of switch String
to switch int
, as detailed in this article. Quick summary:
This code:
public class StringInSwitchCase {
public static void main(String[] args) {
String mode = args[0];
switch (mode) {
case "ACTIVE":
System.out.println("Application is running on Active mode");
break;
case "PASSIVE":
System.out.println("Application is running on Passive mode");
break;
case "SAFE":
System.out.println("Application is running on Safe mode");
}
}
}
Becomes this code:
public class StringInSwitchCase {
public StringInSwitchCase() {}
public static void main(string args[]) {
String mode = args[0];
String s;
switch ((s = mode).hashCode()) {
default:
break;
case -74056953:
if (s.equals("PASSIVE")) {
System.out.println("Application is running on Passive mode");
}
break;
case 2537357:
if (s.equals("SAFE")) {
System.out.println("Application is running on Safe mode");
}
break;
case 1925346054:
if (s.equals("ACTIVE")) {
System.out.println("Application is running on Active mode");
}
break;
}
}
}
We can't reliably turn Class
objects into ints the same way. Class doesn't override hashCode, it uses System.identityHashCode
.
Note also that the same class is not always the same Class
, if it has been loaded with a different ClassLoader
.
Based on documentation on the switch
statement:
A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types (discussed in Enum Types), the String class, and a few special classes that wrap certain primitive types: Character, Byte, Short, and Integer.
So essentially, it was made to work with only those types, and nothing else. Class
is not one of them.
The reason for the restriction is, as you yourself said in a comment, that switch tables are indexed by int
. All of the above types are easily convertible to int
(including String
, by hashing), while Class
is not.
In Java 8 you can create your own "switch-case" using lambdas. Here's a simple example on how to switch on object class (not the Class
object itself as you want, but this seems to be more useful):
import java.util.function.Consumer;
public class SwitchClass<T> {
private static final SwitchClass<?> EMPTY = new SwitchClass<Object>(null) {
@Override
public <S> SwitchClass<Object> when(Class<S> subClass,
Consumer<? super S> consumer) { return this; }
@Override
public void orElse(Consumer<? super Object> consumer) { }
};
final T obj;
private SwitchClass(T obj) {
this.obj = obj;
}
@SuppressWarnings("unchecked")
public <S> SwitchClass<T> when(Class<S> subClass,
Consumer<? super S> consumer) {
if (subClass.isInstance(obj)) {
consumer.accept((S) obj);
return (SwitchClass<T>) EMPTY;
}
return this;
}
public void orElse(Consumer<? super T> consumer) {
consumer.accept(obj);
}
public static <T> SwitchClass<T> of(T t) {
return new SwitchClass<>(t);
}
}
Usage example:
SwitchClass.of(obj)
.when(Integer.class, i -> System.out.println("Integer: "+i.intValue()))
.when(Double.class, d -> System.out.println("Double: "+d.doubleValue()))
.when(Number.class, n -> System.out.println("Some number: "+n))
.when(String.class, str -> System.out.println("String of length "+str.length()))
.orElse(o -> System.out.println("Unknown object: "+o));
If executes only the first matching branch, so for Double
object only Double
branch will be executed, not Number
branch. The neat thing is that typecast is performed automatically.
Well, according to the documentation, switch
only supports a limited set of data types:
A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types (discussed in Enum Types), the String class, and a few special classes that wrap certain primitive types: Character, Byte, Short, and Integer
My guess at why this is the case: because it would be difficult for the compiler to generate efficient code to switch
on something else than a relatively simple data type.