Preface
Null pointers are the most common and annoying exceptions we hate. In order to prevent null pointers from exceptions, you must not write a lot of non-null judgments in your code.
Java 8 introduces a new Optional class. To avoid the occurrence of null pointers, there is no need to write a large number of if(obj!=null) judgments, as long as you have to load the data in Optional, it is a container that wraps the object.
It is said that no programmer who has encountered a null pointer exception is not a Java programmer, and null has indeed caused many problems. Java 8 introduces a new class called java.util.Optional to avoid many problems caused by null.
Let's see what harm a null reference can cause. First create a class Computer, the structure is shown in the figure below:
What happens when we call the following code?
String version = computer.getSoundcard().getUSB().getVersion();
The above code seems to be fine, but many computers (such as Raspberry Pi) actually do not have sound cards, so calling getSoundcard() method will definitely throw a null pointer exception.
A regular 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 on an empty reference, which will obviously throw a control exception during the program running, causing the program to stop running. Think about it, how embarrassing it is to suddenly appear when your program is running on a client computer?
The great computer science Tony Hoare once wrote: "I think null citations were created in 1965, which resulted in a billion dollars in losses. The biggest temptation for me when using null citations was that it was easy to implement."
So how can we avoid null pointer exceptions when the program is running? You need to be alert and constantly check for possible null pointers, like this:
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. But we have to use this judgment to ensure that there will be no null pointers when the system is running. It is simply annoying to judge if there are a lot of such empty references in our business code, and it also leads to poor readability of our code.
If you forget to check whether the value is empty, null references also have great potential problems. In this article I will prove that using null references as a representation where values do not exist is a bad way. We need a better model that indicates that the value does not exist, rather than using null references again.
Java 8 introduced a new class called java.util.Optional<T> , which was inspired by the Haskell language and Scala language. This class can contain an arbitrary value, as shown in the figure and code below. You can think of Optional as a value that may contain a value. If Optional does not contain a value, then it is empty, as shown in the figure below.
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 replace a sound card (the sound card may or may not exist). The sound card may also include a USB port. This is an improvement method, and the model can more clearly reflect that a given value may not exist.
But how to deal with the Optional<Soundcard> object? After all, what you want to get is the USB port number. It's very simple. The Optional class contains some methods to deal with whether the value exists. Compared with null references, the Optional class forces you to handle whether the value is related, thus avoiding null pointer exceptions.
It should be noted that the Optional class does not replace null references. On the contrary, to make the designed API easier to understand, when you see the signature of a function, you can determine whether the value to be passed to the function may not exist. This prompts you to open the Optional class to handle the actual value.
Adopt Optional mode
After saying so much, let’s take a look at some code! Let's first look at how to use Optional to rewrite 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 Objects
An empty Optional object can be created:
Optional<Soundcard> sc = Optional.empty();
Next is to create an Optional containing non-null values:
SoundCard soundcard = new Soundcard();Optional<Soundcard> sc = Optional.of(soundcard);
If the sound card is null, the null pointer exception will be thrown immediately (this is better than just throwing it when you get 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 there is an Optional object, you can call the corresponding method to handle whether the value in the Optional object exists. Compared to null detection, we can use the ifPresent() method, like this:
Optional<Soundcard> soundcard = ...;soundcard.ifPresent(System.out::println);
This way, there is no need to do null detection. If the Optional object is empty, then any information will not be printed.
You can also use the isPresent() method to see if the Optional object really exists. In addition, there is also a get() method that returns the included values in the Optional object, if present. Otherwise, a NoSuchElementException will be 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 compared to null detection). Below we will discuss the usual ways of working.
Returns default values and related operations
When encountering null, a regular operation is to return a default value, which you can use ternary expressions to implement:
Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard : new Soundcard("basic_sound_card"); If you use Optional object, you can use orElse() to override. When Optional is empty orElse() can return a default value:
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut")); Similarly, when Optional is empty, orElseThrow() can be used to throw exceptions:
Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);
Use filter to filter specific values
We often call an object method to judge its properties. For example, you may need to check whether the USB port number is a specific value. For safety reasons, you need to check whether the medical use pointing to USB is null, and then call getVersion() method, like this:
USB usb = ...;if(usb != null && "3.0".equals(usb.getVersion())){ System.out.println("ok");} If you use Optional, you can use the filter function to rewrite:
Optional<USB> maybeUSB = ...; maybeUSB.filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok")); The filter method requires a predicate opposite as a parameter. If the value in Optional exists and satisfies predict, the filter function will return a value that satisfies the condition; otherwise, an empty Optional object will be returned.
Use map method to extract and convert data
A common pattern is to extract some properties of an object. For example, for a Soundcard object, you may need to get its USB object and then determine its version number. Usually our implementation is like this:
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 detection null and then extract the object of object type.
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
This is the same as using the map function using stream. Using stream requires passing a function as a parameter to the map function, and the passed function will be applied to each element in the stream. When streaming space and time, nothing happens.
The value contained in Optional will be converted by the function passed in (here is a function that gets USB from the sound card). If the Optional object is space-time, nothing will happen.
Then, we combine the map method and the filter method to filter out sound cards with USB version number 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 null detection.
Passing Optional object using flatMap function
Now, an example of how to refactor the code using Optional has been introduced. So how should we implement the following code in a safe way?
String version = computer.getSoundcard().getUSB().getVersion();
Note that the above code is all extracting another object from one object, which can be implemented using the map function. In the previous article, we set up a Optional<Soundcard> object in Computer, and Soundcard contains an Optional<USB> object, so we can refactor the code in this way
String version = computer.map(Computer::getSoundcard) .map(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN"); Unfortunately, the above code compiles errors, so why? The computer variable is of type Optional<Computer> , so it has no problem calling the map function. However, getSoundcard() method returns an Optional<Soundcard> object, which returns an object of type Optional<Optional<Soundcard>> . After the second map function is called, the call to getUSB() function becomes illegal.
The following figure describes this scenario:
The source code implementation of the map function is 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)); } } It can be seen that the map function will call Optional.ofNullable() again, resulting in the return of Optional<Optional<Soundcard>>
Optional provides the flatMap function, which is designed to convert the value of the Optional object (like a map operation) and then compress a two-level Optional into one. The following figure shows the difference between Optional objects in the type conversion by calling map and flatMap:
So we can write this:
String version = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN"); The first flatMap ensures that the return is Optional<Soundcard> rather than Optional<Optional<Soundcard>> , and the second flatMap implements the same function so that the return is Optional<USB> . Note that map() is called the third time, because getVersion() returns a String object instead of an Optional object.
We finally rewrite the ugly code of nested null checks we just started using, which is highly readable, and also avoids the occurrence of null pointer exceptions.
Summarize
In this article, we adopt the new class java.util.Optional<T> provided by Java 8. The original intention of this class is not to replace null references, but to help designers design better APIs. Just read the function's signature and know whether the function accepts a value that may or may not exist. Additionally, Optional forces you to turn on Optional and then handle whether the value exists, which makes your code avoid potential null pointer exceptions.
Okay, the above is the entire content of this article. I hope that the content of this article has certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.