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.