What if null was an Object in Java?
Brace yourself and get ready to try the red pill.
Object-oriented purity
Smalltalk is often referred to as a pure object-oriented programming language. Smalltalk was created by Alan Kay, who was also the person who coined the term “object-oriented." Part of the pure object-oriented nature of Smalltalk is that “everything is an object” in Smalltalk. Because everything is an object, you accomplish programming tasks by sending messages to objects.
Java is also object-oriented, but is not a pure object-oriented programming language. Java has things in it that are not objects (e.g. primitives), that require special mechanisms for handling them. Java also has a special case for handling null
.
In this blog, I will demonstrate and explain how Java and Smalltalk deal with absent values, referred to as null
in Java, and nil
in Smalltalk. This blog will not provide a full explanation of Java or Smalltalk syntax. There are resources available on the internet for both if you want to learn more. The blog will only cover what is necessary to explain the code examples I share. I will do my best to explain the examples in detail so that syntax is not a barrier to understanding. This blog is intended to make you ponder larger possibilities, not struggle with minutiae of language syntax.
In the sections that follow, I will compare solutions to the same simple problems using both Java and Smalltalk. I will be using Java 21 with IntelliJ IDEA CE 2023.3.2 for the Java code examples. I will be using Pharo Smalltalk 11.0 for the Smalltalk code examples.
Null vs. Nil
The literal null in Java
In Java, there is a literal named null
. You can assign null
to any variable that has an Object
type, but the reference the variable points to is not an instance of an Object
. I like to think of null
as an instance of the Spoon in the movie, “The Matrix”. There is no Spoon.
The following test code illustrates some of the properties of null
in Java.
@Test
public void nullIsNotAnObject()
{
// Assert things about null
Assertions.assertFalse(null instanceof Object);
final Set<?> set = null;
Assertions.assertFalse(set instanceof Set);
// Assert things about exceptions thrown when calling methods on null
Assertions.assertThrows(
NullPointerException.class,
() -> set.getClass());
Assertions.assertThrows(
NullPointerException.class,
() -> set.toString());
// Assert things about a non-null variable named set2
final Set<?> set2 = new HashSet<>(Set.of(1, 2, 3));
set2.add(null);
Assertions.assertNotNull(set2);
Assertions.assertNotNull(set2.getClass());
Assertions.assertTrue(set2 instanceof Set);
// Filter all of the non-null values from set2 in the Set named set3
// Uses a static method refererence from the Objects class
final Set<?> set3 = set2.stream()
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableSet());
Assertions.assertEquals(Set.of(1, 2, 3), set3);
}
In this test, I assert that null
is not an instance of Object
. I initialize a final variable of type Set<?>
named set
to null
, and further assert
that set
, which is null
, is not an instance of Set
. I assert
that when I call getClass()
or toString()
on set
, which is still null
, a NullPointerException
is thrown. This happens because null
is not an Object
. Note, I made the first declaration of the set
variable final
here, in order to reference the variable set
in the two lambdas in the first section where I assert
that NullPointerException
is thrown. I could have left it as “effectively final” by not trying to reset the value, but thought I would just go with having it be explicitly final.
In the second section, I create a mutable Set
and store it in a variable named set2
. I add null
to the set
. Set.of()
will not accept null values, so I had to convert the immutable Set
to a HashSet
, which does accept null
values. I add null
manually to set2
. I assert that set2
is not null
, that its class is not null
, and that set2
is in fact an instance of a Set
as the compiler says it is.
Finally, I filter
the instances contained in set2
into a new set3
as long as they respond true to the method reference Objects::nonNull
, which references a static method on Objects
that returns a Predicate
that checks that object != null
. Once again, since null
is not an object, you cannot call any methods on it that could be used to construct a valid method reference as a Predicate
.
This is the null
in Java that we are all used to. It is not an Object
. We’ve all learned to deal with null
when coding in Java. Former Smalltalkers will know there is a different way.
The literal nil in Smalltalk
In Smalltalk, there is a singleton object instance named nil
. The literal nil
, is an instance of the class UndefinedObject
. UndefinedObject
is a subclass of Object
. The Object
class is a subclass of… nil
. This circular definition has melted many programmers brains, including mine. Somehow, this all just works. It is one of the magical aspects of Smalltalk. There’s a turtle at the top, sitting on top of another turtle, and it’s just turtles all the way down.
The following test code passes using Pharo Smalltalk 11.0.
testNilIsAnObject
|set setNoNils|
# Assert things about nil
self assert: nil isNil.
self assert: 'nil' equals: nil printString.
# Assert things about the set variable which is nil
set := nil.
self assert: set isNil.
self assert: set equals: nil.
self assert: set class equals: UndefinedObject.
self assert: (set ifNil: [ true ]).
self assert: set isEmptyOrNil.
# Assert things about the set variable which is not nil
set := Set with: 1 with: 2 with: 3 with: nil.
self deny: set isNil.
self assert: set isNotNil.
self deny: set equals: nil.
self deny: set isEmptyOrNil.
self assert: set class equals: Set.
# Select all the non-nil values into a new Set name setNoNils
setNoNils := set select: #isNotNil.
self assert: (Set with: 1 with: 2 with: 3) equals: setNoNils.
Above is what a method definition looks like in Smalltalk. I wrote a unit test method named testNilIsAnObject
. I define two temporary variables named set
and setNoNils
by declaring them between the two vertical pipes after the method name like this |set setNoNils|
. In the first section of code I assert a few things about nil
, in order to demonstrate it is in fact an instance of an Object
. The literal self
is the equivalent of this
in Java, and refers to the instance of the class that testNilIsAnObject
is defined on, which has methods it inherits named assert:
, deny:
, and assert:equals:
. I assert
that nil
can respond to messages like any other object in Smalltalk. I assert
that nil
responds true
to isNil
. I also assert that calling printString
on nil
results in the String
‘nil’
being returned.
The :=
operator is used for variable assignment in Smalltalk. I assign the instance referenced by the literal nil
to the variable named set
. I assert that set
responds true
when sent the message isNil
. I assert that the object reference contained in the set
variable is an instance of UndefinedObject
. I assert that calling the ifNil:
message on set returns true
. The code [ true ]
is a zero argument block or lambda. In Java, the equivalent would be the a lambda typed as a Supplier
. Finally, I assert that set responds to true when sent the message isEmptyOrNil
.
In the second section of code, I create an instance of a Set
by using the class method named with:with:with:with:
which takes four parameters. I then deny
that the set isNil
. I assert
that that the set isNotNil
. I assert
the set
is not equal to nil
. I deny the set isEmptyOrNil
, since it it neither nil
or isEmpty
. I then assert
that the class of set
is Set
.
In the third section of code, I select
all instances contained in set
into a new Set
named setNoNils
, if the instance returns true to the message isNotNil
. The important thing here is that every subclass of Object
in Smalltalk responds true
or false
to the message isNotNil
.
Here’s a final example of one of the methods available to all objects in Smalltalk, including nil
. The method name is ifNil:ifNotNil:
. It is a control structure method which takes two block (aka lambda) parameters. The result is determined polymorphically by the type. The set
here knows it is not nil
, so it will automatically execute the second block and return the result which here is the String
, ‘not nil’
. The literal nil
will know it is nil
, so it will automatically execute the first block and return the result which here is the String
, ‘nil’
.
# Use the built in control structures around nil on all objects
self assert: (set ifNil: [ 'nil' ] ifNotNil: [ 'not nil']) equals: 'not nil'.
self assert: (nil ifNil: [ 'nil' ] ifNotNil: [ 'not nil']) equals: 'nil'.
What if null was an Object in Java?
The following code is speculative, won’t compile, is untested, and unproven by Java language experts. But perhaps if null
were an instance of some class named Null
, the following code might be possible.
@Test
// NOTE THIS CODE WILL NOT COMPILE AND IS PURELY SPECULATIVE!!!
public void nullIsNotAnObject()
{
// Assert things about null
Assertions.assertTrue(null instanceof Object);
final Set<?> set = null;
Assertions.assertFalse(set instanceof Set);
// Assert things about calling methods on null as an object
Assertions.assertTrue(set.isNull());
Assertions.assertEquals(Null.class, set.getClass());
Assertions.assertEquals("null", set.toString());
// Assert things about a non-null variable
final Set<?> set2 = Set.of(1, 2, 3);
Assertions.assertNotNull(set2);
Assertions.assertNotNull(set2.getClass());
Assertions.assertTrue(set2 instanceof Set);
// Filter all of the non-null values from set2 in the Set named set3
// Uses an instance method refererence from the Object class
final Set<?> set3 = set2.stream()
.filter(Object::isNotNull)
.collect(Collectors.toUnmodifiableSet());
// Calling methods defined on Object, that would be overridden in Null
Assertions.assertEquals("null", null.ifNull(() -> "null"));
Assertions.assertNull(null.ifNotNull(() -> "notNull"));
Assertions.assertEquals("not null", set3.ifNotNull(() -> " not null"));
}
If null
were an instance of a class named Null
, it would also be possible to add methods like ifNull
, isNotNull
, ifNotNull
, isEmptyOrNull
to both Object
and Null
as Smalltalk does. The ifNull
and ifNotNull
methods would take some functional interface type like Supplier
, Consumer
, or Function
and then work with lambdas and method references. In the example above, I changed the filter
code to use a Predicate
formed from a method reference using the method named isNotNull
which would be defined on Object
.
The tricky part would be how to make null
capable of representing any interface and class, and dispatching calls to a method named doesNotUnderstand
for any methods from those interfaces that null
wouldn’t understand. The Null
class would have to behave like a Proxy
for any type it is stored in, and forward methods sent to a class like Set
, to a single method that could handle the “I’m not a Set” response appropriately.
Perhaps with null
as an Object in Java, it would be possible to do away with a whole variety of NullPointerExceptions
.
Why is nil being an Object useful?
The fact nil
is an instance of the UndefinedObject
class in Smalltalk allows it to be treated like all other objects in Smalltalk. It can respond to basic methods that are supported on all objects. The instance nil
has first class treatment in the Object hierarchy in that every object knows that it is or isn’t nil
. This brings a nice symmetry to the language and libraries. This does not remove the need to handle nil
by avoiding or excluding it, but nil
handling is done using the same mechanisms that you would use to handle or exclude other types, by calling methods like isNil
, isNotNil
, ifNil:
,ifNotNil:
, ifNil:ifNotNil:
.
In a language like Smalltalk where controls structures are not defined as statements in the language, but as methods in the libraries, having nil
exist like everything else, as an object, leads to a an amazing level of consistency and clarity. In my next blog I will delve more into how some other control structures in Smalltalk are defined in the class library.
Final thoughts
I’m not looking to change Java, as the null
ship sailed a very long time ago. What I am hoping is that I can help illuminate the possibilities for Java developers who may not have seen another way of handling null
in a different object-oriented programming language.
This blog was intended to explain in simple terms the differences between null
in Java, and nil
in Smalltalk. Learning multiple languages that use different strategies to address the same problems is useful. Learning a whole new language takes time. My hope is that this bitesize comparison is useful for you to be able to understand the pros and cons of a different approach.
In my next blog, I plan to include two other literal objects in Smalltalk named true
and false
., and compare them to their primitive Java equivalent also named true
and false
. I will also compare them to the Java Object equivalent named Boolean
. I will also look to explain how control structures can be defined effectively in a library, and not require special syntax and reserved words for if
, for
, while
statements.
Stay tuned, and thanks for reading!
I am the creator of and committer for the Eclipse Collections OSS project, which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.