Null Object Pattern in Java
Want to learn more about using the null object pattern in Java? Check out this tutorial to learn how to use the null object pattern with the optional class!
Join the DZone community and get the full member experience.
Join For FreeToday, I am discussing the less-used pattern called the null object pattern. In object-oriented programming, we deal very frequently with null objects. A null object refers to an object without any reference or an object defined with neutral/null functionality/behavior. These null objects need to be checked to ensure that they are not null while accessing any member or invoking any methods. This is because members or methods typically cannot be invoked on null objects.
Null Object Pattern
The null object design pattern describes the uses of null objects and their behavior in the system.
Null object patterns deal with null objects.
Instead of checking for the null object, we define null behavior or call do-nothing behavior.
These null objects can also be used to provide default behavior in case data is unavailable.
The advantage of this approach over a working default implementation is that a null object is very predictable and has no side effects — it does nothing.
The null object pattern can also be used to act as a stub for testing, in case a resource is unavailable for testing.
Before We Use the Null Object Pattern, We Should Understand:
This pattern should be used carefully. It can make bugs appear as a normal program execution.
We should not implement this pattern just to avoid null checks and make the code more readable. Actually, it is harder to read code that is moved to another place, like the null object class.
We have to perform additional testing to make sure that there is nowhere we have to assign a null instead of the null object.
Let's take a look at an example to better understand this pattern.
Example of Null Objects
Create an abstract class (or interface) to specify various functionalities. I am using the shape interface for this example. Please note that I have created a method
isNull()
as well in the interface. It's nice to have a method, and I like it because I can better identify and control null-defined objects. This method will returnfalse
for all of the concrete classes. And, it will returntrue
only for the null object class.
package design.nullobject;
public interface Shape {
double area();
double perimeter();
void draw();
// nice to have method to indicate null object
boolean isNull();
}
You will need to create a concrete class that extends this class or implements the interface). Each concrete class will define a specific version of the functionalities. I am defining three types of the shape:
Circle
,Rectangle
, andTriangle
. These concrete classes will define different types of shapes. Below is the code for theCircle
class:
package design.nullobject;
public class Circle implements Shape {
// sides
private final double radius;
public Circle() {
this(1.0d);
}
public Circle(double radius) {
this.radius = radius;
}
@Override public double area() {
// Area = π r^2
return Math.PI * Math.pow(radius, 2);
}
@Override public double perimeter() {
// Perimeter = 2πr
return 2 * Math.PI * radius;
}
@Override public void draw() {
System.out.println("Drawing Circle with area: " + area() + " and perimeter: " + perimeter());
}
@Override
public boolean isNull() {
return false;
}
}
Below is the code for the Rectangle
class:
package design.nullobject;
public class Rectangle implements Shape {
// sides
private final double width;
private final double length;
public Rectangle() {
this(1.0d ,1.0d);
}
public Rectangle(double width, double length) {
this.width = width;
this.length = length;
}
@Override
public double area() {
// A = w * l
return width * length;
}
@Override
public double perimeter() {
// P = 2(w + l)
return 2 * (width + length);
}
@Override
public void draw() {
System.out.println("Drawing Rectangle with area: " + area() + " and perimeter: " + perimeter());
}
@Override
public boolean isNull() {
return false;
}
}
Below is the code for the Triangle
class:
package design.nullobject;
public class Triangle implements Shape {
// sides
private final double a;
private final double b;
private final double c;
public Triangle() {
this(1.0d, 1.0d, 1.0d);
}
public Triangle(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public double area() {
// Using Heron's formula:
// Area = SquareRoot(s * (s - a) * (s - b) * (s - c))
// where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
@Override
public double perimeter() {
// P = a + b + c
return a + b + c;
}
@Override public void draw() {
System.out.println("Drawing Triangle with area: " + area() + " and perimeter: " + perimeter());
}
@Override
public boolean isNull() {
return false;
}
}
Now, the most important step is to create a null object class that extends the abstract class or interface and define the do nothing behavior. The do nothing behavior is like a default behavior in case data is not available.
package design.nullobject;
public class NullShape implements Shape {
// no sides
@Override
public double area() {
return 0.0d;
}
@Override
public double perimeter() {
return 0.0d;
}
@Override
public void draw() {
System.out.println("Null object can't be draw");
}
@Override
public boolean isNull() {
return true;
}
}
Now, we define the
Factory
class to create various types of shapes. Please refer to (Strategy vs Factory Design Pattern in Java) to understand the factory pattern. I am creating theShapeFactory
class for this example.
package design.nullobject;
public class ShapeFactory {
public static Shape createShape(String shapeType) {
Shape shape = null;
if ("Circle".equalsIgnoreCase(shapeType)) {
shape = new Circle();
} else if ("Rectangle".equalsIgnoreCase(shapeType)) {
shape = new Rectangle();
} else if ("Triangle".equalsIgnoreCase(shapeType)) {
shape = new Triangle();
} else {
shape = new NullShape();
}
return shape;
}
}
To keep the example simple, I have not received parameters for the shape sides in the ShapeFactory
method. So, the factory is creating the different Shape
object with the fixed side values.
And, at the last step, create a
Main
class to execute and test the code:
package design.nullobject;
import design.nullobject.ShapeFactory;
public class ShapeMain {
public static void main(String[] args) {
String[] shapeTypes = new String[] { "Circle", null, "Triangle", "Pentagon", "Rectangle", "Trapezoid"};
for (String shapeType : shapeTypes) {
Shape shape = ShapeFactory.createShape(shapeType);
// no null-check required since shape factory always creates shape objects
System.out.println("Shape area: " + shape.area());
System.out.println("Shape Perimeter: " + shape.perimeter());
shape.draw();
System.out.println();
}
}
}
Below is the output of the code:
Shape area: 3.141592653589793
Shape Perimeter: 6.283185307179586
Drawing Circle with area: 3.141592653589793 and perimeter: 6.283185307179586
Shape area: 0.0
Shape Perimeter: 0.0
Null object can't be draw
Shape area: 0.4330127018922193
Shape Perimeter: 3.0
Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
Shape area: 0.0
Shape Perimeter: 0.0
Null object can't be draw
Shape area: 1.0
Shape Perimeter: 4.0
Drawing Rectangle with area: 1.0 and perimeter: 4.0
Shape area: 0.0
Shape Perimeter: 0.0
Null object can't be draw
In Java 8, we have the
java.util.Optional
class to deal with the null references. This class was originally from the Guava API.
Optional
The purpose of the class is to provide a type-level solution for representing optional values instead of using null references. To gain deeper knowledge on using Optional, please refer to Oracle's article Tired of Null Pointer Exceptions? Consider Using Java SE 8's.
Below, I am going to demonstrate some of the useful APIs of java.util.Optional
. There are several static APIs to creating Optional
objects:
Optional.empty()
: To create an empty Optional object, use the empty API:
@Test
public void optionalEmptyTest() {
Optional<Shape> empty = Optional.empty();
assertFalse(empty.isPresent());
}
Optional.of()
: To create a non-empty Optional object, if we are sure that we have an object that we would like to make it Optional, use the API. If you call this API using null, it will throw the null pointer exception (NullPointerException
).
@Test
public void optionalOfTest() {
Shape shape = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.of(shape);
assertTrue(nonEmpty.isPresent());
}
@Test(expected = NullPointerException.class)
public void optionalOfWithNullTest() {
Shape shape = null;
Optional.of(shape);
}
Optional.ofNullable()
: In the code above, both APIs are useful when we are on empty or non-empty. If we are not sure about the object, the object may or may not be null. For this, use theofNullable
API.
@Test
public void optionalOfNullableTest() {
Shape shape1 = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.ofNullable(shape1);
assertTrue(nonEmpty.isPresent());
Shape shape2 = null;
Optional<Shape> empty = Optional.ofNullable(shape2);
assertFalse(empty.isPresent());
}
Additional APIs
isPresent()
: This method returns true if, and only if, the object wrapped in the Optional is not-empty (not null).
@Test
public void optionalIsPresentTest() {
Shape shape = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.of(shape);
assertTrue(nonEmpty.isPresent());
Optional<Shape> empty = Optional.empty();
assertFalse(empty.isPresent());
}
ifPresent()
: This enables us to run code on the wrapped value if it is found to be non-null.
@Test
public void optionalIfPresentTest() {
Shape shape = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.of(shape);
nonEmpty.ifPresent(circle -> circle.draw());
Optional<Shape> empty = Optional.empty();
empty.ifPresent(circle -> circle.draw());
}
-
get()
: This returns a value if the wrapped object is not null. Otherwise, it throws a no such element exception (NoSuchElementException
).
@Test
public void optionalGetTest() {
Shape shape = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.ofNullable(shape);
assertNotNull(nonEmpty.get());
}
@Test(expected = NoSuchElementException.class)
public void optionalGetWithNullTest() {
Shape shape = null;
Optional<Shape> empty = Optional.ofNullable(shape);
empty.get();
}
orElse()
: This is used to retrieve the object wrapped inside theOptional
instance. The method has one parameter as a default object. WithorElse
, the wrapped object is returned if it is present, and the argument given toorElse
is returned if the wrapped object is absent.
@Test
public void optionalOrElseTest() {
Shape shape = ShapeFactory.createShape("Rectangle");
Shape shape1 = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.of(shape1);
assertEquals(shape1, nonEmpty.orElse(shape));
Optional<Shape> empty = Optional.empty();
empty.ifPresent(circle -> circle.draw());
assertEquals(shape, empty.orElse(shape));
}
orElseGet()
: This method is similar toorElse
. The only difference is, instead of taking an object to return if theOptional
object is not present, it takes a supplier functional interface, which is invoked and returns the value of the invocation.
@Test
public void optionalOrElseGetTest() {
Shape shape = ShapeFactory.createShape("Rectangle");
Shape shape1 = ShapeFactory.createShape("Circle");
Optional<Shape> nonEmpty = Optional.of(shape1);
assertEquals(shape1, nonEmpty.orElseGet(() -> ShapeFactory.createShape("Rectangle")));
Optional<Shape> empty = Optional.empty();
empty.ifPresent(circle -> circle.draw());
// comparing the area of the shape since orElseGet will create another instance of rectangle
assertEquals(shape.area(), empty.orElseGet(() -> ShapeFactory.createShape("Rectangle")).area(), 0.001d);
}
orElseThrow()
: This is similar toorElse
. The only difference is it adds a new approach for handling an absent value. Instead of returning a default value when the wrapped object is absent, it throws a given (as parameter) exception.
@Test(expected = IllegalArgumentException.class)
public void optionalOrElseThrowWithNullTest() {
Shape shape = null;
Optional<Shape> empty = Optional.ofNullable(shape);
empty.orElseThrow(IllegalArgumentException::new);
}
Now, we can write the ShapeFactory
and Main
using Optional
, which is demonstrated below:
package design.nullobject;
public class ShapeFactoryJava8 {
public static Optional<Shape> createShape(String shapeType) {
Shape shape = null;
if ("Circle".equalsIgnoreCase(shapeType)) {
shape = new Circle();
} else if ("Rectangle".equalsIgnoreCase(shapeType)) {
shape = new Rectangle();
} else if ("Triangle".equalsIgnoreCase(shapeType)) {
shape = new Triangle();
} else {
// no need to have NullShape anymore
shape = null;
}
// using ofNullable because shape may be not null.
return Optional.ofNullable(shape);
}
}
And, the Main
class to execute and test the code:
package design.nullobject;
import java.util.Arrays;
import java.util.Optional;
public class ShapeMainJava8 {
public static void main(String[] args) {
String[] shapeTypes = new String[] { "Circle", null, "Triangle", "Pentagon", "Rectangle", "Trapezoid"};
Arrays.asList(shapeTypes).stream().forEach(shapeType -> {
Optional<Shape> optionalShape = ShapeFactoryJava8.createShape(shapeType);
optionalShape.ifPresent((shape) -> {
// null-check is done by ifPresent of Optional
System.out.println("Shape area: " + shape.area());
System.out.println("Shape Perimeter: " + shape.perimeter());
shape.draw();
System.out.println();
});
});
}
}
Below is the output of the code:
Shape area: 3.141592653589793
Shape Perimeter: 6.283185307179586
Drawing Circle with area: 3.141592653589793 and perimeter: 6.283185307179586
Shape area: 0.4330127018922193
Shape Perimeter: 3.0
Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
Shape area: 1.0
Shape Perimeter: 4.0
Drawing Rectangle with area: 1.0 and perimeter: 4.0
Like this article? Don't forget to press the like button. Happy coding!
Need more articles on Design Patterns? Below are some of them I have shared with you.
Some additional Articles:
Opinions expressed by DZone contributors are their own.
Comments