Java generics have a couple of unfixable problems.
For one, you can have List<Foo> and List<Bar> both be passed to a place where Object is expected. But getting them back from that place and trying to recover what was known at creation time, would normally be done with a cast. But List<Foo> and List<Bar> are not runtime distinguishable from each other. Such a polymorphic cast in the code is just syntactic sugar; you'll get a compile-time warning, but no runtime check! E.g. You can get a naked List and cast it to a List<Bar> and then go ahead and try to use it like a List<Bar>. It will only fail when you get an actual non-Bar thing out of the list. It pointedly won't fail if you take your List<Bar> and pass it to other generic code that does more generic stuff with it. And it won't fail if the list is full of nulls, is empty, etc. In essence, it's completely dynamically typed at that point. The static generic types are lies. In reality, all generic Java code compiles down to non-generic code with runtime casts everywhere. That costs performance and means that you can screw up.
Second, erasure also means that you cannot be polymorphic over primitive types in Java, as the VM doesn't support that in the bytecode. So you can't write really basic stuff like Vector<T>, array sorting code, and now, closures and lambdas, that manipulate primitives. All that has to be duplicated, once per primitive type, and for objects. Or you have to box stuff. So they added implicit boxing (autoboxing) so you don't have to type those characters. But they are there in the code. You're stuck with either duplicating for performance (hand-doing template specialization, if you will), or just creating an assload of garbage.
[1] I say "along the lines of". I designed Virgil's generics system not knowing C#, but working from what I knew from ML. It ended up with a lot of the same choices, but I mapped ML's "unit" onto "void" and that works out nicely for zero-arg and/or zero-return functions. You can even have an Array<void> in Virgil! And none of it creates boxes or introduces unsafe casts.
The issue with primitive types and boxing is certainly noted. Hopefully Valhalla will address it and more.
The problem with reified generics is that the same variance model must be adopted by all guest languages on the runtime. Hence you basically don't see any guest languages on the CLR, and efforts by languages such as Scala to port to the CLR failed due to problems interoping with C#. I think one of the JVM engineers "pron" has discussed this multiple times.
I think this is a fair observation, but it really boils down to "a dynamically typed compilation target is an easier target", which isn't all that surprising.
> such as Scala to port to the CLR
I am not as familiar with Scala's saga here, but I've heard multiple conflicting reports from Scala insiders, so I think this is a more complicated issue than just the generics model of the CLR.
Your second point is true though, and the fix is something that requires at least 6 PhD’s combined, but they are working on it.
With significant experience with reified generics, there's just a lot of patterns that you can't do in Java. I wrote a whole PLDI paper about it back in 2013. Reified generics mean you can do stuff like ad-hoc polymorphism without direct language support. Ironically enough, the little trick of hiding type arguments with subtyping but getting them back with casts is a powerful dynamic tool.
What I find strange in these erasure-reification “wars” is that so many other languages get a pass. And I sort of understand that, languages without a runtime seldom have reflection, or only in some primitive form, and most language on top of a runtime are dynamically typed. So outside of guest languages, Java and C# are unique in this aspect, and Java does use reflection very heavily, where I can imagine that restricted access to the whole type may be a hindrance.
Virgil doesn't have reflection, so not a lot of metadata needed at runtime. It does full monomorphization (like MLTon), so you can't have polymorphic recursion. Of course that could go exponential, but in practice I see something like 20%-30% space overhead. I have a tendency to use polymorphism for really generic datastructures, like lists, vectors, maps, but I use tuples a ton and now I added algebraic datatypes. It's a lot of fun and the compiler generates pretty good code and compiles fast--full optimized bootstrap in < 400ms).
Of course everyone worries about exponential code blowup. I have a slightly broken implementation of specialization-up-to-machine-rep, but that's not turned on because of some bugs. I think that's the way I'll go in the future.
It is certainly possible to do a type-passing scheme. The built-in interpreter can interpret polymorphic code using dynamic type environments, but can also run monomorphized code. The interpreter is slow.