Java: how to handle null values more gracefully?

Introduction

         In our development process, we often see that there are null value judgments everywhere in the project. These judgments will make people feel confused. It is likely that its emergence has nothing to do with the current business logic. But it will give you a headache. Sometimes, the more terrible thing is that the system will throw null pointer exceptions because of these null values, resulting in problems in the business system. In this article, I have summarized several methods of dealing with null values, hoping to be helpful to readers.

Null value in business

scene                 

         There is a UserSearchService to provide user query functions:

public interface UserSearchService{
  List<User> listUser();

  User get(Integer id);
}

Problem site

         For object-oriented languages, the level of abstraction is particularly important. Especially the abstraction of interface, which accounts for a large proportion in design and development. We hope to make interface oriented programming as much as possible.

From the interface method described above, it can be inferred that it may contain the following two meanings:

  • listUser(): query the user list

  • get(Integer id): query a single user

In all development, the TDD mode advocated by XP can well guide us to define the interface, so we take TDD as the "promoter" of code development.

For the above interfaces, potential problems are found when we use TDD to test case first:

  • If listUser() has no data, does it return an empty collection or null?

  • get(Integer id) if there is no such object, will it throw an exception or return null?

In depth listUser research

Let's discuss it first

listUser()

For this interface, I often see the following implementations:

public List<User> listUser(){
    List<User> userList = userListRepostity.selectByExample(new UserExample());
    if(CollectionUtils.isEmpty(userList)){//spring   util utility class
      return null;
    }
    return userList;
}

The return of this code is null. From my many years of development experience, it is best not to return null for the return value of a collection, because if NULL is returned, it will bring a lot of trouble to the caller. You will leave this call risk to the caller to control.

If the caller is a cautious person, he will make a conditional judgment on whether it is null. If he is not cautious, or he is a fanatic of interface oriented programming (of course, interface oriented programming is the right direction), he will call the interface according to his own understanding without judging whether it is null. If so, it is very dangerous, and it is likely to have null pointer exception!

According to Murphy's Law:   "Problems that are likely to occur will certainly appear in the future!"

Based on this, we optimize it:

public List<User> listUser(){
    List<User> userList = userListRepostity.selectByExample(new UserExample());
    if(CollectionUtils.isEmpty(userList)){
      return Lists.newArrayList();//The ways provided by the guava class library
    }
    return userList;
}

For the interface (List listUser()), it will certainly return List. Even if there is no data, it will still return List (there are no elements in the set);

Through the above modifications, we have successfully avoided the possible null pointer exception, which is safer!

In depth study of get method

For interfaces

User get(Integer id)

What you can see is that if I give an id, it will definitely return User to me. But the fact is very likely not.

I've seen implementations:

public User get(Integer id){
  return userRepository.selectByPrimaryKey(id);//Get the entity object directly from the database by id
}

I believe many people will write like this.

When you pass the code, you know that its return value is likely to be null! But we can't distinguish the interface we pass!

This is a very dangerous thing. Especially for callers!

My suggestion is to supplement the document when the interface is explicit. For example, for the description of exceptions, use the annotation @ exception:

public interface UserSearchService{

  /**
   * Obtain user information according to user id
   * @param id User id
   * @return User entity
   * @exception UserNotFoundException
   */
  User get(Integer id);

}

After we have explained the interface definition, the caller will see that if this interface is invoked, it is likely to throw an exception such as "UserNotFoundException (not able to find the user)".

In this way, you can see the interface definition when the caller calls the interface, but this method is "weak prompt"!

If the caller ignores the comments, there may be a risk to the business system, which may lead to 100 million!

In addition to the above "weak prompt" method, another way is that the return value may be empty. What should we do?

I think we need to add an interface to describe this scenario

Introduce the option of jdk8 or use the option of guava. See the following definitions:

public interface UserSearchService{

  /**
   * Obtain user information according to user id
   * @param id User id
   * @return User entity, which may be the default value
   */
  Optional<User> getOptional(Integer id);
}

Optional has two meanings: existence or default.

Then, by reading the interface getOptional(), we can quickly understand the intention of the return value. This is actually what we want to see. It removes ambiguity.

Its implementation can be written as:

public Optional<User> getOptional(Integer id){
  return Optional.ofNullable(userRepository.selectByPrimaryKey(id));
}

Deep into the reference

Through the above description of all interfaces, can you confirm that the input parameter id must be passed? I think the answer should be: not sure. Unless otherwise stated in the documentation notes of the interface.

How to constrain the input parameters?

I recommend two ways:

  • Mandatory constraint

  • Document constraint (weak prompt)

1. For mandatory constraints, we can make strict constraint declarations through jsr 303:

public interface UserSearchService{
  /**
   * Obtain user information according to user id
   * @param id User id
   * @return User entity
   * @exception UserNotFoundException
   */
  User get(@NotNull Integer id);

  /**
   * Obtain user information according to user id
   * @param id User id
   * @return User entity, which may be the default value
   */
  Optional<User> getOptional(@NotNull Integer id);
}

Of course, it should be verified with AOP operation, but spring has provided a good integration scheme, so I won't repeat it here.

2. Document constraints

In many cases, we will encounter legacy code. For legacy code, the possibility of overall transformation is very small.

We prefer to explain the interface by reading the implementation of the interface.

jsr 305 specification gives us a way to describe interface input parameters (the library com. Google. Code. Findbugs: jsr305 needs to be introduced):

The annotation @ Nullable @Nonnull @CheckForNull can be used for interface description. For example:

public interface UserSearchService{
  /**
   * Obtain user information according to user id
   * @param id User id
   * @return User entity
   * @exception UserNotFoundException
   */
  @CheckForNull
  User get(@NonNull Integer id);

  /**
   * Obtain user information according to user id
   * @param id User id
   * @return User entity, which may be the default value
   */
  Optional<User> getOptional(@NonNull Integer id);
}

Summary

Through the return value of empty set, Optional,jsr 303 and jsr 305, our code can be more readable and have a lower error rate!

  • Empty collection return value: if a collection returns a value like this, unless you really have a reason to convince yourself, you must return an empty collection instead of null

  • Optional: if your code is jdk8, introduce it! If not, use Guava's optional or upgrade the JDK version! It can greatly increase the readability of the interface!

  • jsr 303: if a new project is under development, try this! There must be a special feeling!

  • jsr 305: if the old project is in your hands, you can try to add this kind of document annotation, which is helpful for your later refactoring, or if new functions are added, your understanding of the old interface!

Empty object mode

scene

Let's take a look at a DTO conversion scenario, object:

@Data
static class PersonDTO{
  private String dtoName;
  private String dtoAge;
}

@Data
static class Person{
  private String name;
  private String age;
}

The requirement is to convert the Person object into a Person dto and then return it.

Of course, for the actual operation, if the return Person is empty, it will return null, but the Person DTO cannot return null (especially the DTO returned by the Rest interface).

Here, we only focus on the conversion operation. See the following code:

@Test
public void shouldConvertDTO(){

  PersonDTO personDTO = new PersonDTO();

  Person person = new Person();
  if(!Objects.isNull(person)){
    personDTO.setDtoAge(person.getAge());
    personDTO.setDtoName(person.getName());
  }else{
    personDTO.setDtoAge("");
    personDTO.setDtoName("");
  }
}

Optimization modification

For such data conversion, we know that the readability is very poor. The judgment of each field is set to an empty string ("") if it is empty

In another way of thinking, we get the data of the Person class and then perform the assignment operation (setXXX). In fact, it doesn't matter who implements the Person.

Then we can create a Person subclass:

static class NullPerson extends Person{
  @Override
  public String getAge() {
    return "";
  }

  @Override
  public String getName() {
    return "";
  }
}

It exists as a special case of Person. If Person is empty, some default behaviors of get * will be returned

Therefore, the code can be modified to:

@Test
 public void shouldConvertDTO(){

   PersonDTO personDTO = new PersonDTO();

   Person person = getPerson();
   personDTO.setDtoAge(person.getAge());
   personDTO.setDtoName(person.getName());
 }

 private Person getPerson(){
   return new NullPerson();//If Person is null  , An empty object is returned
 }

