When we talk about NPE, what are we talking about

NullPointerException (NPE) is not a complex problem in itself. The solution can be solved in one line of code, including NullPointerException of string type:

if (xxx == null) return null; If (stringutils.isempty (XXX)) return null;Copy the code

This line of code is also called null logic, and I’m sure all programmers are familiar with it. The problem, however, is that nPES are so frequent that we need to null them so often that the code is flooded with if-else logic, which can seriously affect the readability of the code, so it’s important to find other ways to handle NPES elegantly (rather than brutally null them)

How many null Pointers are there?

In fact, if you break it down, null Pointers can be categorized as follows, depending on the scenario that occurs

The service request result object is empty

This is the most common type, such as the scenario where a remote service is called without nullation, followed by a call to an object’s method

    String orderName = orderService.getOrderById(id).getName();
Copy the code

Many developers, in order to save trouble and not to write so much null-detection logic, directly chain calls lead to NPE, which is the most classic scenario

Wrap the method calls implied by the class

It may never be noticed if you don’t encounter it, but it will never happen again. This is caused by Java’s automatic unboxing of implicit type conversions, a classic example:

    // Boolean checkIfServiceOpen(long serviceId);
    if (checkIfServiceOpen(id)) {
        // ...
    } else {
        // ...
    }
Copy the code

If the service returns null, NPE will also be thrown, because there is an implicit Boolean logic that converts a Boolean class to Boolean, calling Boolean#booleanValue, and almost no one would notice if it hadn’t been poked

The Switch – a Case of NPE

If the switch argument is null, a null-pointer exception will be thrown

    String status = taskCommonService.queryStatusById(id);
    switch(status) {
        case "end":
            return true;
        case "processing":
            return false;
        default:
            return null;
    }
Copy the code

NPE for remote service

The more companies that code poorly, the more likely they are to encounter this situation, which has nothing to do with their own service, but is caused by passing null values into RPC interface parameters

    SearchModel searchModel = buildSearchModel(id);
    Result<Company> result = queryCompanyFromEngine(searchModel);
Copy the code

If the queryCompanyFromEngine method does not nulify the input parameter, an NPE will also result if the parameter is null

The remote service is not injected

This situation is not fully NPE, but it does cause null-pointer exceptions. Mainly due to automatic injection, if the service is not injected successfully, then the NPE is thrown when the method is called

The call parameter is empty

In this case, most people will notice that the operation is usually performed without nullating when receiving parameters, which will not be described again

How to gracefully handle null Pointers

The solution to null Pointers is simple, as all programmers know, and is a line of null-checking code. But putting judgment logic in front of nullpointer risk code can be very indirectness (unless your company is paying by lines of code), so here are a few ways I’ve used to deal with nullpointers in a concise way

Optional class

First of all, I don’t personally use the Optional class very often, nor do I personally advocate the Optional class as the ultimate solution to the NPE problem. If (XXX! = null) { return xxx; } else { return null; }, Optional’s encapsulation logic can actually reduce boilerplate code in some scenarios.

There are only two Optional core methods (or more specifically, one, only orElse methods), namely get and orElse methods. The get method is responsible for extracting the object from the encapsulated Optional class, and orElse is responsible for returning the default value if the object is empty. Take a simple example

