I haven't given it a ton of thought but I thought that apart from reflection it would actually be nicer if the runtime erased types. I.e. at runtime a type should just be either a stack/copy/move type or a reference type. So erase all reference type to objects.
It feels like it's the job of the compiler to handle these. Obviously at runtime you can get some extra safety guarantees from reified types, but at the cost of languages on the runtime being more difficult to adapt to other paradigms etc.
I imagine a runtime that doesn't have strong opinions about types to be easier to make type level language changes for (i.e. the language(s) can evolve without the need for runtime changes), and it should also be easier to support entirely different type systems.
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:
http://joeduffyblog.com/2011/10/23/on-generics-and-some-of-t...
"... 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.
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.
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.
When Dotty and Java 10 land many annoyances will go away thankfully.
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.
They took a risk, and it paid off. In my opinion, C# is both a better language and has better class libraries.
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.
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>)
...
else
...
} 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. 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();
aList.Add(item);
return aList;
}
// Usage
List<string> myList = CreateACollectionAndAddAnItem<List<string>, string>("hello"); <T> static void addItem(List<T> list, Class<T> type) {
T item = type.newInstance();
list.add(item);
}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).
That's covered by a sibling valhalla feature called 'generic specialisation'. See http://openjdk.java.net/jeps/218.
My understanding of generic type erasure is that it is more of a language issue than a VM issue. Nothing in the VM prevents reified generics, but when generics were introduced in Java 5 they decided to use type erasure in order to avoid having to produce a whole new collections standard library or break compatibility with existing code.
They could have simply left the old library, deprecated it, and added in their genericed one as a a new package. It had to be modified anyway.
Type assertions have no ability to recurse. RN you assert both classes are a UTF8 value in the constant pool. Or really 1 class is a UTF8 value, which the compiler know the first one is.
But with Generics, you may not know what it holds at runtime so this doesn't work. You'd have to constantly rebuild the constant pool (Slow) each time a new _kind_ of generic is used.
So either a new type+byte code would have to be introduced, or the type assertion instruction would have to become recursive. The later is a breaking change.
Also interfaces break this b/c those are class specific. So a Lorg/my/company/myClass$5; and Lorg/your/company/yourClass$0 might both implement the same interface. But that interface information isn't in the collection class, but those to class's class files. So really it just needs new byte code.
There's something seriously wrong with the type system when you want "instanceOf T" checks or "new T".
Instantiating a generic type is like a function call at compile time, where the type arguments are substituted for type parameters in the body of the generic type. It's a variable at compile time, but it's a concrete type at runtime for any version of the code that can execute.
Also, why do you consider the introduction of these new bytecodes as breaking changes for the JVM? Backward compatibility has always been one of the highest priorities for the Java architects.
No and yes.
No, it doesn't really matter for dynamically typed languages (just use Object for generic types).
Yes, it matters for statically typed languages. One of the factors for Scala .NET abondonment was the difficulty in creating a interoperable advanced type system in a fully reified environment.
IMO, full type erasure (like C/C++) is the right way to go. Java erasure is weird only because it didn't do it completely, not because they did it at all.
At which point any call to the underlying environment fails with a type error. Sorry your List<Object> is not and will never be a List<int>, trying to pass it as one wont do. Either you provide a way to construct a generic type with the right type parameters in the dynamic language or you accept that you can only inter-op with a limited subset of the underlying environment.
Of course that also gets ugly. You now have to deal with the fact that you not only have a List but several incompatible List<T> floating around, adding the contents of two lists suddenly includes the question what sort of list to return, List<Int>,List<Double>,List<Number> or an error? In a type erased context the answer is always the same, you return a List<Object>.
This line is disproven by all the dynamic languages that happened and failed on the CLR.
I would sincerely doubt they're considering breaking back-compat in Java 10 (that is, making it so older bytecode targeting something <10 cannot run on 10+).