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

An Optional is just a tri-valued null (null, None, and Some), so no.

It'd be nice if Java had a concept of a never-null reference (like a C++ reference vs. a C++ pointer), but the @NotNull annotation wasn't enforced the last time I checked.

Also, there's no way for an object to express that invariant because encapsulation is so weak. Given only this constructor (and no reflection):

   Foo() { foo.bar = new Bar(); /* foo.bar is final; Bar() does not throw */ }
callers can still get an instance of Foo with bar set to null.

Anyway, null handling in java somehow manages to be worse than C, where you can at least inline a struct into another, statically guaranteeing the instance of the inlined struct exists.

I can't think of another statically typed language that screws this up so badly. It just keeps getting worse with stuff like Optional and @NotNull.

(Disclaimer: I haven't followed java for 4-5 years; it's possible they finally fixed this stuff.)




Wait, javas Optional is a reference type so it can be null? Doesn’t that almost defeat the purpose of it?


Arguably yes, but that doesn't stop people using it.

Basically Java had nulls from the start. A decade or so later some people who didn't like nulls introduced their own Optional type, as a third-party library. Enough people liked it that Optional was added to Java's standard library.

But as it's just an object, it can be null. Some null avoidance enthusiasts also use third-party @Nullable and @NotNull annotations, which some automated code checking tools will attempt to verify during compile/test.


It gives you the ability to treat nulls as something-to-fix.

In a team without Optionals, every time you touch a null that you didn't expect, you have to decide "Is this deliberately null, or was it a mistake?" Without that knowledge, you don't know whether your code should assert against the null, or allow it to pass through as a valid value.

With Optionals, it becomes much simpler to cut through that nonsense. A null is a bug and you fix it (with the exception of json at the boundaries of your system, etc.) If you do find a value where you change your mind about its nullability, changing it to/from Optional will give you compile errors in exactly those other parts of the code that you now have to check/change.


Yup. Your IDE will likely highlight it as an issue, but it's totally legal to return a null Optional. There's nothing special about it, it's just a wrapper class.


Did the project to add value types to Java (I’m sure I heard of it a decade ago) never finish?


Not yet, that's Project Valhalla iirc. It's coming along but hasn't been merged yet. I don't believe it's even a preview feature within the JDK yet.



Kinda.

Every non-primitive is nullable in Java. Adding Optional doesn't/can't change that.

You can have a gentlemen's agreement to prefer None to null.


It doesn’t defeat the problem in theory, but in my experience it does in practice. I’ve never come across an NPE on a nullable reference even in development - it would have to be the result of a really fundamental misunderstanding of the concept.

YMMV. Obviously it depends on your teammates.


Fortunately, Uber made tooling for languages with broken type systems

* https://github.com/uber/NullAway

* https://github.com/uber-go/nilaway


Lombok, Error Prone, and Kotlin also have their takes on the problem.


> I can't think of another statically typed language that screws this up so badly. It just keeps getting worse with stuff like Optional and @NotNull.

Java might be the only language where a simple assignment `x = y` can throw a NullPointerException (due to auto-unboxing)


Assuming that was the only constructor you defined on class Foo, and you used this.bar instead of foo.bar (latter won't compile), then the caller can't possibly get a Foo with bar set to null (except by reflection, and there are ways to prevent that). Moreover, even if new Bar() did throw an (unchecked) exception, the invariant would still hold, since Foo would rethrow the exception. This has always been the case, as far as I know.


Doing it requires two threads.

Thread A sets a shared reference to a newly allocated and null initialized reference to Foo:

   shared = new Foo();
While that's running, thread B invokes a method on the reference that assumes bar is non-null:

   shared.useBar();  // null pointer exception
Later, thread A runs the constructor for Foo.


I think you're right, if access to shared is not in any way synchronized. But the correct way to handle this, at least in this case, is to mark shared as volatile, which guarantees thread B will only ever read null or a fully constructed Foo from shared. This has been the case since Java 5, released 20 years ago, thanks to JSR-133.


By that standard, C and C++ are much worse, since they offer no runtime encapsulation at all, and have much worse and more subtle multithreaded errors (e.g. Java at least guarantees that all native word sized reads/writes are atomic, if I recall correctly). C++ doesn't even guarantee that a reference can't be null, or worse, deallocated before it is dereferenced. They allow you to specify that a field is of some type and shouldn't be null, which is nice, but they don't enforce that in any way, they just call any code path that violates it UB.

For example, this is code that any C or C++ compiler will happily run and do something:

  struct Bar {
    int b;
  };
  struct Foo {
    struct Bar bar;
  } foo;

  strcpy((char*)(&foo), "ABC");
Or in relation to null C++ references:

  int& foo(int* p) {
    return *p;
  } 
  
  int &r = foo(nullptr); //UB, but in practice will likely result in a null reference at runtime
Similarly, accessing an object from multiple threads without synchronization means its value is not fully defined in Java. Unlike C or C++, it is at least known to be a Java type, not a memory corruption vulnerability.


We can quibble on definitions here, but a reference in C++ can not be null. The undefined behavior happens before any assignment to a reference is executed so that at the moment that the assignment happens, the reference is guaranteed to not be null.

In your example, it's the dereference of the pointer to p that is undefined behavior, so anything that happens after that point is also undefined behavior. Note that means there is never an actual assignment to r if p is null.

As I mentioned earlier, this might seem like quibbling with definitions, but this is the proper mental model to have with respect to C++'s semantics.

Having said that, I don't disagree with the main crux of your point, which is that C++'s semantics are terrible and there is little that the language provides to write correct code, but I do think there are subtleties on this matter that are worth clarifying.


I agree, but by that same definition, a private reference field in Java that is initialized in the constructor also can't be null.

My point is that we can compare two things: valid programs, or programs that compile.

In valid C++ programs, references can't be null and there are no data races. In valid Java programs, all final fields initialized in an object's constructor have that value for the lifetime of the object.

If we compare invalid programs that compile, which is an important point as well, then those guarantees go out the window. But here Java is much more forgiving than C++: if you have improper synchronization, you may see fields which are null instead of having their final value, which is bad and confusing. But in C++ with improper synchronization, you can see literally any outcome at all.


That's not accurate for a final field, as final fields are initialized in a special way.

https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.ht...

>An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

If you don't publish a reference to the object from within the constructor, you will not see a null value of the final field, even if the object itself was unsafely published across threads via a non-volatile field.


This too is thanks to JSR-133. In fact, it seems that, thanks to this part of JSR-133, what I said above about marking shared volatile is actually unnecessary. There must be a memory barrier somewhere, under the hood, though.


At least on android arm64, looks like a `dmb ishst` is emitted after the constructor, which allows future loads to not need an explicit barrier. Removing `final` from the field causes that barrier to not be emitted.

https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename...


Yeah, I wish the VM would prevent null assignment of optional and force to empty. There are probably side effects I can’t think of here and certainly would cause problems with legacy code misusing optionals.


null can be avoided with a good linter


Not avoided altogether. Static checkers cannot possibly follow all code paths, and they generally err on the side of false negatives rather than risking too many false positives causing people to disable them.


I wasn’t aware they preferred type II errors. That makes sense, but I don’t really expect tools like that to work across modules.


It depends on the specific tool and how it's configured. But that has been my experience with many tools configured with their recommended settings.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: