46

Features to Avoid Null Reference Exceptions in Java and Swift

 5 years ago
source link: https://www.tuicool.com/articles/hit/j6n2Uf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Have you recently faced a NullPointerException   in your code? If not, then you must be a careful writer. One of the most common exception types in Java applications are   NullPointerExceptions . As long as the language allows us to assign null values to any object, it will always be easy to write a small piece of code, which, at some point, will cause a  NullPointerException   and crash the entire system. The  java.util.Optional<T>   class was introduced in Java 8 to alleviate this problem. Indeed, the Optional's API turns out to be quite powerful. There are plenty of cases where Optionals fit well. However, they are not designed to completely solve the problem with  NullPointerExceptions . Additionally, Optionals themselves are very easy to be misused. A good indicator for that is the number of articles that are often being published on how Optionals should or should not be used.

In contrast to Java, the type systems of other languages, like Kotlin, Swift, Groovy and more, are able to distinguish between variables that are allowed to point to null values and those that are not. In other words, they do not allow a null value to be assigned to a variable unless it is explicitly declared as nullable. In this article, we will give an overview of some features of different programming languages that reduce or avoid the necessity of working with null values.

Java Optionals

With java.util.Optional<T>   introduced in Java 1.8, the need for null references is significantly reduced. Nevertheless, some care is needed when creating an instance of an Optional or when using it. For instance, the  Optional.get   method will throw a  NoSuchElementException   if the value is not present, or the  Optional.of   method will throw a  NullPointerException   if the provided value is null. Therefore, both of these methods are as risky as directly de-referencing potential null values. One benefit we get from an Optional is that it provides a set of higher order functions, which can be chained without worrying whether the value is present or not.

Null Checks

Let’s consider a simple example with two classes' user and address where the required field in user is only username   and the required fields in address are  street   and  number . The task is to find the ZIP code of the user with the given id. If any of the non-required values are missing, then an empty string should be returned. Assume that a  UserRepository   is also provided. One way to implement this task is the following:

public String findZipCode(String userId) {
    User user = userRepository.findById(userId);
    if(user != null) {
        Address address = user.getAddress();
        if(address != null) {
            String zipCode = address.getZipCode();
            if(zipCode != null) {
                return zipCode;
            }
        }
    }
    return "";
}

Provided that the userRepository   is not  null   , this code will not throw a  NullPointerException . However, three if-statements are sitting in the code only for doing null-checks. The amount of the boilerplate code is comparable to the amount of code written to accomplish the task.

Optional Chaining

If Optionals were used as return types on the methods that do not guarantee to return a non-null value, the above implementation could also be written as:

public String findZipCode(String userId) {
    Optional<User> optUser = userRepository.findById(userId);
    if(optUser.isPresent()) {
        User user = optUser.get();
        Optional<Address> optAddress = user.getAddress();
        if(optAddress.isPresent()) {
            Address address = optAddress.get();
            return address.getZipCode().orElse("");
        }
    }
    return "";
}

If not worse, the second implementation is not any better than the first one. The null-checks are simply replaced with Optional.isPresent which, indeed, should be used before invoking   Optional.get . By the way, the  Optional.get   is a good candidate to get deprecated. Java 10 introduced a better alternative —  Optional.orElseThrow  —  whose behavior is the same, but the method name is screaming that an exception will be thrown if the value is not present.

The code above is only intending to show an ugly usage of Optionals. A more elegant approach would be to make a chain of higher-order functions provided by the Optional API:

public String findZipCode(String userId) {
    return userRepository.findById(userId)
        .flatMap(User::getAddress)
        .flatMap(Address::getZipCode)
        .orElse("");
}

If the Optional returned by the user repository is empty, the flatMap   will simply return an empty Optional. Otherwise, it will return an optional wrapping the user’s address. In this way, there is no need for any null-checking. The same holds for the second  flatMap   invocation. Thus, the optional is cascaded until the value we were looking for is reached.

Enhancements in Java 9

The Optional API is further enriched in Java 9 with three other methods: orstream   and  ifPresentOrElse :

Optional.or provides another possibility to chain Optionals. For instance, if we already have a collection of users in memory and we want to search through this collection before going into the repository, we could do the following:

