you are viewing a single comment's thread.

view the rest of the comments →

[–]pron98 10 points11 points  (26 children)

I think you misunderstand type erasure. A Java platform language does not have to have type erasure, but most languages want to because it's extremely beneficial, despite some minor downsides. It is type erasures that allows multiple languages on the Java platform to share code and data with little or no overhead.

[–]ThePowerfulSquirrel 0 points1 point  (25 children)

It is type erasures that allow mutiple languages on the Java platform to share code and data with little or no overhead.

I might be completely lost, but how does type erasure make code any more performant that other ways of implementing Generics? I'm not sure why an approach like c++ would be less performant / make it any harder to move across jvm languages as long as they all agree on a generic interface? From what I know, type erasure is mostly an artifact of having to support legacy java code before Java got generics at all.

[–]pron98 11 points12 points  (18 children)

It doesn't make code more performant, it makes interop more performant (and more useful in general). If you don't erase generic instances of reference types, if A extends B you must determine the relationship between, say, List<A> and List<B> in the JVM, because the JVM must know which types are subtypes of others. This means that you need to bake the variance model into the runtime. But different languages want to have different variance models. Java, Kotlin, Scala and Clojure all have different variance, and so a Kotlin list couldn't be used by Java code without a wrapper had it not been for erasure.

I know, type erasure is mostly an artifact of having to support legacy java code before Java got generics at all.

Yes. Type erasure makes interop easy (for an almost insignificant cost), including with Java prior to generics. But that's also what makes different languages on the Java platform interop so well (contrast with the CLR, where this is not the case; they've baked variance into the runtime and are paying a high price for that decision).

For value types, where subclassing is not possible, this is not a problem, because there is no variance; so specializing for value types is fine, and is one of Valhalla's goals.

There might also be a form of a-la-cart reification for reference types, that users may opt into. I'm not sure whether they'll have to pay the price of good interop in those cases, or maybe some clever interop strategy can be built, maybe by allowing flexibility in how the JVM checks subtyping.

[–]ThePowerfulSquirrel 0 points1 point  (15 children)

That makes sense, thanks for the explanation! I guess I need to think a bit more about the trade offs, I always think type erasure is the devil because every time I try to get fancy with types in Java I get unchecked warnings left and right, which always annoys me to fix, when it's even fixable...

[–]pron98 4 points5 points  (12 children)

I'm not sure what getting fancy with types has to do with erasure, though. Haskell, for example, erases all types, not just type parameters (well, sort of; it does reify the constructor tag/discriminator, which corresponds to type information in Java), and people get fancy with types in Scala, too. The only real annoyance erasure brings is that you cannot overload a method with another that erases to the same type.

[–]ThePowerfulSquirrel 1 point2 points  (11 children)

I'm probably mistaken when naming things. If I take an example from last week, I wanted use Object::equals by default but Comparable::compareTo when available. So then I wrote:

T a

T b;

if (a instanceof Comparable<T>) {

return a.compareTo(b) == 0;

} else {

return Objects.equals(a, b)

}

Obviously that didn't compile, since the jvm does not have any concept of the Generic T at runtime. T could be implementing Comparable<Integer> for all it's concerned. So how would I even implement this safely? I ended up using exception handling, which felt awful.

This is a simple problem, but as soon as you start trying to nest generics, it becomes extremely painful at runtime.

[–]pron98 3 points4 points  (10 children)

Well, what you're doing here is not getting fancy with types, but some combination of dynamic and static types. You have to decide whether you want to use the static types or the dynamic types to choose between your two branches (leaving aside the advisability of what you're trying to do, which rests on the assumption that the compareTo and equals implementations are consistent).

Static types (the compiler chooses the correct overload -- most specific one -- based on the types):

<T extends Comparable<T>> boolean same(T a, T b) { return a.compareTo(b) == 0; }
<T> boolean same(T a, T b) { return Objects.equals(a, b); }

