Java NullPointerExceptionの検出、修正、およびベストプラクティス

java.lang.NullPointerExceptionは、Javaプログラミングで最も一般的な例外の一つです。Javaで作業している人なら誰でも、JavaのスタンドアロンプログラムやJavaウェブアプリケーションで突如としてこれが現れるのを見たことがあるでしょう。

java.lang.NullPointerException

NullPointerExceptionはランタイム例外なので、プログラムでそれをキャッチする必要はありません。アプリケーションでNullPointerExceptionが発生するのは、オブジェクトが必要なところでnullに対して何らかの操作をしようとしているときです。JavaプログラムでNullPointerExceptionが発生する一般的な理由は次のとおりです:

  1. オブジェクトのインスタンスに対してメソッドを呼び出していますが、ランタイムではそのオブジェクトがnullです。
  2. ランタイムでnullのオブジェクトインスタンスの変数にアクセスしています。
  3. プログラムでnullをスローしています。
  4. nullの配列のインデックスにアクセスしたり、その値を変更したりしています。
  5. ランタイムでnullの配列の長さを確認しています。

Java NullPointerExceptionの例

1. JavaプログラムでのNullPointerExceptionの例を見てみましょう。

1. インスタンスメソッドを呼び出すときのNullPointerException

public class Temp {

	public static void main(String[] args) {

		Temp t = initT();
		
		t.foo("Hi");
		
	}

	private static Temp initT() {
		return null;
	}

	public void foo(String s) {
		System.out.println(s.toLowerCase());
	}
}

上記のプログラムを実行すると、次のNullPointerExceptionエラーメッセージがスローされます。

Exception in thread "main" java.lang.NullPointerException
	at Temp.main(Temp.java:7)

ここで、ステートメントt.foo("Hi");でNullPointerExceptionが発生しています。これは、ここで「t」がnullであるためです。

2. ヌルオブジェクトのフィールドにアクセス/修正する際のJava NullPointerException

public class Temp {

	public int x = 10;
	
	public static void main(String[] args) {

		Temp t = initT();
		
		int i = t.x;
		
	}

	private static Temp initT() {
		return null;
	}

}

上記のプログラムは、次のNullPointerExceptionスタックトレースをスローします。

Exception in thread "main" java.lang.NullPointerException
	at Temp.main(Temp.java:9)

ここで、ステートメントint i = t.x;でNullPointerExceptionがスローされます。これは、ここで「t」がnullであるためです。

3. メソッド引数にnullが渡される場合のJava NullPointerException

public class Temp {

	public static void main(String[] args) {

		foo(null);

	}

	public static void foo(String s) {
		System.out.println(s.toLowerCase());
	}
}

これはjava.lang.NullPointerExceptionの最も一般的な発生の1つです、なぜなら、null引数を渡しているのは呼び出し側だからです。

上記のプログラムがEclipse IDEで実行されたときに、下の画像はnullポインタ例外を示しています。

4. nullが投げられたときのjava.lang.NullPointerException

public class Temp {

	public static void main(String[] args) {

		throw null;
	}

}

上記プログラムの例外スタックトレースは、throw null;ステートメントのためにNullPointerExceptionを示しています。

Exception in thread "main" java.lang.NullPointerException
	at Temp.main(Temp.java:5)

5. null配列の長さを取得するときのNullPointerException

public class Temp {

	public static void main(String[] args) {

		int[] data = null;
		
		int len = data.length;
	}

}
Exception in thread "main" java.lang.NullPointerException
	at Temp.main(Temp.java:7)

6. null配列のインデックス値にアクセスするときのNullPointerException

public class Temp {

	public static void main(String[] args) {

		int[] data = null;
		
		int len = data[2];
	}

}
Exception in thread "main" java.lang.NullPointerException
	at Temp.main(Temp.java:7)

7. nullオブジェクトを同期化したときのNullPointerException

public class Temp {

	public static String mutex = null;
	
	public static void main(String[] args) {

		synchronized(mutex) {
			System.out.println("synchronized block");
		}		
	}
}

synchronized(mutex) は、”mutex”オブジェクトがnullのため、NullPointerExceptionをスローします。

8. HTTPステータス500 java.lang.NullPointerException

