Major online incident! Null pointer problem caused by ternary expression

It’s really exciting. I encountered this kind of epic online bug not long after I joined the company. The homepage crashed directly and the old code exploded. No matter what the underlying reason is, I feel that the main reason is that the upstream and downstream links are too complicated. , management is relatively difficult, and a single move can affect the whole body.

Knowledge review

Everyone is familiar with the ternary operator:

<expression1> ? <expression2> : <expression3>

I’m used to calling this a ternary expression. It should be noted that a ternary expression never evaluates both and . Conditional operators are right associative, that is, they are evaluated in groups from right to left. For example, a ? b : c ? d : e will be executed as a ? b : (c ? d : e).

Let’s review the automatic unboxing and boxing mechanisms. Java uses this mechanism to make conversion between wrapper classes and basic data types more convenient:

  • Boxing: Convert basic data types into wrapper classes (the constructor of each wrapper class can receive variables of their respective data types)

  • Unboxing: Take out the packaged basic type data from the packaging class (use the xxxValue method of the packaging class)

Taking Integer as an example, let’s take a look at how Java’s built-in packaging class unboxes:

Integer obj = new Integer(10); // Boxing
int temp = obj.intValue(); // Unboxing

This form of code is before JDK 1.5. After JDK 1.5, Java designers provide automatic boxing and automatic unboxing mechanisms to facilitate development, and objects of package classes can be directly used to perform mathematical calculations.

Still taking Integer as an example, let’s take a look at the process of automatically unboxing:

Integer obj = 10; // Automatic boxing. Basic data type int -> Packaging class Integer
int temp = obj; // Automatic unboxing. Integer -> int
obj + + ; // Directly use objects of the wrapper class to perform mathematical calculations
System.out.println(temp * obj);

To convert a basic data type to a wrapper class, you don’t need to use a constructor as above, just = and that’s it; similarly, to convert a wrapper class to a basic data type, we don’t need to manually call the wrapper. The xxxValue method of the class is available, and unboxing can be completed directly by =. This is why they are called automatic.

c9f6a8b7e569db58bddc96b065db5009.png

Let’s take a look at the decompiled file of this code to see what the underlying principle is:

Integer obj = Integer.valueOf(10);
int temp = obj.intValue();

It can be seen that the underlying principle of automatic boxing is actually calling the valueOf method of the packaging class, and the underlying principle of automatic unboxing also calls the intValue() method of the packaging class. .

f86573b56ea722399537b9c2163f1f69.png

Problem recurrence

The actual code business logic is more complicated. Here we give a relatively simple example to reproduce this problem:

//Set to true to ensure that the second expression of the conditional expression can be executed.
boolean flag = true;
//Define a Boolean variable of the wrapper class object type, with a value of null
Boolean nullBoolean = null;
//Define a boolean variable of basic data type
boolean simpleBoolean = false;

//Use ternary operator and assign value to x variable
boolean x = flag ? nullBoolean : simpleBoolean;

The above code will throw NPE during operation:

Exception in thread "main" java.lang.NullPointerException

Moreover, this has nothing to do with the JDK version you are using. I have tested on JDK 6, JDK 8 and JDK 14, and NPE will be thrown.

Try to decompile the above code. After using the jad tool to decompile, you get the following code:

boolean flag = true;
boolean simpleBoolean = false;
Boolean nullBoolean = null;

boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;

As you can see, in the last line of the decompiled code, the compiler did an automatic unboxing for us (nullBoolean is a wrapper class, and x is a basic type), and nullBoolean is null, so null appears. booleanValue, thus throwing NPE.

So why does the compiler do automatic unboxing? Under what circumstances is automatic unboxing required?

Principle analysis

As for why the editor automatically unboxes the expressions in the ternary operator during the code compilation phase, in fact, “The Java Language Specification” (hereinafter referred to as JLS) is a Java language specification and is the basic reference document for all Java programming. ) has relevant introductions in Chapter 15.25. Let’s look directly at the description of this part in Java SE 1.7 JLS (because the expression of 1.7 is more concise), original address -> https://docs.oracle.com/javase/specs/jls/se7/html/ jls-15.html#jls-15.25:

c4514908d5575e4a63c8b8f418898d3a.png

Look at the two sentences I framed:

  1. If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression. The result of the formula is of the same type as the two operands

  2. If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T. When the third operand is a basic type and a packaging type corresponding to the basic type, the type of the result of the expression must be the basic type.