The getPerson() method can be used to obtain the possible objects of Person according to the business logic (for the current example, if Person does not exist, return the special case NUllPerson of Person). If it is modified to this way, the readability of the code will become very strong.

You can optimize with Optional

The disadvantage of the empty object model is that it needs to create a special case object, but if there are many special cases, do we need to create multiple special case objects? Although we also use the object-oriented polymorphism, we still need to think about this model again if we really need to create multiple special case objects due to the complexity of the business, It can lead to code complexity.

For the above code, you can also use Optional for optimization.

@Test
  public void shouldConvertDTO(){

    PersonDTO personDTO = new PersonDTO();

    Optional.ofNullable(getPerson()).ifPresent(person -> {
      personDTO.setDtoAge(person.getAge());
      personDTO.setDtoName(person.getName());
    });
  }

  private Person getPerson(){
    return null;
  }

I think the Optional use of null value is more appropriate. It is only applicable to the "whether it exists" scenario.

If you only judge the existence of control, I suggest using Optional

Proper use of Optioanl

Option is so powerful that it expresses the most primitive features of the computer (0 or 1). How can it be used correctly!

Optional do not use as a parameter

If you write a public method that specifies some input parameters, some of which can be null, can you use Optional at this time?

My advice is: don't use it like this!

for instance:

public interface UserService{
  List<User> listUser(Optional<String> username);
}

The method listUser in this example may tell us that we need to query all data sets according to username. If username is empty, we also need to return all user sets

When we see this method, we will feel some ambiguity:

"If username is absent, do you want to return an empty set or all user data sets?"

Optioanl is a branch judgment. Should we focus on Optional or Optional.get()?

My advice to you is, if you don't want such ambiguity, don't use it!

If you really want to express two meanings, split it into two interfaces:

public interface UserService{
  List<User> listUser(String username);
  List<User> listUser();
}

I think this semantics is stronger and can better meet the "single responsibility" in the software design principle.

If you think your input parameter is really necessary and may pass null, please use jsr 303 or jsr 305 for description and verification!

Please remember! Optional cannot be used as parameter of input parameter!

Optional as return value

When an entity returns

Can Optioanl be used as the return value?

In fact, it is very satisfied with the existence of this semantics.

For example, you need to obtain user information according to the id. this user may or may not exist.

You can use this:

public interface UserService{
  Optional<User> get(Integer id);
}

When calling this method, the caller knows that the data returned by the get method may not exist. This can make more reasonable judgments and better prevent null pointer errors!

Of course, if the business party really needs to query the User according to the id, do not use it like this. Please explain the exceptions you want to throw

Only when it is considered that it is reasonable to return null, can the Optional return be performed

Return of collection entities

Not all return values can be used like this! If you are returning a collection:

public interface UserService{
  Optional<List<User>> listUser();
}

Such a return result will confuse the caller. Do I still use isEmpty to judge after I judge Optional?

This brings ambiguity in the return value! I don't think it's necessary.

We need to agree that for the return value of a set such as List, if the set is really null, please return an empty set (Lists.newArrayList);

Using the Optional variable

Optional<User> userOpt = ...

If you have such a variable userOpt, remember:

  • You must not use get directly. If you use it in this way, you will lose the meaning of option itself (such as userOp.get())

  • Don't use getOrThrow directly. If you have such a requirement: throw an exception if you can't get it. It is necessary to consider whether the interface design of the call is reasonable

Use in Getters

For a java bean, all properties may return null. Do you need to rewrite all getter s to become Optional types?

My advice to you is not to abuse option like this

Even though the getter s in my Java beans are Optional, because there are too many Java beans, more than 50% of your code will be judged optically, which will pollute the code. (I want to say that in fact, all fields in your entity should have business meanings. I will seriously think about the value of its existence and can't abuse it because of the existence of option)

We should pay more attention to the business, not just the judgment of null value.

Please do not abuse Optional. In getter s

Summary

The use of Optional can be summarized as follows:

  • When the use value is empty and does not originate from an error, you can use Optional!

  • Optional, do not use for collection operations!

  • Don't abuse Optional, such as in the getter of Java beans!   

Source: https://lrwinx.github.io/

Tags: Java

Posted on Sun, 05 Dec 2021 20:33:05 -0500 by Toxinhead