Hacker News new | comments | show | ask | jobs | submit login

Erased generics are awful compared to proper generics like C#. If you ever used both of them it should be clear as the sun. In C# you don't have to pass around the type in a method parameter and use reflection. I’m missing how you can claim that the languages on the runtime with proper generics are more difficult to adapt to other paradigms given the extremely fast evolution of C# and F# compared to Java that for example received closures something like 10 years after C#.

That's a relatively minor syntax issue, fixable with something like Kotlin.

All runtimes need to erase types at some level. Otherwise ArrayList<Foo> and ArrayList<Bar> would end up compiling identical versions if both Foo and Bar are reference-only types, which just wastes memory. At some level the compiler and runtime need to merge duplications - in C++ that feature is called COMDAT folding, or used to be.

.NET has had serious problems with code duplication in the past. Here's an excellent blog post by a Microsoft engineer on it:


Your comment is a bit misleading, because .NET has always only instantiated one version of a generic for all reference types. Even the article you posted backs this up, though I'm sure I've read the same on MSDN.

"... instantiations over reference types are shared among all reference type instantiations for that generic type/method, whereas instantiations over value types get their own full copy of the code."

Just about the only thing it could do better is to reuse the same instantiation for all value types of the same size.

.NET always preserves generic type information for reflection but there is code reuse when a generic type is used multiple times with different reference types, e.g. there is little overhead having List<object> and List<string>

Duplication exists with generics and value types, e.g. List<long> and List<DateTime> are entirely separate code. It's just a thing to keep in mind when mixing them with value types.

Don't Haskell and Scala also erase the types? If I remember correctly Martin Odersky even said erasure is better for some things in one of his videos/keynotes (?), but I'm not sure if I remember that correctly.

Haskell totally erased types and has a mechanism to recover that information as values. Both are way safer and easier to use than Java's versions.

Scala follows a similar route. It erases types, but lets you reify on demand in form of TypeTags.

Except not:

    x match {
      case _: List[Int] => 1
      case _: List[Char] => 2
      case _: String => 3
This used java's semi-erased class tags and doesn't report a TypeTag constraint in its type. It will also return 1 when x is of type List[Char] since partial erasure means that the first two patterns end up being identical. The compiler will warn you about this situation, but generally it shows up all the time for various reasons. Super bad news.

That all said, the TypeTag system could be very nice someday. Especially if asInstanceOf were dropped eventually or, better, relied upon TypeTag.

Scala does. It's annoying in pattern matching clauses.

and yet it prevents you from shooting yourself in the foot at run time (if you have `-Xfatal-warnings` enabled), so the compiler has your back.

When Dotty and Java 10 land many annoyances will go away thankfully.

Java maintained compatibility between non-generic containers and generics.

Look at C# on the other hand and you see they had to add entirely new types which means that the .NET framework has an ugly split between APIs based on the old container classes and those based on the new container classes. That fits the trend that C# is a better language than Java, but Java has better class libraries to work with.

But this split happened in 2005. The ecosystem fully migrated to generic containers almost instantly, and you won't find the old containers being used anywhere.

They took a risk, and it paid off. In my opinion, C# is both a better language and has better class libraries.

The be fair, C# was a lot less entrenched at that time than Java was, even though I think Java could have made the same step at the time. But the Java maintainers probably had good reasons for that decision as well.

As for better class libraries, I found the .NET BCL to be excellently designed and thought through. It's also very consistent throughout. Now, parts of the FCL, like System.Windows.Forms are another matter ...

There's still some warts, such as there not being an ISet<T> before .NET 4, but Java's standard library has its share of quirks and historical weirdnesses as well. And as libraries age there are always old ways of doing things you can never really remove, and newer ways that are better. None of the two is as bad as C++, but depending on what you do you can stumble around in a swamp of old APIs for a while before finding what you're actually supposed to use.

> In C# you don't have to pass around the type in a method parameter and use reflection.

I know - but I'm not familiar with the situation where I'd have to to that in java. Do you mean e.g.

    Foo(object x)
      if (X is List<int>)

The canonical example in the standard library would be this one from the entire collections framework:

    public Object[] toArray()
    public <T> T[] toArray(T[])
You cannot turn an ArrayList<T> into a T[], for example, without passing a T[] into the function so that you can grab the type from the passed parameter at runtime. C#, which retains the generic information at runtime doesn't need this so you can just do

    public T[] toArray()
The other place I've seen this come up is with Exceptions- you cannot catch a generic Exception. It's pretty annoying now that Java 8 has streams because you cannot have a checked exception abort out of processing a Stream unless you either 1) catch Exception (rather than the specific subclass) and deal with everything or 2) catch the checked exception in the lambda, wrap it in a runtime exception, rethrow it, and then catch the wrapped exception.

Basically every time you need to instantiate a type passed as a type parameter. You either need to pass the appropriate class literal or a factory function. And if you need ro create an instance of a generic type parameterized by a type parameter (eg. Collection<T>) you need a factory or else resort to raw types and/or ugly casting.

Do you mean things like this (C#)?

    void AddAnItem<T>(List<T> aList) where T : new()
       aList.Add(new T());
Or for instantiating generic types:

    public TColl CreateACollectionAndAddAnItem<TColl, T>(T item) 
       where TColl : ICollection<T>, new()       
       TColl aList = new TColl(); 
       return aList;

    // Usage 
    List<string> myList = CreateACollectionAndAddAnItem<List<string>, string>("hello");

Both those examples are exactly the type of thing you cannot do in Java because of type erasure. For instance, your first example would need to be:

    <T> static void addItem(List<T> list, Class<T> type) {
        T item = type.newInstance();

Can't that be solved by passing these types around under the hood, but allowing the language to sugar them out when they are known? I thought this kind of half-erasure was how some languages already operated on runtimes that erase generics.

That's basically what Java does. Java class definitions contain the full, reified type information, but when the runtime loads the class, those types are lost. Only the compiler uses them.

There are some ways to introspect generic types in Java, but you need a concrete binding. For instance, if a method returns List<Integer>, you can in fact see that it returns List<Integer> and not just List. Method.getGenericReturnType() would return a ParameterizedType with a List raw type and Integer type arguments. But that requirement for it to be a concrete binding means it's not really helpful from the context of writing a generic class or method in the first place.

Using the above example, I'm not sure how you could desugar that without having the type known to the runtime. The generic method is going to be the same code no matter the type provided, but the type provided is necessary in order to know the type to construct. So either the runtime must provide the type, or it must be given as a parameter.

Additionally, glossing over the issue like that creates a huge trade-off. Now programmers must build a mental model of when the compiler can and can't do the type binding. The difficulty of building such a mental model accurately is one of the central complaints against Rust's borrow and lifetime checker(s).

You don’t have to in Kotlin either, and that’s on the JVM.

IIRC Kotlin can only do it on inline functions on type Params explicitly marked reified

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact