はじめに
今回のテーマはnullです。
エンジニアなら誰でも共感できるnullの怖さ。私たちの一生の課題「null」について調べていきます。そして、そのnullによっておこるエラーとその対象法も一緒に記述します。
nullに関する記事は2つに分けて記述します。
目次
- エンジニア一生の課題「null」について
- optionalをoptionalらしく=> 作成中
抜け出せない「nullチェック」の沼
nullの歴史は?
まず、nullという概念はいつ、だれによって作られたのでしょうか?
Nullという概念は1965年、Tony Hoareというイギリスのコンピューターサイエンスによって導入されました。当時、彼は「存在しない値」を表現する最も便利に方法がnull参照だと思ったそうです。
その後様々な言語でNullが導入されていますが、2009年のカンファレンス(QCon)でTony Hoare自身が「Nullは10億ドルに相当する誤りだった」という旨の発言をしています。
NPE(NullPointException)
JAVAエンジニアなら、一度ぐらいはNullPointExceptionと遭遇した経験があるはずです。
JAVAでは参照先がない状態をnullといいますが、null状態(参照先がない)のオブジェクトを参照してメソッドを呼び出すなどの処理を行う際に、発生するエラーがNullPointExceptionです。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at com.company.OptionalTest.main(OptionalTest.java:8)
nullに脆弱性のあるコード
では、エンジニアたちはどんなミスを犯してNPEが発生するのか?コードを見ながら説明します。
下記のようなデータモデルがあるとしましょう。
/* 注文 */
public class Order {
private Long id;
private Date date;
private Member member;
// getters & setters
}
/* ユーザ */
public class Member {
private Long id;
private String name;
private Address address;
// getters & setters
}
/* 住所 */
public class Address {
private String street;
private String city;
private String zipcode;
// getters & setters
}
Order
クラスはMember
タイプのmember
フィールドを持ち、Memberクラス
はAddress
タイプのaddress
フィールドを持っています。
そして、「○○を注文したユーザの住所」を取得するために、次のようなメソッドがあるとしましょう。
/* ○○を注文したユーザの都市を返す */
public String getCityOfMemberFromOrder(Order order) {
return order.getMember().getAddress().getCity();
}
上記のコードがNPEに対してどれだけの脆弱性があるのかはエンジニアの皆さんなら簡単に見つけると思います。
NPE発生のシナリオ
上記のメソッドでは、具体的にどのような状況でNPEが発生するのでしょうか。
- orderパラメータがnullの場合
- order.getMember()がnullの場合
- order.getMember().getAddress()がnullの場合
- Addressインスタンスのcityがnullの場合
呼び出し元でそれぞれのケースにnullチェックをしないとNPEが発生するでしょう。
伝統的な(?) NPE回避方法
Java8以前は、次のようにNPEを回避してきました。
複数のIF文
public String getCityOfMemberFromOrder(Order order) {
if (order != null) {
Member member = order.getMember();
if (member != null) {
Address address = member.getAddress();
if (address != null) {
String city = address.getCity();
if (city != null) {
return city;
}
}
}
}
return "東京"; // default
}
業務で頻繁に見えるコードです。すべてのreturnに対してnullチェックを行っています。複数のif文を使っているため、可読性が低下し、コードだけではメソッドがどのような意図を持っているのか把握しづらいです。
returnを使う
public String getCityOfMemberFromOrder(Order order) {
if (order == null) {
return "東京";
}
Member member = order.getMember();
if (member == null) {
return "東京";
}
Address address = member.getAddress();
if (address == null) {
return "東京";
}
String city = address.getCity();
if (city == null) {
return "東京";
}
return city;
}
複数のIF文のコードを少しだけリファクタリングしてみました。
若干読みやすくなったとは言えるかもしれませんが、returnが4箇所もあるので、結果的にはメンテナンス性が低下します。
上記の2つの方法は基本的にオブジェクトのフィールドやメソッドを呼び出す直前にnullをチェックしてNPEを回避していますが、残念ながら、このせいで初期コードよりずいぶん汚いコードになってしまいました。
Null Object Pattern
このパターンは「null」を使わずに、それを代替するオブジェクトを定義することです。
下記の例を通じて説明します。
まず、インタフェースを定義し、それを継承する具象クラスクラスを作成します。
interface Messenger {
void send(String msg);
}
class Kakao implements Messenger {
@Override
public void send(String msg) {
// ... コード省略
}
}
class Line implements Messenger {
@Override
public void send(String msg) {
// ... コード省略
}
}
具象クラスはそれぞれMessenger
インタフェースを継承し、メッセージを送るメソッドをオーバライドしています。
Null Object Patternではnullというキーワードを使ってnullチェックを行っていないため、その代わりの「nullオブジェクト役割のクラス」を作成します。
- ほかの具象クラスとの違いはNPEを回避する役割のオブジェクトのため、sendメソッドをオーバライドするだけです。(どんな処理も行わない。)
class NullMessenger implements Messenger {
@Override
public void send(String msg) {
// どんな処理も行わない
}
}
上記のコードが実際にどのように動作するか見てみましょう。
条件に合う場合、それに該当するオブジェクトを返し、nullの場合はnullオブジェクトを返します。
class MessengerFactory {
public static Messenger getMessenger(String name) {
if ( ... ) {
// 条件にあうMessengerの具象クラスをreturnする
}
// 結果がない場合nullの代わりにnullオブジェクトをreturnする
return new NullMessenger();
}
}
Messenger messenger = MessengerFactory.getMessenger("KakaoTalk");
messenger.send("Hello");
このパターンで注目すべき点はnullを返さないというところです。
だから、私たちはメソッドからの戻り値がnullかどうかをチェックする必要がなくなりました。(IF文でnullチェックしないから、コードの可読性が高まるということです。)
しかし、このパターンでNPEから解消されるのか?残念ながら、それはできません。
もし、インタフェースに新しいメソッドが追加されたらどうでしょう。
具象クラスに加えてnullオブジェクトクラスにもそのメソッドを追加しなければなりません。これはメンテナンスに大きなリソースがかかるということを意味します。
nullの沼
上記のgetCityOfMemberFromOrder()
メソッドやsend()
メソッドの要求事項はかなり明確で簡単でした。
"○○を注文したユーザの住所が知りたい"
"メッセージを送る"
しかし、簡単な要求とは逆で、if文やreturnをたくさん使ったり、nullオブジェクトを使ったりしたせいで、メンテナンスに大きなリソースがかかるはずです。
私もそうですが、障害にならにためにコードの可読性とメンテナンス性を犠牲にするのが現実です。
javaは「存在しない値」を表現するため、nullを使うように作られました。
しかし、nullを設計した当時の意図に反してnullはjavaエンジニアにNPEという一生の課題を残すことになりました。
救世主の登場(feat.JAVA8)
java8の登場に従い、javaエンジニアたちがnullへの接し方に大きな変化が訪れました。
この部分については次の記事で記述します。
参考サイト
https://noiseless-blog.net/entry/2017/04/16/224146#Null%E3%81%AE%E7%99%BA%E7%A5%A5
https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%93%A4%EC%9D%84-%EA%B4%B4%EB%A1%AD%ED%9E%88%EB%8A%94-NULL-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0#npe_%EB%B0%9C%EC%83%9D_%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4
https://www.daleseo.com/java8-optional-before/
https://madplay.github.io/post/what-is-null-in-java