Why can't we switch on classes in Java 7+?
Asked Answered
E

6

7

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.

Elizebethelizondo answered 27/7, 2015 at 22:0 Comment(11)
Thx @Jashaszun, I was confused with Github MD.Elizebethelizondo
It is not allowed, however your easier work around would be to use c.getName() and in case, use class name insteadQuillon
Not an exact duplicate, but see #338706Miyamoto
a problem is classes are not compile time constants. you cannot use non-constant String either. to branch on dynamic values requires a different mechanism.Infare
Please note I am looking for an explanation not a workaround.Elizebethelizondo
Even if it compiled, your snippet would just add a syntactic sugar for instanceof and casting.If you want a real pattern-matching, take a look at Scala'sFalter
@Dici, my question excludes instanceof / inheritance considerations.Elizebethelizondo
Your snippet is basically a series of instanceof, or could be easily implemented with themFalter
But...but...polymorphism.Areola
Please, read more about the question. It can look like an easy one about polymorphism, but it is an advanced java question about classes, not instances..Elizebethelizondo
You can switch on the name of the class or better use the Visitor pattern.Lati
M
10

Interestingly, all answers so far are basically saying “because the specification says so”, which is correct but not really satisfying. Before Java 7, Strings 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…

Micco answered 28/7, 2015 at 8:45 Comment(3)
This is interesting but may not work as expected if you are mixing class loaders i.e. classes coming from different class loaders may be different and yet carry the same name.Magda
@Kiril: the name is only used for the switch, afterwards a reference comparison with a class literal is made. Class literals, like any other non-reflective use of a class, are always resolved in the context of the class in which they appear and are always resolved to the same class. And all occurrences of the same class name within one class are always resolved to the same class. At this level, there is no room for classes with the same name. These can only handled by dealing with ClassLoaders and Reflection manually.Micco
I am using the same workaround, but it is disappointing as it could be more efficient. I am still looking in this direction though : https://mcmap.net/q/1483540/-are-all-class-names-guaranteed-to-be-interned-in-all-jvm/693752Elizebethelizondo
S
11

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.

Soudan answered 27/7, 2015 at 22:7 Comment(3)
That's interesting. If A.class is not a constant expression, what is that ?Elizebethelizondo
More over why wouldn't a switch on class use System.identityHashCode more than hashcode ? There is no reason not to.Elizebethelizondo
@Snicolas: the identity hashcode is a value typically derived from the memory location (pointer value) of an object or, simply said, is different in every runtime. Therefore the compiler can’t create code relying on the identity hash code of an object. But of course, every class has a name and it wouldn’t be that hard to switch over the name’s hashcode.Micco
M
10

Interestingly, all answers so far are basically saying “because the specification says so”, which is correct but not really satisfying. Before Java 7, Strings 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…

Micco answered 28/7, 2015 at 8:45 Comment(3)
This is interesting but may not work as expected if you are mixing class loaders i.e. classes coming from different class loaders may be different and yet carry the same name.Magda
@Kiril: the name is only used for the switch, afterwards a reference comparison with a class literal is made. Class literals, like any other non-reflective use of a class, are always resolved in the context of the class in which they appear and are always resolved to the same class. And all occurrences of the same class name within one class are always resolved to the same class. At this level, there is no room for classes with the same name. These can only handled by dealing with ClassLoaders and Reflection manually.Micco
I am using the same workaround, but it is disappointing as it could be more efficient. I am still looking in this direction though : https://mcmap.net/q/1483540/-are-all-class-names-guaranteed-to-be-interned-in-all-jvm/693752Elizebethelizondo
J
9

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.

Jahncke answered 27/7, 2015 at 22:4 Comment(10)
Hey wait. That means that a long cannot be switched on?Soelch
@MCEmperor that means a long can't be swiched on. Which could make sense as switching tables are stored with arrays (of size int).Elizebethelizondo
@MCEmperor Give it a try and you'll see :P. No you can't switch using long.Deer
Thx for your answer, but I am looking for an explanation not a workaround.Elizebethelizondo
I don't think you can switch on an expression like SubCleassOfC1.class.getName(). You'll need to just use "SubClassOfC1".Fariss
@jashszun. Still a class is unique in the JVM, it can for sure be converted to a numerical ref.Elizebethelizondo
@Fariss Yes, of course. I for some reason forgot that calling c.class.getName() isn't compile-time constant.Jahncke
@Elizebethelizondo - a class literal may appear as different objects at runtime; each invocation of your method m(c) may be in a different class loader.Infare
@Elizebethelizondo The conversion to numerical ref has to happen at compile time. The only reason it's possible for strings is because the API for java.lang.String specifies the mechanism by which the hash code is required to be computed for all JVM implementations.Miyamoto
@Elizebethelizondo I didn't know how switch blocks worked internally. But the way you describe it—yeah, it makes sense. ;-)Soelch
S
2

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.

Stockholder answered 27/7, 2015 at 22:6 Comment(3)
I agree that it would be confusing if you want inheritance to be take into account, but that is not what the syntax of switch would suggest. Just reference equality, nothing else.Elizebethelizondo
Why it would it be a problem to deal with subclasses, as long as you only check for identity equality? If c is Object, do stuff, if it is String, do something else.Lamonica
You're both right - it's too late to try to guess why a language feature works the way it does.Stockholder
T
2

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.

Tetrameter answered 28/7, 2015 at 9:52 Comment(5)
of course the promise of a switch statement is to provide efficient lookup, ideally by a computed jump instruction, to find the right branch. your approach on the other hand has to evaluate the conditions sequentially and might even allocate objects if EA and inlining were to fail.Ruvolo
@the8472, I agree that this version would be slower (probably times slower). Actually I would not recommend anybody to use this approach. Nevertheless I think that OP cares about code clarity and beauty, not about performance overhead.Tetrameter
Great, but instanceof is too slow.Elizebethelizondo
@Snicolas, have you actually measured the performance or you just think that it's slow?Tetrameter
Sorry you are right, I am more used to performances of reflection on Android. Maybe on a JVM it's not that bad. SorryElizebethelizondo
T
0

You can now do this in Java 17+, using the new pattern-matching switch feature. To tweak your example:

public void m(final Object c) {
    switch (c) {
        case SubClassOfC1 c1 -> throw new IllegalStateException(c1.toString());
        case SubClassOfC2 c2 -> throw new IllegalArgumentException(c2.toString());
        default -> throw new NotImplementedException();
    }
}
Tremml answered 8/7, 2022 at 22:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.