10 Best Practices for Handling Null in Java

Hello guys, Dealing with null is an inevitable part of Java programming, but following best practices can help you manage it effectively and write more reliable code. In this article, we'll explore ten best practices for handling null in Java to improve code quality and avoid common pitfalls. If you can handle null then you will sure write better code as null is often the cause of Runtime errors in Java, you may heard of NullPointerException? No. If not then I can say that you have not coded enough in Java because it was the first error I got when I start writing Java program in my college days. At that time I don't know how to handle null better so it's long painful debugging until we find the cause and then do some workaround or fix but if you know how to handle null better then you can write code which can withstand test of time in production. 


10 Tips to Handle Null in Java

Now that we know how important it is to handle null in Java, here are a few tips to handle null better in Java. 


1) Return Zero-Length Arrays Instead of Null
When a method returns an array, consider returning a zero-length array (new Type[0]) instead of null when there is no data to return. This practice simplifies client code and eliminates the need for null checks.

Here is a code example of returning zero length arrays instead of null in Java:

public class ArrayExample {

    // Method that returns an array of integers or an empty array if null
    public static int[] getArrayOrEmpty(int size) {
        if (size <= 0) {
            // Returning an empty array instead of null
            return new int[0];
        }

        // Logic to create and populate the array
        int[] resultArray = new int[size];
        for (int i = 0; i < size; i++) {
            resultArray[i] = i + 1; // Filling the array with values for demonstration
        }

        return resultArray;
    }

    public static void main(String[] args) {
        int size = 5;

        // Getting the array from the method
        int[] result = getArrayOrEmpty(size);

        // Checking if the array is empty
        if (result.length == 0) {
            System.out.println("The array is empty.");
        } else {
            System.out.println("Array elements:");
            for (int value : result) {
                System.out.print(value + " ");
            }
        }
    }
}

In this example, the getArrayOrEmpty() method takes an integer size as a parameter. If the size is less than or equal to 0, it returns an empty array (zero-length array). Otherwise, it creates and populates an array with values based on the provided size. The main method demonstrates calling this function and handling the returned array, including checking if it's empty before performing any operations.


2) Return Empty Collections
For methods returning collections like List or Set, return Collections.emptyList() or Collections.emptySet() instead of null. This helps prevent NullPointerExceptions and promotes cleaner client code.

import java.util.Collections;
import java.util.List;

public class ItemProcessor {

    // Method to retrieve a list of items
    public List<String> getItems() {
        // Some logic to fetch items; for simplicity, let's assume an empty list for now
        List<String> itemList = Collections.emptyList();

        // Ensure never to return null; return an empty list instead
        return itemList != null ? itemList : Collections.emptyList();
    }

    public static void main(String[] args) {
        ItemProcessor processor = new ItemProcessor();

        // Retrieve items
        List<String> items = processor.getItems();

        // Process the items (in this case, just printing them)
        for (String item : items) {
            System.out.println("Processing item: " + item);
        }
    }
}


3) Avoid Null for Wrapper Classes
Avoid returning null for methods that return wrapper classes like Boolean, Integer, or Double. When clients store the return value in a primitive field, unboxing null can lead to a NullPointerException. Return appropriate default values or use Optional instead.

import java.util.Optional;

public class ProcessExecutor {

    // Method to execute a process and return a Boolean indicating success or failure
    public Boolean executeProcess() {
        // Some logic to execute the process; for simplicity, let's assume success for now
        boolean processSuccessful = true;

        // Return appropriate default value or use Optional
        return processSuccessful ? Boolean.TRUE : Boolean.FALSE;
    }

    // Method using Optional to indicate success or failure
    public Optional<Boolean> executeProcessWithOptional() {
        // Some logic to execute the process; for simplicity, let's assume success for now
        boolean processSuccessful = true;

        // Use Optional to avoid returning null
        return Optional.of(processSuccessful);
    }

    public static void main(String[] args) {
        ProcessExecutor executor = new ProcessExecutor();

        // Example without Optional
        Boolean result = executor.executeProcess();

        // Example using Optional
        Optional<Boolean> optionalResult = executor.executeProcessWithOptional();

        // Process the results
        if (result != null) {
            // Process the Boolean result (without risking NullPointerException)
            System.out.println("Process Result: " + result);
        } else {
            System.out.println("Process Result is null");
        }

        // Process the Optional result
        System.out.println("Optional Process Result: " + optionalResult.orElse(false));
    }
}


