- Mature support for async/await since 2012. Yes, Project Loom is coming, a decade later..
- Support for generic-aware value types (struct vs. class) and low-level features like stackalloc: very valuable for high-performance scenarios and native FFI. See for instance https://github.com/ixy-languages/ixy-languages. In comparison, Java doesn't even have unsigned integers. Yes, Project Valhalla is coming someday.
As well, debatable to some folks, but: properties (get/set); operator overloading; LINQ > Java streams; extension methods; default parameters; collection initializers; tuples; nullable reference types; a dozen smaller features
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.
One of C#'s biggest assets is Anders Hejlsberg of Turbo Pascal fame. He's been in charge of C# since its inception. He's done a very good job of keeping the language clean and concise, and generally ahead of Java when it comes to adopting features like generics and functional programming.
What’s up with Java in HFT stuff then? I’ve never understood this: from what I understand in order for it to work, you have to intentionally avoid doing many allocations, and that seems like you’ll be throwing massive amounts of the ecosystem (Javas biggest strength) away.