public queryOffer(QueryParam param) { // ... String status = param.getStatus(); if (StringUtils.isEmpty(status)) { status = OfferStatus.NORMAL; } / /... }Copy the code

If we have a requirement for default values, and default values are not supported in the parameters (or cumbersome, such as classes in binary packages), then we need to nullize them in the code. If you have a large number of parameters that need to be set to default values, the code will be full of if-else logic, so you can use Optional to solve this problem

public queryOffer(QueryParam param) { // ... (other query parameters are omitted.) String status = option.ofNullable (param.getStatus()).orelse (offerStatus.normal); / /... }Copy the code

I personally recommend using the Optional type only for simple situations like this. If you treat Optional as an elegant solution to NPE in any case, you fall into the trap of over-encapsulation

Way to break up

This approach works well for most service layer code, separating the core logic into a single method to avoid too much null-checking and long methods (too many lines of single-function code). For example, the following logic (the log printing part has been omitted for code brevity) :

public boolean checkAndStartVipService(Long userId, ServiceType type) { if (userId == null || type == null) { return false; } List<Order> orders = orderService.listOrdersByUserIdAndType(userId, type); if (orders == null || orders.isEmpty()) { return false; } Order order = orders.get(0); if (order == null) { return false; } // omit dozens of lines of order validity check and start service logic code //... / /... / /... return true; }Copy the code

Itself code as the parameter calibration of the three convicted empty logic is acceptable, but there are too many lines of code in itself, is redundant, especially if the core code and contains a large number of empty logic, it can appear more bloated, so we can split the code for the front parameter calibration + core logic, this way of split is suitable for most applications, the following:

public boolean checkAndStartVipService(Long userId, ServiceType type) { if (userId == null || type == null) { return false; } List<Order> orders = orderService.listOrdersByUserIdAndType(userId, type); if (orders == null || orders.isEmpty()) { return false; } Order order = orders.get(0); if (order == null) { return false; } return startVipService(userId, type, order); } private Boolean startVipService(Long userId, SeviceType type, Order Order) {private Boolean startVipService(Long userId, SeviceType type, Order Order) { }Copy the code

This method split is more like a way to deal with long methods and code decoupling, but can also be seen as an elegant way to deal with NPE. Generally speaking, the pre-parameter verification place will contain a lot of null-judgment logic, and the logic is relatively simple, so the core logic is extracted separately and simple logic is stacked together, so that the code will not appear bloated.

The only sentenced to empty

Unique nulling, which refers to nulling logic for the same parameter, occurs only once in a link. Here’s an example to help you understand:

// OfferService.java
public OfferStatus queryOfferStatusById(Long id) {
    // ...
    String status = offer.getStatus();
    if (StringUtils.isEmpty()) {
        return null;
    }
    return OfferStatus.getByName(status);
}

// OfferStatus.java
public static OfferStatus getByName(String status) {
    if (StringUtils.isEmpty(status)) {
        return null;
    }
    // ...
}
Copy the code

We can see that the status after two actually found empty, because with the upstream data do not believe that any idea, many developers will take place at the interface first line parameter to null, this method is of course no problem, but if we can directly see the call method the method body, or under the two methods in a project, There is no need to do more than nulls. For example, the above code can be changed to:

// OfferService.java
public OfferStatus queryOfferStatusById(Long id) {
    // ...
    return OfferStatus.getByName(offer.getStatus());
}

// OfferStatus.java
public static OfferStatus getByName(String status) {
    if (StringUtils.isEmpty(status)) {
        return null;
    }
    // ...
}
Copy the code

Note that the nulling of the underlying methods is not removed, but the nulling of the upper methods is removed, because the underlying functional methods may be called by others and must be as robust as possible. However, if two methods have no upstream or downstream relationship, but are split for decoupling or avoiding long methods, or if the call is restricted to a method and will not be called outside, then it does not matter which method null-detection logic is removed.

Merge to empty

This method is mainly used for nulling strings as follows:

public boolean updateStatus(String id, String status, String operator) { if (StringUtils.existEmpty(id, status, operator)) { return false; } / /... } // stringutils. Java (own utility class, not apchae utility class) public static Boolean existEmpty(String... strings) { if (strings == null) { return true; } if (strings.length == 0) {// If there is no data, it is not null. } for (String string: strings) { if (isEmpty(string)) { return true; } } return false; }Copy the code

Similarly, nullating logic can be written in the model to expose it as a separate method (this is not recommended unless the model is very generic and nullating logic is uniform)

Exception handling

This approach is also often used if the RPC interface is called in a method and null values are returned as outliers (that is, null is not often returned), then additional nulls can be eliminated if no special handling is required

public String upload(String fileName, byte[] file) { // ... try { Result result = storageService.upload(fileName, file); return result.getData().getFilePath(); } catch(Exception e) { // ... return null; }}Copy the code

In addition to doing catch handling in your own methods, you can also pass exceptions to upper-level or top-level entry methods for uniform catch. If it is confirmed that methods on the link (not across applications) will only have a unique upstream and downstream, then some null-detection logic can be thrown upstream as an exception in the downstream method

Deal with NPE pitfalls

All possible NPES need to be shorted

Although most likely NPE are need to process, return to a specific value or to catch, but sometimes the NPE is also a kind of normal logical process, which I have said in the “capture” abnormal way to some NPE can treat as a branch of logic, as long as the upstream can handle this kind of situation, is not necessary to empty processing

Null-nulling logic requires encapsulation

Personally, I am not used to encapsulating some simple methods. In many cases, it is over-designed. There are other ways to solve the problem

In order to improve the execution speed of the method, the nullify check can be saved

This is a typical mistake. In fact, the complexity of native operation logic is insignificant compared to the time of HTTP/RPC calls, let alone the simple if-else logic. We should pay more attention to the code readability.

conclusion

So to summarize, all NPE problems, see if the nulled value is nulled somewhere else, if it’s not nulled see if we can catch it all together, if it’s not caught see if we can split the method to make the code look clean, if it’s not broken then encapsulate the logic, if it’s not wrapped then use if-else nulled.

This article was written on the spur of the moment before I was bored. There may be a lot of loose points. If there are any mistakes or disapprobation, please exchange and correct them.