Java Null Safe Equals with Objects.equals
java null safe equals: Learn how to perform null-safe equality checks in Java using Objects.equals and understand the tradeoffs of manual null handling.
When you call a.equals(b) directly in Java, a NullPointerException is thrown if a is null. This is a common source of runtime failures, especially when objects come from external sources like databases, APIs, or configuration files. The java null safe equals pattern solves this by using Objects.equals() from the standard library, which handles null arguments gracefully without requiring explicit checks in your code.
The Problem with Direct equals() Calls
Consider a simple method that compares two String values:
public boolean isSame(String first, String second) { return first.equals(second); }
If first is null, this line throws NullPointerException. Even if you are careful to check first for null, you still need to handle the case where second is null but first is not. The typical workaround is to write a manual null check:
public boolean isSame(String first, String second) { return first != null && first.equals(second); }
This works, but it only protects against first being null. If first is not null and second is null, first.equals(second) returns false without throwing, which is usually the desired behavior. However, the logic becomes more convoluted when you need to treat two null values as equal.
Using Objects.equals for Null-Safe Comparison
The java.util.Objects class provides a static method equals(Object a, Object b) that performs a null-safe equality check. Its implementation is roughly:
public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); }
This method returns true if both arguments are null, returns false if exactly one is null, and otherwise delegates to a.equals(b). Using it simplifies the earlier example:
import java.util.Objects; public boolean isSame(String first, String second) { return Objects.equals(first, second); }
Now the method is safe for any combination of null and non-null arguments. It returns true when both are null, false when one is null, and the result of equals otherwise.
How Objects.equals Handles the Null Cases
Understanding the exact behavior of Objects.equals is important. The expression (a == b) first checks reference equality. If both references point to the same object, it returns true immediately, which also covers the case where both are null. If they are not the same reference, the second part (a != null && a.equals(b)) runs. This short-circuits if a is null, returning false. If a is not null, it calls a.equals(b), which may return true or false based on the object's own equality contract.
This design means that Objects.equals never throws a NullPointerException due to the arguments themselves. The only way it can throw is if a.equals(b) itself throws, which would be a bug in the custom equals implementation.
Comparing Objects.equals with Manual Null Checks
Before Objects was introduced in Java 7, developers often wrote ternary expressions like:
public boolean isSame(String first, String second) { return first == null ? second == null : first.equals(second); }
This works but is less readable and easy to get wrong, especially when dealing with multiple fields. For example, comparing two Person objects with several fields requires nested ternaries or helper methods. Objects.equals reduces that boilerplate and makes the intent clear. It also centralizes the null-handling logic, so you do not accidentally forget a null check in one branch.
Using Objects.equals in Collections and Streams
Null-safe equality is particularly useful in functional-style code. For instance, when filtering a list to find an element that equals a given value, Objects.equals avoids the need for a lambda that manually checks for null:
List<String> names = Arrays.asList("Alice", null, "Bob"); String target = null; List<String> matches = names.stream() .filter(name -> Objects.equals(name, target)) .collect(Collectors.toList());
Here, the filter retains only the null element because Objects.equals(null, null) returns true. If you used name.equals(target) directly, you would get a NullPointerException when name is null. The same pattern applies when using Map lookups or when implementing custom equals methods for domain objects.
Performance and Maintainability Considerations
Objects.equals is a static method with no object allocation and negligible overhead. The reference equality check (a == b) is fast and often short-circuits the call. In performance-sensitive code, the cost is comparable to a manual null check plus an equals call. There is no reason to avoid it for the sake of speed.
From a maintainability perspective, using Objects.equals reduces the chance of introducing subtle bugs. Manual null checks scattered across codebases are easy to forget, and the compiler cannot help you catch them. By using a standard library method, you also improve readability because the behavior is well-defined and familiar to other developers.
Edge Cases: Arrays and Custom equals() Implementations
One important limitation is that Objects.equals delegates to the equals method of the first argument. For arrays, equals is reference equality, not content equality. So Objects.equals(new int[]{1,2}, new int[]{1,2}) returns false. To compare arrays by content, use Arrays.equals for primitive arrays or Arrays.deepEquals for nested arrays. If you are working with collections, List.equals and Set.equals already handle null elements correctly, so Objects.equals works as expected.
Another edge case is a custom equals implementation that does not follow the contract. For example, if a class overrides equals but does not handle null properly, Objects.equals will still throw a NullPointerException if the first argument is non-null and the second is null, because it calls a.equals(b). Therefore, Objects.equals is not a substitute for a well-written equals method; it only protects against the caller passing null arguments.
When Not to Use Objects.equals
There are situations where you need to distinguish between null and a default value. For example, if you are updating a database record and want to set a column to null only when the incoming value is explicitly null, you might need a manual check. Objects.equals treats two null values as equal, which is correct for equality comparisons but not for presence checks. In such cases, use explicit if (value == null) logic.
Also, if you are comparing objects that are never null, a direct equals call is simpler and slightly more efficient because it avoids the extra reference check. But the difference is negligible, and using Objects.equals consistently across a codebase can prevent future bugs when nulls are introduced later.
In summary, Objects.equals is the standard way to implement java null safe equals in modern Java. It is concise, safe, and performs well. Use it whenever you need to compare two objects where either could be null, and reserve manual null checks for cases where you need to treat null as a distinct value.