public String findZipCode(String userId) {
    return findInMemoryUser(userId)
        .or(() -> userRepository.findById(userId))
        .flatMap(User::getAddress)
        .flatMap(Address::getZipCode)
        .orElse("");
}

Optional.stream   allows converting an Optional to a stream of at most one element. Let’s say we want to convert a list of userIds to a list of users. In Java 9, this could be done as:

public List<User> findAll(List<String> userIds) {
    return userIds.stream()
        .map(userRepository::findById)
        .flatMap(Optional::stream)
        .collect(Collectors.toList());
}

Optional.ifPresentOrElse   is similar to  Optional.ifPresent   from Java 1.8, but it performs a second action if the value is not present. For example, if the task was to print the ZIP code and it is provided or print a message otherwise, we could do the following:

public String printZipCode(String userId) {
    userRepository.findById(userId)  
        .flatMap(User::getAddress)
        .flatMap(Address::getZipCode)
        .ifPresentOrElse(
            System.out::println, 
            () -> System.out.println("The zip Code is not provided!")
        );
}

After all, one of the biggest pitfalls in Java is that it allows every non-primitive type to be assigned to null—  ven the Optional type itself. Nothing can stop us from assigning  null   instead of an empty Optional to an Optional type. If it happens that the  findById   method simply returns  null , then everything we described above becomes pointless.

Kotlin's Null Safety

Unlike Java, the Kotlin’s type system supports nullable types, which means types that except for the usual values of their data type may also represent the special value null . By default, all variables are non-nullable. To declare a nullable variable, the type on the declaration should be followed by a question mark. Furthermore, dereferencing nullable variables is allowed either through the null-safe call   ?. , the non-null assertion !!,   or the Elvis operator   ?: . The following examples show how to declare, assign, and reference nullable variables:

var user : User = null // does not compile, user is non-nullable
var nullableUser : User? = null // declares and assigns null to nullableUser
val name = nullableUser.name // does not compile. Either use  '?.' or '!!'
var lastName = nullableUser?.lastName // returns null since  nullableUser is null
val email = nullableUser!!email // compiles, but throws NullPointerException on runtime
lastName = nullableUser?.lastName ?: "" // returns an empty string.

Notice the difference between the null-safe call ?.   and the non-null assertion operator !! . Just like the name suggests, if the de-referenced variable is null, the former will immediately return null, whereas the latter will throw a  NullPointerException   instead. You don’t want to use  !!   unless you are a lover of the   NullPointerExceptions . The Elvis operator is similar to the   Optional.orElse . It returns the value of the expression on the left-hand side of  ?:   if it is not null. Otherwise, it evaluates the right-hand side expression and returns the result.

Nullable Chaining

Similar to Optionals in Java, nullable values in Kotlin can also be chained by using, for example, the null-safe call operator. An implementation of the findZipCode   method in Kotlin would be done in a single statement:

fun findZipCode(userId: String) = 
    userRepository.findById(userId)?.address?.zipCode ?: ""

Instead of Optional.flatMap   , we can use the null-safe call  ?.   and, instead of  Optional.orElse , we can use the Elvis operator   ?: . Additionally, we don’t have to be concerned whether the  userRepository   is  null   or not, because if it was, the compiler wouldn’t allow us to write  userRepository.findById(userId) , but we would rather be forced to use any of the operators mentioned above.

Swift

Swift behaves very similar to Kotlin. A type has to be marked explicitly to be able to store nil values. This can be done by adding the  ?   postfix operator to the type of a field or variable declaration. This is, however, just a short form of the type   Optional<Wrapped> , which is defined in the Swift standard library. Swift Optionals, unlike normal types, don’t have to be initialized directly or by a constructor. They are  nil   by default. A Swift Optional is actually an enumeration, which has two states:  none   and  some , where  none   represents  nil   and some represents an existing wrapped object.

var zipCode = nil // won’t compile, because zipCode is not optional
var zipCode : String =  nil // same here

var zipCode : String? = nil // compiles, zipCode contains "none"
var zipCode : Optional<String> = nil // same as above

var zipCode : String? = "1010" // zipCode contains "some" String

Implicitly Unwrapped Optionals