In this example, the executeProcess method returns a Boolean value to indicate the success or failure of a process. Instead of returning null, it returns Boolean.TRUE or Boolean.FALSE based on the outcome of the process. 

Additionally, there's a method executeProcessWithOptional that uses Optional to encapsulate the result, eliminating the need to return null. The calling code can then use Optional methods like orElse to handle the absence of a value gracefully.

10 Best Practices for Handling Null in Java



4) Use Optional for Nullable Results
When a method could return null in the absence of results (e.g., findByName()), return an Optional instead of null. Optional makes it explicit that a result may be absent and encourages clients to handle this case.

import java.util.Optional;

public class NullableResultExample {

    // Method returning a potentially nullable result
    public String findUserById(int userId) {
        // Some logic to find a user by ID; for simplicity, let's assume the user is not found
        // and we might return null
        return null;
    }

    // Method using Optional for handling nullable results
    public Optional<String> findUserByIdOptional(int userId) {
        // Delegate to the original method and wrap the result in Optional
        return Optional.ofNullable(findUserById(userId));
    }

    public static void main(String[] args) {
        NullableResultExample example = new NullableResultExample();

        // Example without Optional
        String user = example.findUserById(123);

        if (user != null) {
            // Process the user
            System.out.println("User found: " + user);
        } else {
            System.out.println("User not found");
        }

        // Example using Optional
        Optional<String> optionalUser = example.findUserByIdOptional(123);

        // Process the Optional result
        if (optionalUser.isPresent()) {
            System.out.println("User found: " + optionalUser.get());
        } else {
            System.out.println("User not found");
        }

        // Alternatively, use ifPresent method
        optionalUser.ifPresentOrElse(
            foundUser -> System.out.println("User found: " + foundUser),
            () -> System.out.println("User not found")
        );
    }
}


In this example, the findUserById method returns a potentially nullable result (in this case, null to indicate that the user is not found). The findUserByIdOptional() method then uses Optional.ofNullable() to wrap the potentially nullable result in an Optional. 

The calling code can then use various Optional methods (isPresent, ifPresent, etc.) to handle the presence or absence of a value more elegantly and without explicit null checks.


5) Avoid Synchronizing on Null
Never synchronize on a return value of a method, as it may lead to a NullPointerException if the method returns null. Synchronization should be performed on valid objects to avoid unexpected issues.

Here is a code example showing that:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SynchronizationExample {

    // Dedicated lock object for synchronization
    private final Lock lock = new ReentrantLock();

    // Some data to protect
    private String sharedData;

    // Method for synchronized access to sharedData
    public void updateSharedData(String newData) {
        lock.lock();
        try {
            // Perform synchronized operations on sharedData
            this.sharedData = newData;
            System.out.println("Shared data updated: " + newData);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SynchronizationExample example = new SynchronizationExample();

        // Example updating shared data
        example.updateSharedData("New Value");
    }
}

In this example:

  • The ReentrantLock is used as a dedicated lock object (lock) for synchronization.
  • The updateSharedData method demonstrates how to use the lock to perform synchronized operations on the sharedData field.
Using a dedicated lock object allows for more controlled synchronization and avoids the pitfalls associated with synchronizing on null. It also provides better visibility and avoids potential interference with other parts of the code that might synchronize on the same object.


6) Careful with toString()
When implementing the toString() method, avoid returning null. If there's no meaningful information to provide, it's better not to override the toString() method at all.

public class Person {
    private String name;
    private int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter methods for name and age

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // toString() method override
    @Override
    public String toString() {
        // Careful implementation to avoid returning null
        return "Person{name='" + name + "', age=" + age + '}';
    }

    public static void main(String[] args) {
        // Creating a Person object
        Person person = new Person("John Doe", 25);

        // Calling toString() method
        String personString = person.toString();

        // Displaying the result
        System.out.println("Person as String: " + personString);
    }
}

In this example, the toString() method is implemented to provide a meaningful representation of the Person object. It ensures that even if name is null, the string representation won't be null. Instead, it includes the class name and the available information.

Remember that if there's no meaningful information to provide in the toString() method, it might be better not to override it at all, letting the default implementation from Object handle it. This way, you avoid potential issues with null values in the string representation.


7) Watch for Broken Null Checks
Be cautious when checking for null. Broken null checks can occur if you mistakenly check an object for null but then access its properties or methods without verifying their nullability.

Here is an example of this:

public class NullCheckExample {

    public static class Person {
        private String name;

        // Constructor
        public Person(String name) {
            this.name = name;
        }

        // Getter method
        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        // Creating a Person object
        Person person = new Person("John Doe");

        // Simulating a broken null check
        if (person != null) {
            // Broken null check: Checking for null but not verifying nullability of getName()
            String personName = person.getName();
            System.out.println("Person Name: " + personName);
        } else {
            System.out.println("Person is null");
        }
    }
}

In this example, the Person class has a name property with a getter method. The main method attempts to check for null before accessing the name property, but it's a broken null check. If person were null, attempting to call getName() on it would result in a NullPointerException.

To avoid broken null checks, always ensure that not only the object itself is not null but also verify the nullability of the properties or methods you access. A correct way to handle this would be:

if (person != null) {
    String personName = person.getName();
    if (personName != null) {
        System.out.println("Person Name: " + personName);
    } else {
        System.out.println("Person Name is null");
    }
} else {
    System.out.println("Person is null");
}

In this corrected version, both the person object and its name property are checked for null before accessing getName().



8) Avoid Returning Null from Catch or Finally Blocks
Returning null from a catch or finally block can mask errors and force clients to handle error codes. Instead, throw exceptions or return meaningful results to indicate error conditions.

import java.util.Optional;

public class ExceptionHandler {

    // Method that may throw an exception
    public String performOperation() {
        try {
            // Some operation that may throw an exception
            // For simplicity, let's assume an exception is caught
            throw new RuntimeException("An example exception");
        } catch (RuntimeException e) {
            // Log the exception or perform necessary cleanup
            System.out.println("Exception caught: " + e.getMessage());
            return handleException();
        } finally {
            // Some cleanup code or finalization logic
            // Avoid returning null from finally block
        }
    }

    // Handle the exception and return a default value
    private String handleException() {
        return "Default Result";
    }

    // Method using Optional to avoid returning null
    public Optional<String> performOperationWithOptional() {
        try {
            // Some operation that may throw an exception
            // For simplicity, let's assume an exception is caught
            throw new RuntimeException("An example exception");
        } catch (RuntimeException e) {
            // Log the exception or perform necessary cleanup
            System.out.println("Exception caught: " + e.getMessage());
            return Optional.of(handleException());
        } finally {
            // Some cleanup code or finalization logic
            // Avoid returning null from finally block
        }
    }

    public static void main(String[] args) {
        ExceptionHandler handler = new ExceptionHandler();

        // Example without Optional
        String result = handler.performOperation();

        // Example using Optional
        Optional<String> optionalResult = handler.performOperationWithOptional();

        // Process the results
        System.out.println("Result without Optional: " + result);
        System.out.println("Result with Optional: " + optionalResult.orElse("Default Result"));
    }
}

In this example, the performOperation method may throw an exception, and the catch block handles the exception by invoking the handleException method. Instead of returning null from the catch block or the finally block, a default value is returned. 

Additionally, there's an alternative method performOperationWithOptional() that uses Optional to encapsulate the result, avoiding the need to return null. The calling code can then use Optional methods to handle the absence of a value gracefully.


9) Don't Null-Check Before instanceof
Avoid null-checking before using the instanceof operator, especially in methods like equals. The instanceof keyword returns false when given a null argument, so the null check is redundant.

public class Shape {
    // Some properties and methods for a generic shape
}

public class Circle extends Shape {
    // Additional properties and methods specific to a circle
}

public class Rectangle extends Shape {
    // Additional properties and methods specific to a rectangle
}

public class ShapeProcessor {

