So generics are completely a language semantic concept in case of Java, with no performance detriment/benefit. Of course more information can be used up for better optimizations as well, see Rust’s or C++’s generics/templates.
For example
void <T extends Foo> bar(T f);
will, under the covers, create a function that looks like void bar(Foo f);
If you create a class class Foo implements Supplier<Bar> {
Bar get() { return new Bar(); }
}
The method signature for `Foo` does in fact return a `Bar` type and not some `Object` type even though `Supplier<Bar>` would imply that is erased. The same applies to if that generic was used within the method parameters.That generic type information is, in fact, stored off at the class level and can be extracted through Java's reflection API.
This is how magic like
json.parse("[1,2,3]", new TypeReference<List<Integer>>{});
is getting it's stuff done. It's creating a new anonymous class of "TypeReference<List<Integer>>" which the json library can then use to introspect and pull the generic information out and know that it should create a `List` of `Integer` rather than of `Long` or `Double` or a `Set`.This makes generics more than just a syntactic construction in Java.
An easy way to verify: Are generics in Clojure, Scala, or JRuby the same as Java (the programming language)?
class Foo implements Supplier<Bar> {
Bar get() { return new Bar(); }
}
Bar isn't a generic in Foo though (if we remove the implements-clause, that is very clear). It's only a generic in Supplier, so I don't see where the implied erasure quite comes from.For example, if we did something like
class Foo extends HashMap<Foo, Bar> {
@Override
Bar put(Foo f, Bar b) {
super.put(f, b);
}
}
The `Map` signature for `put` is `Object put(Object, Object)` due to erasures. However, because you are extending and specifying the generics, the overriden method ends up reifying the type.All this is to say that generics end up being a little more than just syntactic sugar.
class Base<T> {
T f() { return null; }
}
class A<T2> extends Base<T2> {
@Override
T2 f() { return null; }
}
class B extends Base<Integer> {
@Override
Integer f() { return null; }
}
class C {
Integer f() { return null; }
}
Base has a parameter T which is erased in Base.A has a parameter T2 which is erased in A.
B because B is not a generic class and nothing is erased from B.
Base has the same shape as A; B has the same shape as C.
In particular if you override a method in a derived class, you're allowed to return an instance of a more specific class (in your example, Bar is an Object so it is fine). At runtime, your example is equivalent to something like
interface Supplier {
Object get();
}
class Foo extends Supplier {
Bar get() { return new Bar(); }
}https://docs.oracle.com/en/java/javase/17/docs/api/java.base...()
Edit: sibling comment by cogman makes the case more thoroughly.
Edit: also HN mangles the link to Class.getTypeParameters()
For what it's worth, this seems to be a common misunderstanding where generics are concerned.
Also, I don’t see erased generics all that big of a deal — if there was no reflection (which is not that common feature in languages) one could not even differentiate between the two models. Haskell also does complete type erasure for example.
As for another view point, it is often brought up on HN that what made the JVM language ecosystem so thriving is exactly the type erasure model. C# effectively forces their idea of generics onto the whole platform, while JVM languages are free to choose their own model. I don’t know how true is that, but empirically it does seem to sound true (F# does share its generics model with C#, and the CLR has a history of getting some attempts at being a target which is later dropped. Though I assume it is more often due to it simply being a smaller target)
You can, even without reflection, because the Java generic type system doesn't have the parametricity property; you can detect runtime types with cast/instanceof. Haskell doesn't have non-parametric operations, so erasure is not observable.
Your method signature may look like
void foo(Iterable<Bar> b)
which would make you think of vtables and casting performance losses.However, Java's JIT is fairly cleaver. It can tell that you always call `foo` with an `ArrayList` and it can tell that `ArrayList`'s iterator always emits a `Bar` Object. From there it can elate a lot of the type tables and virtual dispatch to just be direct dispatch. It does that at the cost of a couple of added runtime checks to make sure those assumption's aren't violated.
The impact of generics on performance has, for me, generally been pretty low. I've seen a much higher impact due to java's memory model (which is getting fixed with Valhalla).
This, btw, is how JavaScript's and indeed most high speed JITs function.
Whether you call that an afterthought? I'm sure the designers had this in mind, but at the same time they painted themselves into a corner somewhat with earlier decisions and a huge dedication to backward compatibility. Their work on threads on the other hand shows some real forethought and they were able to avoid most of the pitfalls that a few other language impl's fell into.