1. Gambaran keseluruhan
Secara amnya, pemboleh ubah nol , rujukan, dan koleksi sukar dikendalikan dalam kod Java. Mereka bukan sahaja sukar dikenali, tetapi juga kompleks untuk ditangani.
Sebenarnya, sebarang kekurangan dalam menangani null tidak dapat dikenal pasti pada waktu penyusunan dan menghasilkan NullPointerException pada waktu runtime.
Dalam tutorial ini, kita akan melihat keperluan untuk memeriksa null di Java dan pelbagai alternatif yang membantu kita untuk mengelakkan pemeriksaan null dalam kod kita.
2. Apa itu NullPointerException?
Menurut Javadoc for NullPointerException , ia dilemparkan apabila aplikasi cuba menggunakan null dalam kes di mana objek diperlukan, seperti:
- Memanggil kaedah contoh objek nol
- Mengakses atau mengubah medan objek kosong
- Mengambil null seakan-akan array
- Mengakses atau mengubah slot nol seolah-olah itu adalah array
- Membuang nol seolah-olah itu adalah nilai Lempar
Mari kita lihat dengan cepat beberapa contoh kod Java yang menyebabkan pengecualian ini:
public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success } } private String doSomethingElse() { return null; }
Di sini, kami cuba menggunakan kaedah panggilan untuk rujukan nol . Ini akan menghasilkan NullPointerException.
Contoh lain yang biasa adalah jika kita cuba mengakses susunan nol :
public static void main(String[] args) { findMax(null); } private static void findMax(int[] arr) { int max = arr[0]; //check other elements in loop }
Ini menyebabkan NullPointerException pada baris 6.
Oleh itu, mengakses medan, kaedah, atau indeks objek null apa pun menyebabkan NullPointerException , seperti yang dapat dilihat dari contoh di atas.
Kaedah biasa untuk mengelakkan NullPointerException adalah dengan memeriksa null :
public void doSomething() { String result = doSomethingElse(); if (result != null && result.equalsIgnoreCase("Success")) { // success } else // failure } private String doSomethingElse() { return null; }
Di dunia nyata, pengaturcara sukar mengenal pasti objek mana yang boleh menjadi kosong. Strategi yang selamat secara agresif adalah memeriksa nol untuk setiap objek. Walau bagaimanapun, ini menyebabkan banyak pemeriksaan sifar yang berlebihan dan membuat kod kami kurang dapat dibaca.
Dalam beberapa bahagian seterusnya, kita akan melalui beberapa alternatif di Java yang mengelakkan kelebihan tersebut.
3. Menangani null Melalui Kontrak API
Seperti yang dibincangkan di bahagian terakhir, mengakses kaedah atau pemboleh ubah objek null menyebabkan NullPointerException. Kami juga membincangkan bahawa meletakkan pemeriksaan nol pada objek sebelum mengaksesnya menghilangkan kemungkinan NullPointerException.
Namun, selalunya ada API yang dapat menangani nilai null . Sebagai contoh:
public void print(Object param) { System.out.println("Printing " + param); } public Object process() throws Exception { Object result = doSomething(); if (result == null) { throw new Exception("Processing fail. Got a null response"); } else { return result; } }
The cetak () kaedah panggilan hanya akan mencetak "null" tetapi tidak akan membuang pengecualian. Begitu juga, proses () tidak akan membatalkan tindak balasnya. Ia lebih melontarkan Pengecualian.
Oleh itu, untuk kod pelanggan yang mengakses API di atas, tidak perlu pemeriksaan kosong .
Walau bagaimanapun, API seperti itu mesti menjadikannya jelas dalam kontraknya. Tempat yang biasa bagi API untuk menerbitkan kontrak tersebut adalah JavaDoc .
Namun, ini tidak memberikan petunjuk yang jelas mengenai kontrak API dan dengan demikian bergantung pada pembangun kod pelanggan untuk memastikan pematuhannya.
Di bahagian seterusnya, kita akan melihat bagaimana beberapa IDE dan alat pembangunan lain membantu pembangun dengan ini.
4. Mengautomasikan Kontrak API
4.1. Menggunakan Analisis Kod Statik
Alat analisis kod statik banyak membantu meningkatkan kualiti kod. Dan sebilangan alat seperti itu juga membolehkan pemaju mengekalkan kontrak batal . Salah satu contohnya ialah FindBugs.
FindBugs membantu menguruskan null kontrak melalui @Nullable dan @NonNull penjelasan. Kami dapat menggunakan anotasi ini menggunakan kaedah, medan, pemboleh ubah tempatan, atau parameter apa pun. Ini menjadikannya jelas kepada kod pelanggan sama ada jenis anotasi boleh menjadi batal atau tidak. Mari lihat contoh:
public void accept(@Nonnull Object param) { System.out.println(param.toString()); }
Di sini, @NonNull menjelaskan bahawa hujah itu tidak boleh dibatalkan. Sekiranya kod pelanggan memanggil kaedah ini tanpa memeriksa argumen null, FindBugs akan menghasilkan amaran pada waktu kompilasi.
4.2. Menggunakan Sokongan IDE
Pembangun umumnya bergantung pada IDE untuk menulis kod Java. Dan ciri-ciri seperti penyelesaian kod pintar dan peringatan berguna, seperti ketika pemboleh ubah mungkin belum ditetapkan, pasti banyak membantu.
Beberapa IDE juga membolehkan pemaju menguruskan kontrak API dan dengan itu menghilangkan keperluan untuk alat analisis kod statik. IntelliJ IDEA menawarkan @NonNull dan @Nullable penjelasan. Untuk menambahkan sokongan untuk anotasi ini di IntelliJ, kita mesti menambahkan ketergantungan Maven berikut:
org.jetbrains annotations 16.0.2
Sekarang, IntelliJ akan menghasilkan amaran sekiranya pemeriksaan nol tidak ada, seperti dalam contoh terakhir kami.
IntelliJ juga menyediakan anotasi Kontrak untuk mengendalikan kontrak API yang kompleks.
5. Ketegasan
Sehingga kini, kami hanya bercakap mengenai penghapusan keperluan pemeriksaan nol dari kod pelanggan. Tetapi, itu jarang berlaku dalam aplikasi dunia nyata.
Sekarang, mari kita anggap bahawa kita bekerja dengan API yang tidak dapat menerima parameter null atau dapat mengembalikan respons nol yang mesti ditangani oleh klien . Ini menunjukkan perlunya kita memeriksa parameter atau tindak balas untuk nilai nol .
Here, we can use Java Assertions instead of the traditional null check conditional statement:
public void accept(Object param){ assert param != null; doSomething(param); }
In line 2, we check for a null parameter. If the assertions are enabled, this would result in an AssertionError.
Although it is a good way of asserting pre-conditions like non-null parameters, this approach has two major problems:
- Assertions are usually disabled in a JVM
- A false assertion results in an unchecked error that is irrecoverable
Hence, it is not recommended for programmers to use Assertions for checking conditions. In the following sections, we'll discuss other ways of handling null validations.
6. Avoiding Null Checks Through Coding Practices
6.1. Preconditions
It's usually a good practice to write code that fails early. Therefore, if an API accepts multiple parameters that aren't allowed to be null, it's better to check for every non-null parameter as a precondition of the API.
For example, let's look at two methods – one that fails early, and one that doesn't:
public void goodAccept(String one, String two, String three) { if (one == null || two == null || three == null) { throw new IllegalArgumentException(); } process(one); process(two); process(three); } public void badAccept(String one, String two, String three) { if (one == null) { throw new IllegalArgumentException(); } else { process(one); } if (two == null) { throw new IllegalArgumentException(); } else { process(two); } if (three == null) { throw new IllegalArgumentException(); } else { process(three); } }
Clearly, we should prefer goodAccept() over badAccept().
As an alternative, we can also use Guava's Preconditions for validating API parameters.
6.2. Using Primitives Instead of Wrapper Classes
Since null is not an acceptable value for primitives like int, we should prefer them over their wrapper counterparts like Integer wherever possible.
Consider two implementations of a method that sums two integers:
public static int primitiveSum(int a, int b) { return a + b; } public static Integer wrapperSum(Integer a, Integer b) { return a + b; }
Now, let's call these APIs in our client code:
int sum = primitiveSum(null, 2);
This would result in a compile-time error since null is not a valid value for an int.
And when using the API with wrapper classes, we get a NullPointerException:
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));
There are also other factors for using primitives over wrappers, as we covered in another tutorial, Java Primitives versus Objects.
6.3. Empty Collections
Occasionally, we need to return a collection as a response from a method. For such methods, we should always try to return an empty collection instead of null:
public List names() { if (userExists()) { return Stream.of(readName()).collect(Collectors.toList()); } else { return Collections.emptyList(); } }
Hence, we've avoided the need for our client to perform a null check when calling this method.
7. Using Objects
Java 7 introduced the new Objects API. This API has several static utility methods that take away a lot of redundant code. Let's look at one such method, requireNonNull():
public void accept(Object param) { Objects.requireNonNull(param); // doSomething() }
Now, let's test the accept() method:
assertThrows(NullPointerException.class, () -> accept(null));
So, if null is passed as an argument, accept() throws a NullPointerException.
This class also has isNull() and nonNull() methods that can be used as predicates to check an object for null.
8. Using Optional
8.1. Using orElseThrow
Java 8 introduced a new Optional API in the language. This offers a better contract for handling optional values compared to null. Let's see how Optional takes away the need for null checks:
public Optional process(boolean processed) { String response = doSomething(processed); if (response == null) { return Optional.empty(); } return Optional.of(response); } private String doSomething(boolean processed) { if (processed) { return "passed"; } else { return null; } }
By returning an Optional, as shown above, the process method makes it clear to the caller that the response can be empty and must be handled at compile time.
This notably takes away the need for any null checks in the client code. An empty response can be handled differently using the declarative style of the Optional API:
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
Furthermore, it also provides a better contract to API developers to signify to the clients that an API can return an empty response.
Although we eliminated the need for a null check on the caller of this API, we used it to return an empty response. To avoid this, Optional provides an ofNullable method that returns an Optional with the specified value, or empty, if the value is null:
public Optional process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response); }
8.2. Using Optional with Collections
While dealing with empty collections, Optional comes in handy:
public String findFirst() { return getList().stream() .findFirst() .orElse(DEFAULT_VALUE); }
This function is supposed to return the first item of a list. The Stream API's findFirst function will return an empty Optional when there is no data. Here, we have used orElse to provide a default value instead.
This allows us to handle either empty lists, or lists, which after we have used the Stream library's filter method, have no items to supply.
Alternatively, we can also allow the client to decide how to handle empty by returning Optional from this method:
public Optional findOptionalFirst() { return getList().stream() .findFirst(); }
Therefore, if the result of getList is empty, this method will return an empty Optional to the client.
Using Optional with collections allows us to design APIs that are sure to return non-null values, thus avoiding explicit null checks on the client.
It's important to note here that this implementation relies on getList not returning null. However, as we discussed in the last section, it's often better to return an empty list rather than a null.
8.3. Combining Optionals
When we start making our functions return Optional we need a way to combine their results into a single value. Let's take our getList example from earlier. What if it were to return an Optional list, or were to be wrapped with a method that wrapped a null with Optional using ofNullable?
Our findFirst method wants to return an Optional first element of an Optional list:
public Optional optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst()); }
By using the flatMap function on the Optional returned from getOptional we can unpack the result of an inner expression that returns Optional. Without flatMap, the result would be Optional
9. Libraries
9.1. Using Lombok
Lombok is a great library that reduces the amount of boilerplate code in our projects. It comes with a set of annotations that take the place of common parts of code we often write ourselves in Java applications, such as getters, setters, and toString(), to name a few.
Another of its annotations is @NonNull. So, if a project already uses Lombok to eliminate boilerplate code, @NonNull can replace the need for null checks.
Before we move on to see some examples, let's add a Maven dependency for Lombok:
org.projectlombok lombok 1.18.6
Now, we can use @NonNull wherever a null check is needed:
public void accept(@NonNull Object param){ System.out.println(param); }
So, we simply annotated the object for which the null check would've been required, and Lombok generates the compiled class:
public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); } }
If param is null, this method throws a NullPointerException. The method must make this explicit in its contract, and the client code must handle the exception.
9.2. Using StringUtils
Generally, String validation includes a check for an empty value in addition to null value. Therefore, a common validation statement would be:
public void accept(String param){ if (null != param && !param.isEmpty()) System.out.println(param); }
This quickly becomes redundant if we have to deal with a lot of String types. This is where StringUtils comes handy. Before we see this in action, let's add a Maven dependency for commons-lang3:
org.apache.commons commons-lang3 3.8.1
Let's now refactor the above code with StringUtils:
public void accept(String param) { if (StringUtils.isNotEmpty(param)) System.out.println(param); }
So, we replaced our null or empty check with a static utility method isNotEmpty(). This API offers other powerful utility methods for handling common String functions.
10. Kesimpulannya
Dalam artikel ini, kami melihat pelbagai sebab untuk NullPointerException dan mengapa sukar untuk dikenalpasti. Kemudian, kami melihat pelbagai cara untuk mengelakkan kelebihan dalam kod di sekitar memeriksa nol dengan parameter, jenis pengembalian, dan pemboleh ubah lain.
Semua contoh boleh didapati di GitHub.