Reputation: 1255
Suppose we have the following hierarchy:
sealed interface Animal permits Dog, Cat, Bear {}
record Dog() implements Animal {}
record Cat() implements Animal {}
record Bear() implements Animal {}
And we want to create a method which pattern matches over a Class<T extends Animal>
.
Why am I prevented from doing this? Is it because of erasure?
So if I do the following it won't work:
static <T extends Animal> String makeNoisePatternMatch(Class<? extends Animal> cls) {
return switch(cls) {
case Dog.class c -> "WOOF";
case Cat.class c -> "MEOW";
case Bear.class c -> "ROAR";
};
}
But I can still do this (which works):
static <T extends Animal> String makeNoise(Class<T> cls) {
if(cls.equals(Dog.class)) {
return "WOOF";
} else if(cls.equals(Cat.class)) {
return "MEOW";
} else if(cls.equals(Bear.class)) {
return "ROAR";
} else {
throw new IllegalStateException("Unknown state");
}
}
IntelliJ actually hints that you should change the above to use switch and then produces a the switch expression above (which does not work).
Again - my question is: Why am I prevented from doing this? Is it because of erasure?
Upvotes: 2
Views: 1589
Reputation: 103273
It has mostly nothing to do with erasure.
Class
is not magical in this regard. Class<? extends Animal>
is no different from List<? extends Animal>
- the JVM does not know that instances of Class
represent specifically, well, classes, and that therefore one could feasibly want to pattern-match on it.
Separate from that, generics is a figment of the compiler's imagination: At runtime you can 'break' generics. This:
String x = foo;
System.out.println(x.getClass());
Cannot possibly print anything other than java.lang.String
(or throw an NPE if x is null
) - because the JVM wouldn't let it happen. Any bytecode that tries to mess this up won't pass class load verification.
However, with generics it doesn't work that way:
List<String> x = foo;
Object first = ((List) x).get(0);
System.out.println(first.getClass());
The weirdness on the second line is just to avoid ClassCastExceptions - the above code can certainly print Integer.class
or whatnot: You can get javac to compile code that 'breaks' generics and the JVM doesn't care.
Hence, a method that accepts a Class<? extends Animal>
can be called with Integer.class
if you ignore enough warnings and write silly code, so in that sense 'erasure' is to blame here, but, the sealed nature of things means there is a silent extra case
for dealing with unmatched scenarios (which throws an exception), because you could also recompile Animal.java
, adding a 4th animal to the sealed list, and fail to recompile the code that contains makeNoisePatternMatch
and you run into the same scenario.
The same principle could be applied there, so, no, erasure really isn't the problem here.
The explanation is much, much simpler!
The things you can pattern-match is a hardcoded list, and 'match on an instance of java.lang.Class
isn't on the list.
NB: It would be more correct to write if (cls == Dog.class)
- class instances are singletons (that is, a.equals(b)
and a == b
, if both a and b are instances of j.l.Class
, neccessarily always give the same answer, but a.equals(b)
makes it slightly more 'hidden' that this is the case).
NB2: Sounds like you should file a bug with intellij.
EDIT:
I thought of something. I think this might work. Switching to Number
for easier testing, which isn't sealed (but the sealed nature of this has no bearing on anything, the JLS isn't aware of the notion of sealed when specifically using Class<?>
, only for instances themselves.
Class<? extends Number> n = int.class;
String type = switch (n) {
case Class<?> c && n == int.class -> "primitive int";
case Class<?> c && n == Integer.class -> "boxed int";
default -> "unknown";
};
System.out.println(type);
And this works!
But, some unfortunate notes:
default
case cannot be avoided; the compiler doesn't know that it would be reasonable to generate it (where the default
case throws something) given that the input is sealed. It doesn't know anything about j.l.Class
.Class<?> c &&
part cannot be avoided either, as far as I know.This therefore looks.. just like an unwieldy, confusing take on a chain of if/else
statements (and wouldn't perform any better either), and therefore I don't recommend doing this.
Upvotes: 4