3 ms·
You can get CCEs without explicit casts or compiler bugs though. javac will catch the most glaring examples of generic type erasure with a warning. List<Stri
by dwohnitmok 7y ago
You can get CCEs without explicit casts or compiler bugs though. javac will catch the most glaring examples of generic type erasure with a warning.
List<String> myStuff0 = new LinkedList<>();
List eraseMyStuff = myStuff0;
// Warning here
eraseMyStuff.add(1);
// CCE here
String myString = myStuff0.get(0);
However, if you obfuscate it a little javac doesn't know about it anymore.
List<String> myStrings = new LinkedList<>();
List<Integer> myIntegers = new LinkedList<>();
myIntegers.add(1);
Stream.of(myStrings, myIntegers)
// Wildcard capture
.map(x -> x)
.reduce((x1, x2) -> {
// Would expect this line to blow up either at compile time or at runtime with reified generics
list1.addAll(x2);
return x1;
});
// Blows up with a CCE
String result = myStrings.get(0);
The above is trick that was featured in some HN post from a while ago (maybe a year or so ago? Need to dig it up...) and is not an original idea of my own. It's essentially combining wildcard capture conversion with type erasure to delay a CCE away from the point of the problem. As I remember I think this isn't a compiler bug in the sense that this follows the Java spec.
I thought, however, there was an easier trick to get a CCE using type erasure of generics without warnings and without having to involve capture conversion, but it looks like I may have just been wrong about that as my efforts all ended up getting warnings from javac.
Of course whether any of this actually matters is a separate issue altogether. Java is still a workhorse language that powers a ton of software.
As for weirdness, I can't think of any other language with generics that preserves runtime type information without a way of preserving that for generics as well. Most as you point out either do full erasure (most stuff in the ML-inspired family) or either fully preserve all the information (of course all the .NET languages)/let you opt-in to preserving generics (Scala to some extent with TypeTags) or just forgo generics (Go).
For example in OCaml if you try to do an unsafe and incorrect cast you segfault. IIRC in GHC you end up with a crazy incorrect value (e.g. unsafeCoerce [1, 2, 3] :: Int results in some messed up integer).
My question was me thinking out loud; I was guessing that CIL bytecode has a instruction analogous to checkcast in JVM bytecode that you can just omit and then you can freely cast at will, potentially wreaking havoc if you cast incorrectly. I don't know if that's true. Basically I was separating the verification of type information from the storage of it at runtime. If the CIL separates them then the downcast trick works. If it doesn't the trick doesn't work.
Also in a way this unsafe cast is really just a run around reified types to begin with. I guess the only difference is that you still have storage of the types so you can do runtime reflection. On reflection, I can certainly see how this complicates implementing things like higher-kinded types (because you sort of end up implementing half of type erasure yourself) but I'm not sure it complicates marshalling ordinary data.
- pron 7y ago> You can get CCEs without explicit casts or compiler bugs though Using raw types is cheating. It's essentially using a different language and, as you say, the compiler will tell you that you are. > However, if you obfuscate it a little javac doesn't know about it anymore. It's a bug (rather, a few bugs): http://wouter.coekaerts.be/2018/java-type-system-broken http://wouter.coekaerts.be/2018/java-type-system-broken (I assume that's where you got the example from) You shouldn't get a CCE without explicit casts, and if you do, that's a (soundness) bug in the Java compiler and/or spec. Those bugs aren't always fixed quickly when they're not very important in practice (if it's determined that it's hard to hit them by mistake, which is why it's taken 15 years to find them). > Would expect this line to blow up either at compile-time or at runtime with reified generics Reified generics have no impact at compile time. Everything a language with reified generics knows at compile-time, a language with erased generics also knows. However, a runtime with reified generics and a similar compiler bug would, indeed, throw at this line at runtime. > As for weirdness, I can't think of any other language with generics that preserves runtime type information without a way of preserving that for generics as well. There are good reasons for doing it the Java way: so that you don't bake a language's particular variance strategy into the VM. There are also good reasons for doing it the C#: it's slightly more convenient for that one language. I can't think of any other language with variance and a runtime type system that reifies generics for reference types. When all you have are two instances, it's hard to say which of them is weird. > If the CIL separates them then the downcast trick works. If it doesn't the trick doesn't work. I don't know CIL, but looking at how languages behave on .NET to interop, I wouldn't think that can work. It would totally break the safety of the runtime type system if you could cast without a typecheck. > I guess the only difference is that you still have storage of the types so you can do runtime reflection. BTW, you can reflect on generic type arguments in Java, as well (https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/reflect/ParameterizedType.html https://docs.oracle.com/en/java/javase/13/docs/api/java.base...). When you reflect on a method that returns a List<String>, it will tell you it returns List<String>, but you can't get such a Type from an instance of that list; you can only get a Class.