In order to meet the above regulations and prevent programmers from over-perceiving this rule, during the compilation process, if the compiler finds that the second and third operand types of the ternary operator are basic data types (such as boolean) and the When the basic type corresponds to a packaging type (such as Boolean), and the return expression needs to be a packaging type, then the packaging class needs to be automatically unboxed.

Understand this sentence, the JLS specification is that if the second and third operands are basic types and wrapper types respectively, then the return value is required to be the basic type. If the return value of the code you write is a package type, then the compiler will automatically do an unboxing in order to meet the JLS specification.

Simple summary: As long as one of the types of expression 1 and expression 2 is a basic type and the other is a packaging type, the unboxing operation that triggers type alignment will be performed.

Here are a few more examples to deepen our understanding:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;

When the second and third expressions are both wrapper classes, the return value of the expression is also a wrapper class, and the compiler does not need to perform unboxing operations.

Boolean x1 = flag ? objectBoolean : objectBoolean;

//Decompiled code (no need to do any special operations)
Boolean x1 = flag ? objectBoolean : objectBoolean;

When the second and third expressions are both of basic types, the return value of the expression is also of basic type, and the compiler does not need to perform unboxing operations.

boolean x2 = flag ? simpleBoolean : simpleBoolean;

//Decompiled code (no need to do any special operations)
boolean x2 = flag ? simpleBoolean : simpleBoolean;

When one of the second and third expressions is a basic type and the other is a wrapper type, the expression return value is the basic type, and the compiler needs to perform an unboxing operation:

boolean x3 = flag ? objectBoolean : simpleBoolean;

//Decompiled code (the packaging class needs to be unboxed)
boolean x3 = flag ? objectBoolean.booleanValue() : simpleBoolean;

If you know the rules of the ternary operator, then you will correctly define the types of x1, x2, and x3 in the above way.

However, not everyone is familiar with this rule, so in practical applications, the following definitions will appear:

boolean x4 = flag ? objectBoolean : objectBoolean;

// Decompiled code (the result of the ternary expression requires a packaging class, and x4 is a basic type, so the compiler needs to do unboxing)
boolean x4 = (flag ? objectBoolean : objectBoolean).booleanValue();
Boolean x5 = flag ? simpleBoolean : simpleBoolean;

// Decompiled code (the result of the ternary expression is required to be a basic type, and x5 is a packaging type, so the compiler needs to do boxing)
Boolean x5 = Boolean.valueOf(flag ? simpleBoolean : simpleBoolean);
Boolean x6 = flag ? objectBoolean : simpleBoolean;

// Decompiled code (the result of the ternary expression is required to be a basic type, and x5 is a packaging type, so the compiler needs to do boxing)
Boolean x6 = Boolean.valueOf(flag ? objectBoolean.booleanValue() : simpleBoolean);

Therefore, the above 6 situations may occur in daily development. In the above 6 situations, if automatic unboxing is involved, once the value of the packaging class is null, that is, null.booleanValue(), NPE will inevitably occur (boxing will not, because Boxing is Boolean.valueOf(null), which does not throw NPE).

Friends can set the above packaging classes in x3, x4 and x6 to null to see if NPE will be thrown:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;
// Set wrapper class to null
Boolean nullBoolean = null;

boolean x3 = flag ? nullBoolean : simpleBoolean;
boolean x4 = flag ? nullBoolean : objectBoolean;
Boolean x6 = flag ? nullBoolean : simpleBoolean;

In the above three situations, NPE will occur during execution:

  • Among them, x3 and x6 are NPE caused by automatic unboxing during the process of determining the type according to the rules of JLS during the operation of the ternary operator. Because the ternary operator is used, and the second and third operands are basic types and objects respectively. It is necessary to unbox the object. Since the object is null, NPE is reported when null.booleanValue() is called during the unboxing process.

  • x4 is an NPE caused by automatic unboxing during the assignment of variables.

Back-end exclusive technology group

To build a high-quality technical exchange community, HR personnel engaged in programming development and technical recruitment are welcome to join the group. Everyone is also welcome to share their own company’s internal information, help each other, and make progress together!

Speak in a civilized manner, focusing on communication technology, recommendation for positions, and industry discussion

Advertisers are not allowed to enter, and do not trust private messages to prevent being deceived.

ffac0c241d4b76b8deef6171219c693f.png

Add me as a friend and bring you into the group
The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Python entry skill treeHomepageOverview 383353 people are learning the system