How to Avoid NULL pointer exceptions using Java 8 Optional
Preface
Null pointers are the most common and annoying exceptions. To prevent null pointer exceptions, you are not allowed to write a large number of non-null judgments in the code.
Java 8 introduces a new Optional class. It is used to avoid the emergence of null pointers and does not need to write a large numberif(obj!=null)The premise is that you have to use Optional to hold the data. It is a container that wraps the object.
It is said that programmers who have never encountered null pointer exceptions are not Java programmers. null does cause many problems. Java 8 introducesjava.util.OptionalThe new class can avoid many problems caused by null.
Let's look at the hazards caused by a null reference. First, create a Computer class. The structure is shown in:
What happens when we call the following code?
String version = computer.getSoundcard().getUSB().getVersion();
The above Code seems to be okay, but many computers (such as Raspberry Pi) do not actually have sound cards, so callgetSoundcard()The method may throw a null pointer exception.
A common but bad method is to return a null reference to indicate that the computer does not have a sound card, but this means that the getUSB () method will be called for an empty reference, it is obvious that a control exception will be thrown during the program running, resulting in the program to stop running. Think about it. When your program runs on the client computer, how embarrassing is this error suddenly?
The great computer science Tony Hoare once wrote: "I think null references were created in 1965, resulting in a loss of billions of dollars. The greatest temptation to use null references is its convenience. "
So how can we avoid a null pointer exception when running the program? You need to be vigilant and constantly check for possible null pointers, as shown below:
String version = "UNKNOWN";if(computer != null) { Soundcard soundcard = computer.getSoundcard(); if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null){ version = usb.getVersion(); } } }
However, you can see that the above Code has too many null checks, and the entire code structure becomes very ugly. However, we have to make sure that no NULL pointer is displayed during system operation. If a large number of such blank references are found in our business code, it will be annoying and cause our code to be poorly readable.
If you forget to check whether the value to be given is null, null reference also has a large potential problem. In this article, I will prove that it is not good to use a null reference as a representation of a value that does not exist. We need a better model to indicate that the value does not exist, instead of using null reference.
Java 8 introduces a new class calledjava.util.Optional<T>The design of this class is inspired by the Haskell language and Scala language. This class can contain any value, as shown in the following figure and code. You can think of Optional as a value that may contain a value. If Optional does not contain a value, it is null.
public class Computer { private Optional<Soundcard> soundcard; public Optional<Soundcard> getSoundcard() { ... } ...}public class Soundcard { private Optional<USB> usb; public Optional<USB> getUSB() { ... }}public class USB{ public String getVersion(){ ... }}
The above Code shows that a computer may package a sound card (sound card may or may not exist ). The sound card may also contain a USB port. This is an improvement method. This model can clearly reflect that a given value does not exist.
But what should I do?Optional<Soundcard>What about this object? After all, you want to obtain the USB port number. The Optional class contains some methods to check whether a value exists. Compared with the null reference, the Optional class forces you to perform value-related processing to avoid null pointer exceptions.
It must be noted that the Optional class does not replace the null reference. On the contrary, it is designed to make the API easier to understand. When you see a function signature, you can determine whether the value to be passed to this function may not exist. This prompted you to open the Optional class to handle the actual value.
Optional Mode
With so many worships, let's look at some code! Let's take a look at how Optional is used to rewrite the traditional null reference detection. At the end of this article, you will understand how to use Optional.
String name = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");
Create Optional object
You can create an empty Optional object:
Optional<Soundcard> sc = Optional.empty();
Next, create an Optional that contains non-null values:
SoundCard soundcard = new Soundcard();Optional<Soundcard> sc = Optional.of(soundcard);
If the sound card is null, a null pointer exception will be thrown immediately (this is better than that thrown when obtaining the sound card attribute ).
By using ofNullable, you can create an Optional object that may contain null references:
Optional<Soundcard> sc = Optional.ofNullable(soundcard);
If the sound card is a null reference, the Optional object is empty.
Processing of values in Optional
Now that you have an Optional object, you can call the corresponding method to check whether the value in the Optional object exists. Compared with null detection, we can use the ifPresent () method, as shown below:
Optional<Soundcard> soundcard = ...;soundcard.ifPresent(System.out::println);
In this way, the null check is unnecessary. If the Optional object is empty, nothing will be printed.
You can also useisPresent()Method to check whether the Optional object exists. In addition, there is a get () method that can return the value contained in the Optional object, if any. Otherwise, a NoSuchElementException exception is thrown. These two methods can be used together like the following to avoid exceptions:
if(soundcard.isPresent()){ System.out.println(soundcard.get());}
However, this method is not recommended (it has no improvement over null detection). Next we will discuss the common method of work.
Return default values and related operations
When null is encountered, a common operation returns a default value. You can use a ternary expression to achieve this:
Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard : new Soundcard("basic_sound_card");
If you use the Optional object, you canorElse()Use rewrite. When Optional is emptyorElse()You can return a default value:
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
Similarly, when Optional is null, you can use orElseThrow () to throw an exception:
Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);
Filter specific values
We often call an object method to determine its attributes. For example, you may need to check whether the USB port number is a specific value. To ensure security, you need to check whether the medical device pointing to USB is null before callinggetVersion()Method, as shown below:
USB usb = ...;if(usb != null && "3.0".equals(usb.getVersion())){ System.out.println("ok");}
If Optional is used, you can use the filter function to override it:
Optional<USB> maybeUSB = ...;maybeUSB.filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));
The filter method requires a predicate pair as a parameter. If the value in Optional exists and satisfies predicate, the filter function returns a value that meets the condition. Otherwise, an empty Optional object is returned.
Extract and convert data using the map Method
A common mode is to extract some attributes of an object. For example, for a Soundcard object, you may need to obtain its USB object and then determine its version number. Generally, our implementation method is as follows:
if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null && "3.0".equals(usb.getVersion()){ System.out.println("ok"); }}
We can use the map method to override this check null, and then extract the object of the object type.
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
This is the same as the map function that uses stream. To use stream, you must pass a function as a parameter to the map function. The passed function will be applied to every element in stream. When stream is in time and space, nothing will happen.
The value in Optional will be converted by the passed function (here is a function for getting USB from the sound card ). If the Optional object is time-space, nothing will happen.
Then, we use the map method and filter method to filter out the sound card with a USB Version Not 3.0.
maybeSoundcard.map(Soundcard::getUSB) .filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));
In this way, our code starts to look a bit like what we gave at the beginning, without the null check.
Use the flatMap function to pass Optional objects
Now we have introduced an example of code reconstruction using Optional. How can we implement the following code in a safe way?
String version = computer.getSoundcard().getUSB().getVersion();
Note that the above Code extracts another object from one object and can be implemented using the map function. In the previous article, we set the Computer to containOptional<Soundcard>Object. Soundcard containsOptional<USB>So we can refactor the code
String version = computer.map(Computer::getSoundcard) .map(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");
Unfortunately, the above Code will be compiled incorrectly. Why? The computer variable isOptional<Computer>So it is okay to call the map function. HowevergetSoundcard()The method returnsOptional<Soundcard>Object, the returned isOptional<Optional<Soundcard>>Type object. The second map function is called and the result is called.getUSB()The function becomes invalid.
The following figure describes this scenario:
The source code of the map function is implemented as follows:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } }
We can see that the map function will be called again.Optional.ofNullable(), Resulting in the returnOptional<Optional<Soundcard>>
Optional provides the flatMap function, which is designed to convert the value of the Optional object (like the map operation) and compress the Optional into one. The following figure shows the differences in type conversion between Optional objects by calling map and flatMap:
Therefore, we can write as follows:
String version = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");
The first flatMap ensures that the returnedOptional<Soundcard>InsteadOptional<Optional<Soundcard>> , The second flatMap implements the same function and returnsOptional<USB>. Note that the third callmap()BecausegetVersion()Returns a String object instead of an Optional object.
We finally adapted the ugly code of the nested null check that we just started to use to rewrite the Code with high readability and avoid code with null pointer exceptions.
Summary
In this article, we use the new class provided by Java 8.java.util.Optional<T>. The original intention of this class is not to replace null references, but to help the designer design a better API. As long as you read the function signature, you can know whether the function accepts a value that may or may not exist. In addition, Optional forces you to open Optional and process whether the value exists. This prevents potential NULL pointer exceptions in your code.
Well, the above is all the content of this article. I hope the content of this article has some reference and learning value for everyone's learning or work. If you have any questions, please leave a message to us, thank you for your support.