This content originally appeared on Level Up Coding - Medium and was authored by Görkem Gök
The null reference was invented by Tony Hoare[1] in 1965 and he called it “my billion-dollar mistake”[2]. There are some languages such as Dart that have sound null safety that prevents unintentional access to null variables [3]. Unfortunately, we don’t have it in Java. NullPointerException is a part of our life as Java developers. We need some techniques and principles to avoid that billion-dollar mistake.
There are 3 main sources of NullPointerException.
- Null method returns
- Null method arguments
- Null object fields
To avoid these you can do the followings:
- Don’t return a null value from a method by using Optional and Throwing exceptions,
- Don’t accept a null argument to a method by using the method overloading technique,
- Design DTOs (Data Transfer Objects) with no or minimum null fields by creating separate DTOs for each use case.
Let’s deep dive into them.
Don’t Return Null
Great idea, right?
If you don’t return any null value from our methods then you wouldn’t have to deal with null values. However, the reality is different. There will be null values. If your method is looking for a user by id then there is a chance that there is no user with that id.
We should define two different cases before continuing.
- Non-existing values and,
- Non-returnable values.
Non-existing values are straightforward. When the value doesn’t exist it is a non-existing value. The following can be some examples;
- There is no record with the specified id,
- There is no file with the specified path and name in the filesystem,
- There is no property in the properties file.
Non-returnable values might be in many forms;
- There is a record with that id but the user has no access to that record,
- The file exists but is too big to return
- The property is in the file but the format is invalid.
Returning a null value can mean any of them. This is why you should not return null and do one of the followings.
1. Use Optionals (Java 8+)
The Optional class was introduced with Java 8.
An Optional is a container object which may or may not contain a non-null value. (Javadoc)
Use Optional for non-existing values and avoid using it for non-returnable values.
There is only one meaning of Optional.empty. The value doesn't exist.
You might think of some cases in which you can use it for different meanings. The following method is an example of that case.
Optional<User> getUserIfHasAccess(String myId, String userId);
Here, there might be two meanings of an Optional.empty.
- The user doesn’t exist or,
- I don’t have access to that user.
Please note that this is not a good way to create a method. The method does multiple things and it is against Single Responsibility Principle (SRP). We discover a close relationship between Optional and SRP. When you give only one meaning to Optional it makes it easy to see SRP violations. To fix that you should create two methods
boolean hasAccess(String myId, String userId);
Optional<User> getUser(String userId);
Now the Optional has only one meaning which is a non-existing user and you obey SRP.
How to User Optional Properly
Create every method with an Optional return and remove the Optional if you are sure there is no case that the method returns a null value.
The most significant benefit of an Optional return is that it makes the developers check the presence of the value.
interface UserRepository {
User getUser(String userId);
}
...
//Bad
//Highly possible NPE
var email = userRepository.getUserName(id).getEmail();
...
//Good
var user = userRepository.getUserName(id);
if (user != null) {
var email = user.getEmail();
if (email != null) {
//send email
} else {
throw new EmailSendingException("No Email");
}
}
...
//Best
interface UserRepository {
Optional<User> getUser(String userId);
}
...
var email = userRepository.getUserName(id).map(User::getEmail)
.orElseThrow(() -> new EmailSendingException("No Email"));
Avoid using Optional::get
If you use Optional.get() you should also check if the value is present by calling Optional.isPresent().
//with optional
if (value.isPresent()) {
use(value.get())
}
//with null check
if (value != null) {
use(value)
}
Checking isPresent() can be missed as you can miss null checks.
Whenever you see a Optional.get() consider using chaining methods.
The real power of Optional reveals itself when it is used with chaining methods such as map , orElse , orElseThrow.
Let’s say you have a complex User model with nullable fields and a getUser method that returns that User model:
class Address {
@Nullable private String street;
@Nullable private String city;
//Getters
}
class User {
@Nullable Address address;
//Getters
}
...
static Optional<User> getUser() {
return Optional.empty();
}
...
When you want to get the city of the user you can write a method as the following:
var userCity = "unknown";
Optional<User> userOptional = getUser();
if (userOptional.isPresent()) {
User user = userOptional.get();
Address address = user.getAddress();
if (address != null && address.getCity() != null) {
userCity = address.getCity();
}
}
There are still null checks. It is not readable and error-prone.
Use optional chaining instead:
var userCity = getUser()
.map(User::getAddress)
.map(Address::getCity)
.orElse("unknown");
This code does the same thing as the above one. I believe there is no need for more words.
Avoid using Optional.orElse(null)
When you use it you introduce another null.
Instead of using Optional.orElse(null) you can do the followings;
- Throw an exception with Optional::orElseThrow ,
- Pass a default value like the example above (orElse("unkown") ),
- Make an execution inside Optional::ifPresent
- Rereturn Optional with the mapped field.
Delay getting the value from Optional as much as possible.
2. Throw Exception
All the best practices of throwing and handling exceptions in Java also apply to this case.
If you have an unexpected case you can throw an exception for both non-existing and non-returnable values rather than returning null.
Let’s say you have a method to get the image file but you have a size limit.
Optional<ImageFile> getImage(String imageId) throws FileOutOfSizeException;
This is completely fine until the file size involves business logic. If you have some logic on file size then you should create another method that gets the file size or checks if the size is acceptable.
boolean isSizeAcceptable(String imageId); or,
long getFileSize(String imageId);
The thing that you should avoid is returning an Optional.empty() after catching an exception as in the example;
Optional<ImageFile> getImage(String imageId) {
try{
...
return Optional.of(imageFile);
} catch (IOException e)
Optional.empty() // DON'T do this. Instead rethrow the exception
}
}
Don’t Accept Null
It seems very easy to apply this principle but what if you have a nullable field in our methods?
Use overloading. Let’s say you have a method with a nullable field as the following example;
Optional<ImageFile> getImage(String imageId, @Nullable ImageFilter imageFilter) {
var filter = imageFilter;
if (filter == null) {
filter = ImageFilters.NO_FILTER;
}
...
}
Here you accept null ImageFilter and use a default value for the null filter value. In this case, you should overload the method and create another method with one argument as the following example;
Optional<ImageFile> getImage(String imageId) {
return getImage(imageId, ImageFilters.NO_FILTER);
}
Optional<ImageFile> getImage(String imageId, ImageFilter imageFilter) {
...
}
You got rid of the null check as you see. You might not always have a default value logic as here then you should try to move null check logic into the overloading method.
DTOs with no/minimum Nullable Fields
This is the last place you need to deal with.
There is a basic rule of thumb.
Don’t reuse model classes.
So what happened “Don’t Repeat Yourself (DRY)” principle?
Most of the time, this is a misunderstood principle.
DRY is for repeated code for the same logic. It is not for repeated code for the data that seem the same but are not the same.
Let’s consider a user info data object;
class User {
private String id;
private String email;
private File profileImage;
private Address address;
//Constructor, gettes and setters
}
When you reuse this object for different purposes you will pass the null values to different fields in different use cases.
Let’s say you have two different methods as in the following example that returns this object.
User getUserProfile(String userId);
...
User getUserAddress(String userId);
In the first case the Address field will be null and in the second case the Profile field will be null.
To avoid this you should create two different DTOs;
class UserProfile {
private String id;
private String email;
private Profile profile;
//Constructor, gettes and setters
}
...
class UserAddress {
private String id;
private String email;
private Address address;
//Constructor, gettes and setters
}
Sometimes, the fields might be the same but again you should create different DTOs. Let’s say you have a method (getFollowingUsersFor) that returns other users that the specified user is following.
class User {
private String id;
private String email;
//Constructor, gettes and setters
}
Optional<User> getUser(String userId);
List<User> getFollowingUsersFor(String userId);
Here it seems that you can use theUser class as the return type for both of the methods. However, it will start getting messy soon. What happens if you want to add an extra field isFollowingBack to the following users? Would you add it to the User class? If you add, that field would be null when returned from the getUser method.
So you should create another DTO for this use case as the following example;
class User {
private String id;
private String email;
//Constructor, gettes and setters
}
class FollowingUser {
private String id;
private String email;
//Constructor, gettes and setters
}
You should create different DTOs for different use cases even if the fields are the same.
At first, it might seem like an overhead to create a new DTO for each use case but eventually, you will have the benefit.
Conclusion
Unfortunately, none of the tips above is going to prevent NullPointerExceptions completely. You should try to obey many good and best practices as much as possible while creating architecture and designing your program. I have tried to give some very basic rules that can easily be applied while writing code and during code reviews. Following these tips will definitely help you avoid some common mistakes.
Thanks for reading so far.
References
Level Up Coding
Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the Level Up Coding publication
- 🔔 Follow us: Twitter | LinkedIn | Newsletter
🚀👉 Join the Level Up talent collective and find an amazing job
Simple Tips for Null-Safety in Java was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Görkem Gök
Görkem Gök | Sciencx (2023-01-30T01:54:36+00:00) Simple Tips for Null-Safety in Java. Retrieved from https://www.scien.cx/2023/01/30/simple-tips-for-null-safety-in-java/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.