[HN Gopher] What if null was an Object in Java?
___________________________________________________________________
What if null was an Object in Java?
Author : ingve
Score : 34 points
Date : 2024-04-27 08:29 UTC (1 days ago)
(HTM) web link (donraab.medium.com)
(TXT) w3m dump (donraab.medium.com)
| mrkeen wrote:
| I totally support the idea of null having type Null, but only if
| null no longer has type Integer, and null no longer has type
| Bufferedreader, etc.
| randomopining wrote:
| Doesn't an Optional basically cover this case
| hedora wrote:
| 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.)
| jflwyasdf wrote:
| Fortunately, Uber made tooling for languages with broken type
| systems
|
| * https://github.com/uber/NullAway
|
| * https://github.com/uber-go/nilaway
| erik_seaberg wrote:
| Lombok, Error Prone, and Kotlin also have their takes on
| the problem.
| durable_queue wrote:
| null can be avoided with a good linter
| kbolino wrote:
| 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.
| dexwiz wrote:
| 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.
| kbolino wrote:
| 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.
| kbolino wrote:
| 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.
| hedora wrote:
| 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.
| kbolino wrote:
| 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.
| tsimionescu wrote:
| 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.
| Maxatar wrote:
| 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.
| alkonaut wrote:
| Wait, javas Optional is a reference type so it can be null?
| Doesn't that almost defeat the purpose of it?
| Defletter wrote:
| 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.
| alkonaut wrote:
| Did the project to add value types to Java (I'm sure I
| heard of it a decade ago) never finish?
| Defletter wrote:
| 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.
| steve_rambo wrote:
| https://openjdk.org/projects/valhalla
| michaelt wrote:
| 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.
| paulddraper wrote:
| 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.
| pgwhalen wrote:
| 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.
| mrkeen wrote:
| 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.
| simpsond wrote:
| 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.
| sedro wrote:
| > 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)
| philipwhiuk wrote:
| I don't actually think it solves any problems. You still have to
| null check.
| jflwyasdf wrote:
| null would just mean the zero value instead of the absence of a
| value String foo = null; String bar
| = ""; foo.equals(bar) --> true
|
| This works well provided the data type has a sensible zero
| value like collection types
|
| EDIT: I'm blocked from posting so I won't be responding
| further, thank you for discussion.
| golergka wrote:
| In the end, you'll have a mixture of NULL and "" in your DB,
| and a couple of years later a piece of logic written in
| another language will fail spectacularly.
| jflwyasdf wrote:
| This is how I would do it. Go: *string
| Java: Option<String> or @Nullable String Rust:
| Option<String> TypeScript: string | undefined
| (or string | null)
| golergka wrote:
| The problem is, not all of these languages think that ""
| and null are equal.
| alkonaut wrote:
| A null collection and an empty collection are two different
| things. A nullable collection is one that has the state "no
| collection" semantically separate from "empty collection".
|
| Similarly an Option<byte> has 257 different values while a
| byte has 256 different values. That the byte has a good zero
| value doesn't change that - the whole reason for choosing a
| maybe-byte is having 257 values, not 256.
| jflwyasdf wrote:
| Right that depends if you subscribe to the belief that null
| means the absence of a value `Option<T>` or does it mean
| the zero value `T`.
| alkonaut wrote:
| If null and [] should be the same thing then I'd make
| absolutely sure you can't represent both. You don't want
| two states representing the same thing. That should be
| easy to ensure if a language is reasonable. E.g a field
| that can't be null (best case a non-nullable type,
| otherwise maybe a constructor guaranteeing it's never
| null)
|
| As the example of byte vs option<byte> either you want
| 256 states or you want 257. If you have 256 or 257 states
| you want to represent will decide which type is correct.
| The other choice of type is _incorrect_.
|
| In some languages, these things are blurred because the
| language doesn't _let_ you choose a correct type for the
| state space, but I'm talking about the case where you can
| (coincidentally the set of languages I'd use).
| coin wrote:
| Null is the absence of a value. How do to distinguish 0
| from no value?
| Maxatar wrote:
| The point is to eliminate the idea of an absence of a
| value. A variable is always assigned to a value, but
| there is a special value called null which behaves as a
| kind of sentinel value whose methods all return the null
| value for their respective type.
| tsimionescu wrote:
| That just gets us back to the problem for which Null is
| introduced in almost every lamguage: indicating the absence
| of a value. This is an important feature in every language,
| and null is the most popular solution to it (the only
| significant alternative is the Maybe monad).
|
| To put this in more concrete terms, if this change were
| integrated in Java, how would you indicate the difference
| between a JSON document which doesn't contain a field VS one
| where the field is an empty string?
| Larrikin wrote:
| Null should be valid.
|
| Kotlin solved Java's problem by making it a compiler error if a
| value that can be null isn't checked and shown to be null or
| the actual value, eliminating an entire class of exceptions.
| equalsione wrote:
| I'm not familiar enough with kotlin to comment fully but from
| your description the checker framework [0] appears to do the
| same thing in Java.
|
| I confess I'm not fond of checker framework. I find the error
| messages can be obtuse but it is very effective.
|
| 0 - https://checkerframework.org/
| sebastianconcpt wrote:
| It enables you to https://www.geeksforgeeks.org/proxy-design-
| pattern/
| recursive wrote:
| If Null is not a subtype of MyType, then you wouldn't be able to
| assign null to a variable decalred as MyType, without breaking
| the rest of the rules of Java. I don't really see how this could
| work, even theoretically.
| Kamq wrote:
| There's no reason that something can't both an object and the
| bottom of the type hierarchy.
|
| It's, technically, an instance of multiple inheritance. Java
| doesn't generally allow this, but there's tons of special cases
| in the compiler for things that you can't do yourself. For
| example, defining operators is done in the compiler, but you
| can't define operators for your own classes.
| DaiPlusPlus wrote:
| It would be a bottom-type of reference-types. This wouldn't
| work for value-types like int, at least not without boxing,
| which would be very painful.
| m_fayer wrote:
| I think the nullable reference type system in c# is a perfectly
| workable compromise for languages that had nullability from the
| very beginning. Once a code base uses them fully, most null-
| related bugs are gone. And it's far from an unwieldy bolt-on.
| aloisdg wrote:
| The Nullable class is nice and I love to use it but it would be
| even better with an option type like in F#
| alkonaut wrote:
| There isn't any big difference between T? And Option<T>
| semantically. Many code bases use an Option<T> in C# to
| indicate a 0-or-1 object result, but refactor those to T?
| instead with nullable. It combines better e.g
| Option<Option<T>> doesn't need to be handled manually.
| ablob wrote:
| I'm going to be a bit pedantic here: There is a semantic
| difference between Option<Option<T>> and Option<T>. If I
| intend to retrieve a setting from a file, the former allows
| me to differentiate between a missing file or a missing
| setting, while the latter destroys that information. i.e.:
| There are 3 possible cases, while only 2 can be
| represented.
|
| So T? doesn't compose, while Option<T> does, which I'd
| consider a big difference.
|
| However, without a builtin option type, duck-typing, or a
| pleasant way of converting the types, Option<T> may become
| a hassle (especially when different dependencies ship their
| own). And as T? is shipped with the language this is
| probably why it is used when the composability is not
| required.
|
| P.S.: In C# T? even neatly composes. return
| obj?.a?.b
|
| is equivalent to if (obj != null && obj.a
| != null) { return obj.a.b } else return null;
| zigzag312 wrote:
| > There is a semantic difference between
| Option<Option<T>> and Option<T>. If I intend to retrieve
| a setting from a file, the former allows me to
| differentiate between a missing file or a missing setting
|
| Does nesting Option's really has practical use or does it
| quickly become confusing?
|
| In your example, Option<Option<T>> return type doesn't
| tell me by itself that this differentiates between a
| missing file or a missing setting. I would need to get
| this information from somewhere else.
| evrimoztamur wrote:
| That sounds like you should be using Result<T, E> to
| handle the two E cases you are describing. Success with
| Ok(T), with Err(Missing File) plus Err(Missing Setting).
| Spivak wrote:
| I think Option<T> is an overrated construct when it's not a
| monad. The whole value is being able to write a chain of
| operations myobj.dothing(x).otherthing(y)
|
| Without having to check the intermediate results. When it's
| just a tagged union you end up having to check for null
| everywhere anyway. It's better than "untyped" null because it
| can't pop up anywhere but it's not super ergonomic. I think
| Nullable where the Option is implicit does it better.
| jeroenhd wrote:
| With a good IDE configured to break on nullability errors,
| @Nonnull and @Nullable can also be used to replace the C#
| system in practice (not the same, but close enough).
| Unfortunately, this requires coordination from different team
| members and external dependencies rather than standard language
| features.
| mbb70 wrote:
| Ruby has a `NilClass` and the best/worst part of it is the to_s
| is "", to_i is 0, to_f is 0.0, to_a is [], to_h is {}
|
| It's incredibly clean and convenient until you wake up one
| morning and have no idea what is happening in your code
| falcor84 wrote:
| Indeed. One's greatest strength is also their greatest
| weakness.
| Terr_ wrote:
| > the to_s is "", to_i is 0, to_f is 0.0, to_a is [], to_h is
| {}
|
| I somehow can't help reading that as some sort of high school
| sports-cheer: "Gimme an S to the quote to the I to the oh to
| the F to the zero! Goooo Rubies!"
| cmiller1 wrote:
| Of course since it's Ruby you can just monkey patch those to_s
| methods to do whatever the hell you want, confounding anyone
| else working on your codebase.
|
| I love using Ruby when I'm the only one who will ever have to
| look at or touch it.
| gls2ro wrote:
| I dont think there is something wrong with that once you think
| about what is a Null Element (or identity) in a group that is
| represented by a set of elements and a function:
|
| Integer, + => 0
|
| Float, + => 0.0
|
| Array, add => []
|
| Hash, merge => {}
|
| and so on.
|
| I think maybe we can debate the operations/functions, but they
| make sense. For Integer in some ways you can define almost all
| other operations that you commonly use based on the addition.
|
| So while nil is an object when trying to find a representation
| in other group I find it logical or expected.
|
| Also Ruby will not automatically try to coerce nil when not
| asked to do so
|
| like for example 0 + nil will throw an error.
| xamde wrote:
| There is also the "Void" type.
|
| > The {@code Void} class is an uninstantiable placeholder class
| to hold a reference to the {@code Class} object representing the
| Java keyword void.
|
| When Java introduced Generics they re-used "Void" type. Method
| calls need to use "null" when "Void" is the type. So in a way,
| "type of null" is "Void".
| JanisErdmanis wrote:
| Has anyone tried to make null as a function? In Julia, there is a
| `get(null::Function, collection, key)` method which can be used
| in a syntax:
|
| value = get(collection, key) do error("$key in
| the collection not found")
|
| end
|
| This syntax has captivated my attention lately as it seems like a
| viable alternative to doing an explicit null check before using
| the value.
| docandrew wrote:
| I proposed something similar for D a long time ago, I didn't
| realize though that there was prior art there with Smalltalk!
| krackers wrote:
| Objective-c allows you to send messages to null objects. On one
| hand it allows for a form of null-coalescing, but on the other it
| allows bugs to slip in and get the program into an unexpected
| state, whereas a more rigorous treatment would result in a more
| deterministic crash.
| downrightmike wrote:
| What if it was a smart object and the system could figure out
| what was supposed to happen and give you that back?
| hgyjnbdet wrote:
| I quite like the way dart handles nulls.
| nitwit005 wrote:
| It'd be fairly easy to modify Java so that the primitives,
| including null, behave more like objects. And certainly, Java has
| inched that direction.
|
| It doesn't solve anything fundamental though. You could make
| null.toString() return "null", but in most cases that will just
| be a bug. You're missing a null check or failed to initialize
| something.
| specialist wrote:
| What if every null reference was an instance of a Null Object
| pattern? https://en.wikipedia.org/wiki/Null_object_pattern
|
| Eliminates null checks, Optionals, and NPEs. Probably moots
| annotations like @NotNull too, but maybe some use cases need
| those for client APIs.
|
| Makes iterating data structures like graphs simple. Faster too,
| because the JIT NOP a Null Object's methods. (Mostly; profile to
| confirm, then tweak as needed.)
|
| I implement a Null Object for each base class.
|
| Each base class has a static final member NULL referencing its
| Null Object implementation (flyweight, singleton). Then assign
| variables to AwesomeThing.NULL instead of null.
|
| A spiffy javac could code generate Null Object implementations.
| (It's on my todo list.) For scalars, just use the default null
| value should be. int is 0, float is NaN, etc. Their boxed values
| will need small shims too.
|
| Customizing javac (with some compiler plugin or something) is
| deep down on my TODO list. So I'm unlikely to be the (first)
| person to do this work. Sorry.
| yafetn wrote:
| If null-safety on the JVM is important to you, just use Kotlin.
| Joker_vD wrote:
| We already have None in e.g. Python but that merely means that
| "x.mathod_name()" instead of throwing a NullPointerException
| raises an AttributeError, because None has no method
| "method_name". Okay? Not really any meaningfully different.
___________________________________________________________________
(page generated 2024-04-28 23:00 UTC)