時々、Java Webアプリケーションからエラーページの応答を受け取り、エラーメッセージが「HTTPステータス500 – 内部サーバーエラー」でルート原因がjava.lang.NullPointerExceptionとなることがあります。

これに対して、Spring MVCの例プロジェクトを編集して、以下のようにHomeControllerメソッドを変更しました。

@RequestMapping(value = "/user", method = RequestMethod.POST)
	public String user(@Validated User user, Model model) {
		System.out.println("User Page Requested");
		System.out.println("User Name: "+user.getUserName().toLowerCase());
		System.out.println("User ID: "+user.getUserId().toLowerCase());
		model.addAttribute("userName", user.getUserName());
		return "user";
	}

以下の画像は、Webアプリケーションの応答によってスローされたエラーメッセージを示しています。

例外のスタックトレースは以下のとおりです:

HTTP Status 500Internal Server Error

Type Exception Report

Message Request processing failed; nested exception is java.lang.NullPointerException

Description The server encountered an unexpected condition that prevented it from fulfilling the request.

Exception

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException
	org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
	org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
	org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
Root Cause

java.lang.NullPointerException
	com.journaldev.spring.controller.HomeController.user(HomeController.java:38)
	sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	java.lang.reflect.Method.invoke(Method.java:498)

ルート原因は、user.getUserId()がnullを返すため、user.getUserId().toLowerCase()ステートメントでのNullPointerExceptionです。

java.lang.NullPointerExceptionの検出方法

NullPointerExceptionの検出は非常に簡単です。単に例外トレースを見れば、例外のクラス名と行番号が表示されます。次にコードを確認し、NullPointerExceptionの原因となる可能性があるnullを確認します。上記の例をすべて見れば、スタックトレースからnullポインタ例外の原因が非常に明確です。

NullPointerExceptionの修正方法

java.lang.NullPointerExceptionは未検査例外なので、キャッチする必要はありません。nullポインタ例外は、nullチェックと予防的なコーディングテクニックを使用して防ぐことができます。以下のコード例を見て、java.lang.NullPointerExceptionを回避する方法を示しています。

if(mutex ==null) mutex =""; //予防的なコーディング
		
synchronized(mutex) {
	System.out.println("synchronized block");
}
//nullチェックの使用
if(user!=null && user.getUserName() !=null) {
System.out.println("User Name: "+user.getUserName().toLowerCase());
}
if(user!=null && user.getUserName() !=null) {
	System.out.println("User ID: "+user.getUserId().toLowerCase());
}

NullPointerExceptionを避けるためのコーディングのベストプラクティス

1. 以下の関数を考え、nullポインタ例外を引き起こすシナリオに注意してください。

public void foo(String s) {
    if(s.equals("Test")) {
	System.out.println("test");
    }
}

引数がnullとして渡されると、NullPointerExceptionが発生する可能性があります。同じメソッドは、NullPointerExceptionを回避するために以下のように書くことができます。

public void foo(String s) {
	if ("Test".equals(s)) {
		System.out.println("test");
	}
}

2. 引数のためにnullチェックを追加し、必要に応じてIllegalArgumentExceptionをスローすることもできます。

public int getArrayLength(Object[] array) {
	
	if(array == null) throw new IllegalArgumentException("array is null");
	
	return array.length;
}

3. 以下のコードのように三項演算子を使用することもできます。

String msg = (str == null) ? "" : str.substring(0, str.length()-1);

4. toString()メソッドの代わりにString.valueOf()を使用してください。例えば、PrintStream println()メソッドのコードは以下のように定義されています。

public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }

以下のコードスニペットは、valueOf()メソッドがtoString()の代わりに使用されている例を示しています。

Object mutex = null;

// nullを表示
System.out.println(String.valueOf(mutex));

// java.lang.NullPointerExceptionをスローします
System.out.println(mutex.toString());

5. 可能な限りnullではなく、空のオブジェクトを返すメソッドを記述してください。例えば、空のリスト、空の文字列などです。

6. NullPointerExceptionを回避するために、コレクションクラスで定義されたいくつかのメソッドがあります。それらを使用するべきです。例えば、contains()、containsKey()、containsValue()などです。

参照: APIドキュメント

Source:
https://www.digitalocean.com/community/tutorials/java-lang-nullpointerexception