Mind the context of the null pointers
A lot of Java developers seem to have an almost superstitious attitude towards the NullPointerException
. Some of them seem to wish that the Java programming language forbade null pointers altogether.
Eliminating or at least reducing the incidence of null pointers was a major concern in the design of both Scala and Kotlin (two programming languages for the Java Virtual Machine).
More important than the occurrence of a NullPointerException
is when and where it occurs. In development, it can point up a silly and easily fixed mistake. In production, it might be cause to fire someone.
Of course it’s scary to think that a NullPointerException
could get you fired. But with test-driven development and robust quality assurance, you can be confident that the dreaded NullPointerException
will only occur in development, not production.
Here’s a toy example to illustrate: bank accounts. Consider the following abstract class:
public abstract class BankAccount { public BankAccount(Entity primary, Entity secondary,
String label, Deposit initialDeposit) {
this.accountBalance = INITIALIZATION_ACCOUNT_BALANCE;
this.processDeposit(initialDeposit);
this.accountNumber = getNewAccountNumber();
this.primaryAccountHolder = primary;
this.noSecondaryAccountHolderFlag = (secondary == null);
this.secondaryAccountHolder = secondary;
this.accountLabel = label;
this.accountHistory = new ArrayList<>();
this.accountBeneficiary = null;
}}
I was going to use this to illustrate how to use abstract classes to avoid unnecessary duplication of common functionality among similar classes. But since that’s not relevant for this example, I’ve left out the duplication.
Here’s the concrete class CheckingAccount
:
public class CheckingAccount extends BankAccount { private ArrayList<Check> checksList; private SavingsAccount assocSav; public CheckingAccount(Entity primary, Deposit initialDeposit) {
this(primary, null, "Primary Checking", initialDeposit);
} public CheckingAccount(Entity primary, Entity secondary,
String label, Deposit initialDeposit) {
super(primary, secondary, label, initialDeposit);
this.checksList = new ArrayList<>();
this.assocSav = null;
}}
As you can already see, there are three fields that can be null for a checking account: the field for the secondary account holder, the field for the beneficiary, and the field for the associated savings account.
But that’s actually not what’s going to get us into trouble when we run the tests for CheckingAccount
.
Testsuite: bankaccounts.CheckingAccountTest
Tests run: 0, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.775 secTestcase: bankaccounts.CheckingAccountTest: Caused an ERROR
null
java.lang.NullPointerException
at bankaccounts.BankAccount.processDeposit(BankAccount.java:53)
at bankaccounts.BankAccount.<init>(BankAccount.java:84)
at bankaccounts.SavingsAccount.<init>(SavingsAccount.java:22)
at bankaccounts.SavingsAccount.<init>(SavingsAccount.java:18)
at bankaccounts.CheckingAccountTest.setUpClass(CheckingAccountTest.java:41)Test bankaccounts.CheckingAccountTest FAILED
test:
Deleting: C:\Users\AL\AppData\Local\Temp\TEST-bankaccounts.CheckingAccountTest.xml
BUILD SUCCESSFUL (total time: 3 seconds)
The exception message “null” is useless. The stack trace, on the other hand, very helpfully points us to the problem in BankAccount.processDeposit()
:
public final void processDeposit(Deposit deposit) {
this.accountBalance =
this.accountBalance.plus(deposit.getTransactionAmount());
this.accountHistory.add(deposit);
}
What happened here was that this procedure attempted to add a Deposit
object to accountHistory
before accountHistory
was even initialized. So the actual problem is in the constructor. The fix is easy:
public BankAccount(Entity primary, Entity secondary,
String label, Deposit initialDeposit) {
this.accountBalance = INITIALIZATION_ACCOUNT_BALANCE;
this.accountNumber = getNewAccountNumber();
this.primaryAccountHolder = primary;
this.noSecondaryAccountHolderFlag = (secondary == null);
this.secondaryAccountHolder = secondary;
this.accountLabel = label;
this.accountHistory = new ArrayList<>();
this.processDeposit(initialDeposit);
this.accountBeneficiary = null;
}
We just have the constructor initialize accountHistory
and then process the initial deposit. Now we can run the tests and get back to work on the actual logic of the program.
Testsuite: bankaccounts.CheckingAccountTest
Savings account balance: $10000.00 prior to test
Checking account balance: $1500.00 prior to test
Verifying the same deposit can't be made twice...
Savings account balance: $10000.00 after test
Checking account balance: $2810.72 after testTestcase: testDoubleDeposit(bankaccounts.CheckingAccountTest): FAILED
expected:<$2155.36> but was:<$2810.72>
junit.framework.AssertionFailedError: expected:<$2155.36> but was:<$2810.72>
at bankaccounts.CheckingAccountTest.testDoubleDeposit(CheckingAccountTest.java:79)
And so on and so forth. I have a GitHub repository for the source and tests of this toy example.
I emphasize that this is a toy example. You could make this sort of mistake, but if it happens this early in the process, the problem would surely be detected long before going to production, even without automated testing.
It’s more worrisome to make this sort of silly mistake in a more evolved project. What if the problem lurks in an obscure corner of the system?
This is part of the reason why good test coverage is important. If every line in which a NullPointerException
could theoretically arise is covered by a test, and all the tests are running and passing, then you don’t have to worry that an unexpected NullPointerException
could occur in production.
In general this applies to all exceptions. It’s just that NullPointerException
is the exception that seems to occur the most often, and for the widest variety of error patterns, according to our own Andras Horvath.
I hope this helps you get a perspective on the NullPointerException
as a useful debugging tool rather than something to dread.