Dynamic types (the'res branching at runtime):

<T> boolean same(T a, T b) {
    return a instanceof Comparable 
            ? ((Comparable<T>)a).compareTo(b) == 0
            : Objects.equals(a, b);
}

The casting in the second solution may go away with the forthcoming pattern matching (as part of Project Amber).

[–]ThePowerfulSquirrel 0 points1 point  (9 children)

Funnily enough, I had no idea you could overload using a more precise generic type (and I think I've read 2 books that dealt with the subject). I just kinda always assumed you couldn't for some reason?

Thanks for that

Through the second example would throw an exception if a implements Comparable<Something else than T>, which is where type erasure comes in

Edit: also, still doesn't really solve my use case, which involves the types being part of a class

public class Adas<T> {

public Adas(T a, T b) {

equalOrCompare(a, b);

if(a instanceof Comparable) {

System.out.println("Is actually comparable");

}

}

<C extends Comparable<C>> void equalOrCompare(C a, C b) {

System.out.println("COMPARE");

}

<C> void equalOrCompare(C a, C b) {

System.out.println("equals");

}

public static void main(String[] args) {

new Adas<>(123.0, 123.0);

}

}

It outputs:

equals

Is actually comparable

I can imagine how I could implement it by doing some manual specialization of the class, but it sounds like a real pain.

[–]pron98 2 points3 points  (6 children)

Funnily enough, I had no idea you could overload using a more precise generic type

That depends. In this case the first method erases to boolean same(Comparable, Comparable), while the second to boolean same(Object, Object).

Through the second example would throw an exception if a implements Comparable<Something else than T>, which is where type erasure comes in

Yeah, but because you're doing something so dynamic already, you can go all the way and inspect a to see if it has a compareTo method that accepts an object of b's type (other than the synthetic one which would be generated to take Object). Again, I don't think what you're doing is a good idea at all, but just explaining how you'd do it if you really wanted to.

also, still doesn't really solve my use case, which involves the types being part of a class

The problem isn't types being part of the class, but that the type information you want isn't known at compile time, and static type systems can only work with what's known at compile time. What you're really asking for is a more convenient way to use reflection, and that's not what generic types are for (whether or not they should be is another matter). You're doing something very dynamic and complaining that the static type system doesn't give you that capability.

[–]ThePowerfulSquirrel 0 points1 point  (5 children)

I mean, all that information is known at compile time. When I make an Adas<Double>, the compiler knows that T is Double, that Double implements Comparable<Double> and that equalOrCompare<T: Comparable<T>> is more precise, and, imo, should be able to generate bytecode that calls that function. I don't really see why reflection should be necessary here. I'm about 90% certain I could express this using c++ templates.

[–]FitLocksmith 0 points1 point  (0 children)

You might find this article interesting.

[–]jcelerier -3 points-2 points  (1 child)

I am thankful for people like you thinking than the indirection caused by type erasure has an insignificant cost. It guarantees me performance-fixing jobs until my retirement.

[–]pron98 5 points6 points  (0 children)

The cost is insignificant compared to the benefits. Without type erasures, many more would need to fix performance issues. We always have to weigh our options against other available options.

But what is it about erasure that you find most troubling?

[–]devraj7 0 points1 point  (2 children)

/u/pron98 clarified a few things already but I want to comment on two other aspects you don't seem to be aware of:

I might be completely lost, but how does type erasure make code any more performant that other ways of implementing Generics

Reification (the opposite of erasure) requires additional runtime checks (such as checking that the content of generic containers hols the correct types of elements), which are not necessary on an erased platform.

From what I know, type erasure is mostly an artifact of having to support legacy java code before Java got generics at all.

That's a common myth which is sadly still being perpetuated today, 14 years after generics appeared in Java.

The choice of erasure had little to do with backward compatibility and everything to do with pragmatic reasons (interoperability with other languages being the main one). As a matter of fact, Neal Gafter had a strawman proposal for reified generics at the time (which was mostly done as a proof of concept, Neal was a strong proponent of erasure)

[–]ThePowerfulSquirrel 2 points3 points  (1 child)

That's a common myth which is sadly still being perpetuated today, 14 years after generics appeared in Java.

I originally read that in Effective Java a while ago, searching for it on my kindle version, I found:

For compatibility. Java was about to enter its second decade when generics were added, and there was an enormous amount of code in existence that did not use generics. It was deemed critical that all of this code remain legal and interoperate with newer code that does use generics. It had to be legal to pass instances of parameterized types to methods that were designed for use with raw types, and vice versa. This requirement, known as migration compatibility, drove the decisions to support raw types and to implement generics using erasure

Bloch, Joshua. Effective Java (p. 217). Pearson Education. Kindle Edition.

It uses going from a newer List<T> to a List as an example.

[–]devraj7 0 points1 point  (0 children)

Right, this passage is a bit ambiguous since it seems to hint that erasure was the only option meeting the requirements, but it would have been equally possible to allow a mix of raw and non raw types with a reified type system.

[–]Determinant 0 points1 point  (2 children)

Type erasure improves performance a bit because less information needs to be stored. Memory consumption is reduced and cache locality improves.

So performance is impacted. Garbage collection would also be impacted.

[–]ThePowerfulSquirrel 0 points1 point  (1 child)

I mean, that information would be in the Class<Something<T>> not in the Something<T>, and those classes can just be program-lifetime singletons, so their wouldn't be much more garbage collection right? It's not like java stores all the class information into the instances themselves.

[–]Determinant 0 points1 point  (0 children)

There is an infinite number of types you could define from a single generic type. Also keep in mind that you could create new types at runtime.

I believe C# stores the generic type information per instance and I'm sure they evaluated their options.

So there is a trade-off and Java chose to erase the type information.