    public static void processShape(Shape shape) {
        // Don't null-check before instanceof

        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            System.out.println("Processing Circle");
            // Process Circle-specific logic
        } else if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            System.out.println("Processing Rectangle");
            // Process Rectangle-specific logic
        } else {
            System.out.println("Unknown Shape");
            // Handle unknown or generic shape logic
        }
    }

    public static void main(String[] args) {
        Shape circle = new Circle();
        Shape rectangle = new Rectangle();
        Shape unknownShape = new Shape();

        // Process shapes without null-checking before instanceof
        processShape(circle);
        processShape(rectangle);
        processShape(unknownShape);
    }
}

In this example, the processShape() method takes a Shape parameter and performs instanceof checks to determine the type of the shape. There is no explicit null-check before the instanceof checks, as instanceof is already capable of handling null gracefully. If the shape reference is null, the instanceof checks will return false for all types, and the appropriate default logic for an unknown shape can be executed.

This approach can make the code cleaner and more concise, avoiding unnecessary null checks in situations where null is a valid input or where null is explicitly handled elsewhere in the code.'


10) Use Proper Default Values
Null should not be considered a default value for anything. When designing domain objects, consider using null-safe design patterns, such as creating dummy objects like dummyUser or emptyOrder, to represent missing or default values.

public class User {

    private String name;

    // Constructor with proper default value
    public User(String name) {
        this.name = name != null ? name : "Unknown User";
    }

    // Getter method
    public String getName() {
        return name;
    }

    public static void main(String[] args) {
        // Creating a User with a null name
        User userWithNullName = new User(null);

        // Creating a User with a specified name
        User userWithSpecifiedName = new User("John Doe");

        // Displaying user names
        System.out.println("User with Null Name: " + userWithNullName.getName());
        System.out.println("User with Specified Name: " + userWithSpecifiedName.getName());
    }
}

In this example, the User class has a constructor that takes a name parameter. Instead of allowing null as a valid input for the user's name, the constructor checks if the provided name is not null. If it is null, it sets a default name, "Unknown User."

When creating instances of the User class, you ensure that a valid default name is always assigned, preventing unexpected NullPointerExceptions when accessing the name property. This is a simple demonstration of using proper default values in the context of a domain object (in this case, a user).

By following these best practices, you can minimize the risks associated with null in your Java code, improve code maintainability, and enhance overall code quality. Handling null effectively is an essential skill for writing robust and reliable Java applications.



Things to Remember

Here is the summary of Java null best practices. NULL is inevitable while coding but these best practices will certainly help

1) If your method returns a array then don't return null if there is no data available, instead return a zero length array.

2) If your method returns a Collection e.g. List or Set then return emptyList() or emptySet() instead of returning null, this will make client code cleaner and also help to avoid NullPpointerException.

3) If your method returns an instance of wrapper class e.g. Boolean, Integer or Double then never return null because if client is storing return value in a primitive filled then it will throw NPE when Java try to unbox null into primitive values.

4) Return Optional from a method which could return null in case of no result e.g. findByName() should return Optional instead of returning null.

5) Don't synchronize on a return value of a method, it will throw NPE if method return null.

6) Never return null from toString(), if you don't have any way to improve just don't override

7) Be careful with broken null check

8) Don't return null from catch or finally block. Returning null from a catch block often masks errors and requires the client to handle error codes.

9) Don't do null check before an instanceof operator e.g. in equals method. the instanceof keyword returns false when given a null argument.

10) Use proper default, null is not a good default value for anything. If you are creating domain object consider using null safe design pattern e.g. creating dummyUser, emptyOrder etc.

11) Using Optional, while I am not a big fan of using Optional to avoid NPE, there are some benefits of using Optional as it allows you to use Stream related methods like map() and flatMap(), you can also use Optional to indicate that a value may be null and the receive should check for null before calling any method on that. 


That's all about these 10 best practices of handling null better in Java. These are very practical tips and I use most of them in my daily coding life. Unfortunately it took me long time to learn them as there was no such article at that time or maybe I wasn't reading enough but one book which certainly helped me was Effective Java and then I discovered few practices like calling equals() on String literal on my own. You can learn and remember these best practices and I sure you will write better and less error prone code in Java, 

If you have any other best practices related to null, except using Optional then feel free to suggest in comments. I will write about why I don't like Optional so much for avoiding NullPointerException in Java. 

No comments:

Post a Comment

Feel free to comment, ask questions if you have any doubt.