Java made several mistakes. It also made some questionable (yet often defensible) decisions. It's understandable. Type erasure was one I believe was a mistake. It's talked about in the article. Yes, you kept binary compatibility but you that created so many other problems such as not being able to use value types in generics. Notably, C# looked at that and said "nope". Type erasure is also hurting Valhalla here and the issue of value classes in generics is the second phase so is being pushed far into the future.
But a huge mistake (IMHO) was not having nullability part of the type system. You can still do this with type erasure.
Anyway, I read your comment as "nullability isn't complex" (paraphrased) but that's not the author's point. What's complex is having a value class and a regular class of every class and you don't necessary know which one you're dealing with at the language level.
C++ is a great example of this. You can create an object ont he stack or the heap and that's really what we're talking about with that proposal. And that's a nightmare. Combined with pointers it meant you never knew if you could free something or not and that ownership had to be passed around with vague comments like "// retains ownership".
Anyway, the whole article is a great tale of how difficult it is to retrofit things later and how difficult it can be to fix mistakes later (eg java.util.Date).
> Yes, you kept binary compatibility but you that created so many other problems such as not being able to use value types in generics.
This is often given as the defensible reason, but it's not even that true. Java 1.5/5 had several "breaking" changes in it regardless including the newly reserved 'enum' and a whole freaking memory model update.
And besides if any of your dependencies updated for all practical purposes you did, too, since you had to use a newer runtime to run their code regardless, it never really made sense to keep using an older javac out of spite
How would a non-nullable class field work in Java when it can be initiqlized by arbitrary imperative code that can read it while it's being initialized?
The type erasure version of this would look a lot like Hack [1]. So generic arguments would simply have a ? if they allow nulls eg List<?Point>. The list itself couldn't be null unless it was ?List<?Point>.
Now, one can argue that this is just smoke and mirrors with type erasure and it is but you can already put a Date into a List<Point> if you're so inclined because the JVM doesn't know the difference, hence type erasure. So this is no different.
I'm no JVM expert but from reading the article it seems like the chosen solution for value classes is to treat them all as a single L-type in the JVM where each primitive type is its own L-type. If I read the correctly, it means that if you have a Point value class then on the JVM level you'll be able to stuff any value class into there if you're so incline, just like with List<Point>.
Obviously we need to be concerned with fuzzing (moreso in C++) but here really we're just trying to have sensible defaults that aren't guaranteed because we can't design the language how we want from the ground up without making a new language.
Oh and there is a prosopal for this [2]. Personally, I prefer the Hack version.
> Now, one can argue that this is just smoke and mirrors with type erasure and it is but you can already put a Date into a List<Point> if you're so inclined because the JVM doesn't know the difference, hence type erasure.
And that's a massive problem that they're planing to solve with specialized generics.
> If I read the correctly, it means that if you have a Point value class then on the JVM level you'll be able to stuff any value class into there if you're so incline, just like with List<Point>.
I'm not sure what you mean. An L-type is an object reference. E.g. Ljava/lang/String, Lcom/org/CustomObject. The issue you're conflating is the erasure of List<T> to List<Object> and it's L-type Ljava/util/List.
> I prefer the Hack version
Hack/HHVM didn't have to worry about backwards compatibility.
---
The blog post does a pretty high level overview of the implementation, but my understanding of it is the following:
1. They are adding a new bytecode class flag for value classes. Bytecode descriptor for value classes are exactly the same (L-type).
2. Primitive wrapper classes will become value classes.
The difference from the CLR is that the JVM implementation is backward compatible with linked legacy bytecode. As they can accept a value class instance into their methods due to the same L-type. It's just additional metadata added to allow the JVM to stack allocate the class.
---
Separately, to handle generics:
1. Parameterized container classes will be flagged with a new bytecode to enable parametric attributes on initialization with the additional data of the parameters in the Constant Pool. Bytecode descriptor for classes are exactly the same with type-erasure, e.g. List<SomeType> erased to Ljava/util/List.
2. Initialization of parameterized classes are done with the additional metadata of the type argument stored in the Constant Pool.
3. The runtime does monomorphization of the parameterized class.
It doesn't seem too different to what the CLR does, i.e. runtime monomorphization. The difference is the JVM implementation is backward compatible with older type-erased code, i.e. the restrictions in Java due to type-erasure are exactly the same as before and the L-types don't change. It's just additional metadata added to allow the JVM to monomorphize the classes for performance.
---
In summary, value objects and specialized generics are backwards compatible with legacy bytecode. The JVM handles the compatibility. Of course, the newer bytecode is not forward compatible with older JVMs.
Look at how Go did it. Any value type has a defined 0 value, and any variable of field of that type is initialized to 0 by default. So any un-initialized non-null able value type field could have the corresponding 0 value.
That's worse than null. Now every object has an invalid state. It works for Go because Go is trying to keep language complexity low. It has no good design principles behind it other than that. Also the zero value of a reference is null so you still haven't answered how a non-nullable reference field would work.
Sorry, I misinterpreted your question to be about non-nullable value classes, not non-nullable classes more generally. For reference variables, it seems that the a priori best approach would have been to generate a compiler error if the field was read before it was written; since this ship has long since sailed with final fields, it seems that the same approach would have to be taken - the "not null able" guarantee only applies after the class is fully initialized, and you can observe it as null during initialization. This is probably not a huge problem, given that initialization code always has to deal with a class breaking its invariants, since they are only established at the end of initialization.
Regarding the 0 value choice in Go, I don't agree that this is worse than null. It simply applies a design constraint that is not usually very hard to satisfy - that the 0 value of your type must have well defined semantics.
The compiler doesn't solve the halting problem so it doesn't know whether a field is read before it's written.
If you allow a nonnull field to hold null at any point then it's trivial to leak that out and have nonnull variables holding null all over the program, which is obviously something to be avoided.
Maybe you can say that if you read a nonnull field when it's null, that throws an exception. That might have its own knock-on effects since nobody expects reading a field from a valid reference to throw an exception, but it might be the best way.
You don't need to solve the halting problem to prevent such access, not even close. You simply apply the same strategy as any other property you want to enforce - you prevent any use of the variable that can't be proven safe. You want to pass this in a call from the constructor to a function defined somewhere else where it might access a final/non-null able field that has not yet been initialized? Not allowed, only member functions that don't call other non-member functions allowed from the constructor.
Of course, as I said, this ship has long sailed for general Java classes. It would be far too big of a breaking change to add such a limitation today. However, we know for sure this could have been done in Java: this is exactly how record and now value classes work: any call that could expose un initialized fields is in fact disallowed.
I think you should require the code to be written in such a way that the compiler can verify the value is never read before initialization. There might be some subtleties in how to do that but I don't think it should be insurmountable.
It's completely insurmountable. It basically means you can't call any functions during initialization. Including the one that creates the object you want to point your nonnull reference to.
...which is exactly how value classes and (after this JEP) records work. You are not allowed to call any instance methods before the call to `super()` (which happens implicitly at the end of the constructor for value classes and for records, unless you explicitly call it), and you are not allowed to call `super()` without first setting a value for all of your fields.
Probably with hacks. Did you know a final field in Java can change its value? And I'm not talking about his reflection to make it non-final. With ordinary code only, you can read a final field before it's been initialized, so it still holds its default zero value. For example "final int x = calcX();" and have calcX print the value of x, it will be zero.
There's a whole bunch of specification language describing how constants aren't actually constant in specific situations.
I don't know Kotlin but I assume it does the same thing: until the non-nullable field gets initialized, it holds null and violates the type system.
Kotlin works around this by not exposing the backing field directly; a "field" becomes a "property" which has a getter and setter under the hood. Attempts to read uninitialized properties are compile-time errors. You can turn these into runtime errors using the "lateinit" keyword.
Kotlin still has a hole where you can run code in "init" blocks which are executed sequentially on object construction; in one, you can call a function that is defined after an unmodifiable property, and it will see the uninitiailized value.
Nothing stops me from sticking my dick into a blender either, yet it works fine for most of the things (the blender). Both Java’s notion of final and Kotlin’s nullability work great for 99.999 use cases and you really need to go out of your way to break them.
If you can get a null into a nonnull variable, it can propagate all over the program and people will be defensively checking for it everywhere - final is not like this.
`lateinit var` was not added to Kotlin to handle nullability, but to address specific Android design where system components like Activity cannot realisticly initialize fields in their constructors. Outside of Android it shouldn't be too commonly used.
"lateinit var" predates Android's adoption of Kotlin by years and was added because fields initialized after construction by frameworks are common in many contexts, but having nullability in the language makes that annoying.
But a huge mistake (IMHO) was not having nullability part of the type system. You can still do this with type erasure.
Anyway, I read your comment as "nullability isn't complex" (paraphrased) but that's not the author's point. What's complex is having a value class and a regular class of every class and you don't necessary know which one you're dealing with at the language level.
C++ is a great example of this. You can create an object ont he stack or the heap and that's really what we're talking about with that proposal. And that's a nightmare. Combined with pointers it meant you never knew if you could free something or not and that ownership had to be passed around with vague comments like "// retains ownership".
Anyway, the whole article is a great tale of how difficult it is to retrofit things later and how difficult it can be to fix mistakes later (eg java.util.Date).