Optionals can also be declared as implicitly unwrapped Optional by using the !   postfix operator on the type of the variable declaration. The main difference is that these can be accessed directly without the  ?   or  !   operators. The usage of implicitly unwrapped Optionals is highly discouraged, except in very specific situations, where they are necessary and where you can be certain, that a value exists. There are very few cases in which this mechanism is really needed, one of which is the Interface Builder Outlets for iOS or macOS.

Here is an example of how it should NOT be done:

// zipCode will be nil by default and is implicitly unwrapped
var zipCode : String! 

/* 
 * if zipCode has a value, it will work fine but in this case 
 * it hasn’t and will therefore throw an error 
 */
zipCode.append("0")

The proper way of achieving the same result:

var zipCode : String?
zipCode?.append("0") // this line will return nil but no error is thrown

Optional Chaining

Optional chaining can be used to safely access fields and methods of the object contained in an Optional using the ?   postfix operator. Many calls to Optionals can be chained together, hence the name Optional chaining. Such an expression always returns an Optional, which will contain either the resulting object or  none if any Optional in the chain contains  none . Therefore, the result of the Optional chain has to be checked for  nil   again. This can be avoided by using either Optional binding, a nil-coalescing operator, or a guard-statement.

/* 
 * Optional chaining for querying the zip code, 
 * where findBy, address and zipCode are Optionals 
 * themselves.
 */
func findZipCodeFor(userId: String) -> String? {
    return userRepository.findBy(userId: userId)?.address?.zipCode
}

Optional Binding

The if let   statement provides a safe way to unwrap Optionals. If the given Optional contains  none , the if block will be skipped. Otherwise, a local constant, which is only valid within the if block, will be declared. This constant can have the same name as the Optional, which causes the actual Optional to be invisible within the block. In addition to multiple unwrapping statements, a boolean expression can also be added to the  if let   statement. These statements are separated by a comma (  , ), which behaves like the  &&   operator.

func printZipCodeFor(user: String) {
    let zipCode = userRepository.findBy(userId: user)?.address?.zipCode
    if let zipCode = zipCode {
        print(zipCode)
    }
}
func findZipCodeFor(userId: String, inCountry country: String) -> String? {
    if let address = userRepository.findBy(userId: userId)?.address,
        let zipCode = address.zipCode,
            address.country == inCountry {
            return zipCode
        }
    return nil
}

Nil-Coalescing Operator

The nil coalescing Operator is represented by ?? . Its purpose is to provide a default value if the Optional contains  none . It has a similar behavior to Kotlin’s Elvis operator (  ?: )

let userId = "1234"
print(findZipCodeFor(userId: userId) ?? "no zip code found for user \(userId)")

The operator also accepts another Optional as the default value. Therefore, multiple nil   coalescing operators can be chained together.

func findZipCodeOrCityFor(user: String) -> String {
    return findZipCodeFor(userId: user) 
          ?? findCityFor(userId: user) 
          ?? "neither zip code nor city found for user \(user)"
}

Guard

The guard statement, as the name suggests, guards code after it. In methods, it’s usually at the very beginning for checking the validity of the method parameters. But, it’s also able to unwrap Optionals (similar to Optional binding) and “guard” the code after it, if the Optional contains none . A guard statement only consists of a condition and/or an unwrapping statement and a compulsory  else   block. The compiler makes sure that this else block exits its enclosing scope by using control transfer statements (  returnthrowbreakcontinue ) or call methods whose return type is  Never . The unwrapped value of the Optional is visible in the enclosing scope of the guard statement, where it can be used like an ordinary constant. The guard statement makes the code more readable and prevents a lot of nested if statements.

func update(user: String, withZipCode zipCode: String) {
    guard let address = userRepository.findBy(userId: user)?.address else {
        print("no address found for \(user)")
        return
    }

    address.zipCode = zipCode
}

Conclusion

Java Optionals are recommended to be used as return types of the API whenever the requested value is not guaranteed. In this way, the client of the API will be encouraged to check for the presence of the returned value and also write cleaner code by utilizing the Optional’s API. However, one of the biggest pitfalls is that Java is incapable of enforcing programmers to not assign null values. Other modern languages, like Kotlin and Swift, are designed to be able to distinguish between types that are allowed to represent a null   value, and types that are not. Furthermore, they provide a rich set of features to cope with nullable variables and, thus, minimizing the risk of the null reference exceptions.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK