Java : メソッドのパラメータ(引数)は使う側でチェックしよう
例えば、メソッドのパラメータにnullを許容したくない場合に…
- メソッドを使う側でチェックしてnullを渡さないようにする
- メソッド側でnullが渡されてもそれなりに動くようにする
どちらにするか、悩んだことはないでしょうか。
本記事では、使う側でチェックすることをおすすめしつつ、そのメリットなどをご紹介します。
概要
さっそくコード例を見ていきましょう。
public class Sample {
private String value = "";
public void setValue(String value) {
this.value = value;
}
public void print() {
final var s = value + " : " + value.length();
System.out.println(s);
}
}
Sampleクラスは、setValue で文字列を指定して、その文字列と長さを print メソッドで出力します。
final String value = "abc";
...
final var sample = new Sample();
sample.setValue(value);
sample.print(); // "abc : 3"
Sampleクラスを使う例です。
問題なく使えていますね。
final String value = null;
...
final var sample = new Sample();
sample.setValue(value);
// NullPointerException: Cannot invoke "String.length()" because "this.value" is null
sample.print();
次に問題が発生する例です。
setValue で null が指定されたので、print で NullPointerException が発生しました。
さて、ここで考えなければなりません。
print メソッドで NullPointerException を発生させないようにするには…
- Sampleクラスを使う側でチェックする
- Sampleクラス側で対処する
それぞれの例を見てみましょう。
使う側でチェック
public class Sample {
private String value = "";
public void setValue(String value) {
if (value == null) {
throw new IllegalArgumentException("nullは許容していません");
}
this.value = value;
}
public void print() {
final var s = value + " : " + value.length();
System.out.println(s);
}
}
使う側で…といいつつ、まずはSampleクラス側です。
使う側でチェックを強制させるために、setValueメソッドの先頭でパラメータを検証します。
許容していない値(今回の例ではnull)であれば、非チェック例外のIllegalArgumentExceptionを投げます。
つまり、許容していない値が指定される = プログラムの不具合、となります。
このような検証を、契約プログラミングの事前条件 (precondition) ともいいます。
メソッドを使う側は、nullを指定したら即座にIllegalArgumentExceptionが発生するため、メソッドの使い方が間違っているとすぐに気づけます。
使われる側は null が指定されないことが保証されるので、安心してパラメータにアクセスできます。
次に、使う側のコード例です。
final String value = null;
...
final var sample = new Sample();
if (value != null) {
sample.setValue(value);
sample.print();
}
setValue に null を指定しないようにチェックしています。
チェックしないと非チェック例外が発生するためですね。
使う側のすべての箇所でチェックが必要となり、それはデメリットなのでは…と感じるかもしれません。
しかし、実際は使う側のチェックは不要となることも多いです。
例えば、AクラスからさきほどのSampleクラスを使うことを考えてみます。
public class A {
private final Sample sample;
private final String valueA;
private final String valueB;
public A(Sample sample, String valueA, String valueB) {
if (sample == null || valueA == null || valueB == null) {
throw new IllegalArgumentException();
}
this.sample = sample;
this.valueA = valueA;
this.valueB = valueB;
}
public void funcA() {
sample.setValue(valueA);
sample.print();
}
public void funcB() {
sample.setValue(valueB);
sample.print();
}
}
final var a = new A(new Sample(), "ab", "xyz");
a.funcA(); // "ab : 2"
a.funcB(); // "xyz : 3"
クラスAのすべてのフィールドは、コンストラクタによってnullではないことが検証済みです。
finalキーワードも付けているので変更されることもありません。
よって、funcA と funcB メソッドによる sample, valueA, valueB の nullチェックは不要となります。
このように、Sampleを使う側でも事前にパラメータの検証をしておけばチェックが不要となります。
さらに、プログラム全体でこのような設計を心がけると、変数にnullが入ること自体が少なくなっていき、NullPointerExceptionが発生することも少なくなるでしょう。
メリット
問題が発生したときに原因の特定がしやすくなります。
契約プログラミングの事前条件(メソッドのパラメータを検証すること)では、契約に違反すると非チェック例外(もしくはassert)などで即座にプログラムを終了させます。
スタックトレースも契約に違反した箇所(setValueの呼び出し箇所)がそのまま出力されます。
そのため問題の解析もやりやすいでしょう。
final var sample = new Sample();
//Exception in thread "main" java.lang.IllegalArgumentException: nullは許容していません
// at Main$Sample.setValue(Main.java:28)
// at Main.main(Main.java:10)
sample.setValue(null);
しかし、パラメータを検証しないと、setValue に null を指定したときにはなにも起きません。
そして、しばらくして print が呼び出されたときに問題が発覚します。
final var sample = new Sample();
sample.setValue(null);
// ... なにかいろいろ処理 ...
//Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "this.value" is null
// at Main$Sample.print(Main.java:31)
// at Main.main(Main.java:12)
sample.print();
このように、スタックトレースにはprintメソッドを呼び出した箇所が出力されます。
しかし、本当に知りたいのは、 setValueでnullを指定したのはどこ なのか?です。
スタックトレースにsetValueは出現しません。
setValueとprintの呼び出し元は、まったく別のクラスになるかもしれません。
そんなこんなで問題の解析が難しくなるでしょう。
このようなデメリットが無くなることが、契約プログラミングのメリットでもあります。
デメリット
メソッドの使い方を少しでも間違うとプログラムが終了してしまいます。
プロトタイプとしてざっと実装してとりあえず動かしてみたい、ということがやりにくいこともあります。
メソッドに許容されていない値を指定しないように、使う側で毎回チェックが必要となります。
とはいえ、前述したようにそこまでチェックが必要となることはありません。
パラメータ検証のためのコードが、メソッドの先頭に必要となります。
これがけっこう多くなっていくので意外と面倒です。
補足 (Objects.requireNonNull)
nullを検証する場合は、IllegalArgumentExceptionの代わりにObjects.requireNonNullを使うこともおすすめです。
public class Sample {
private String value = "";
public void setValue(String value) {
this.value = Objects.requireNonNull(value, "nullは許容していません");
}
...
}
こちらのほうがシンプルに記述できますね。
補足 (assert)
事前条件の検証に、プログラム言語によってはassertを使うこともあります。
(assertは一般的にデバッグ実行時のみ有効になります)
Javaにもassertはありますが、あまり使われてはいないようです。
標準APIを見ても、非チェック例外を投げるのが一般的です。
Java 7 で追加された標準APIのFilesの例です。
public final class Files {
...
public static String readString(Path path, Charset cs) throws IOException {
Objects.requireNonNull(path);
Objects.requireNonNull(cs);
...
assertは使わずに、Objects.requireNonNullを使ってパラメータを検証していますね。
使われる側で対処
先ほどは使う側でnullを指定しないように気をつけました。
次は、使われる側でnullを対処する方法を見ていきましょう。
public class Sample {
private String value = "";
public void setValue(String value) {
this.value = value;
}
public void print() {
if (value == null) {
return;
}
final var s = value + " : " + value.length();
System.out.println(s);
}
}
setValue で null を指定することを許容します。
print では、value が null だった場合は処理をスキップします。
final String value = null;
...
final var sample = new Sample();
sample.setValue(value);
sample.print(); // <なにも表示されません>
使う側のコード例です。
setValue の nullチェックが不要となります。非チェック例外も発生しません。
このような手法を防御的プログラミングともいいます。
ある程度メソッドの呼び出しが雑になってしまっても、プログラムが終了することなく動き続けます。
メリット
多少問題はあってもとりあえず動かしてみたい、ということがやりやすいです。
使う側でパラメータのチェックが不要になります。
デメリット
問題が発生したときに原因の特定がしにくくなります。
使う側でチェックするメリットにも似た問題を記載したのでそちらもご参照ください。
setValue で null を指定して print でなにも表示されないのが、プログラムとして 正しいのか? 間違いなのか? があいまいになります。
もしそれが間違いだった場合に、setValue で null を指定している箇所を探していくことになるため、問題解析に時間がかかります。
また、間違いだった場合もプログラムは終了したりしないので、問題発覚が遅れることになります。
どちらがおすすめ?
プログラムの全体的な方針として、 使う側でチェック (契約プログラミング) することをおすすめします。
問題を早期に発見できるメリットは大きいです。
もちろん、ある一部のクラスは防御的プログラミングのほうが向いていることもあると思います。
そのときは一部混在することも全然OKです。
補足
言語によっても、契約プログラミング向き、防御的プログラミング向き、というのはあると思います。
Javaのような型がしっかりした(静的型付け)言語は契約プログラミングが向いていると思います。
JavaScriptやPHPといった型のゆるい(動的型付け)言語は防御的プログラミングが向いているかもしれません。
(向いているというよりかは、契約プログラミングの型の検証がしにくいイメージがあります)
コンストラクタのパラメータ検証
コンストラクタのパラメータを検証することも非常におすすめです。
特に、finalなフィールドには検証済みの値を初期値としましょう。
finalなフィールドに検証済みの値を入れることで、そのフィールドは以降チェックが不要となります。
public class A {
private final Sample sample;
private final String valueA;
private final String valueB;
public A(Sample sample, String valueA, String valueB) {
this.sample = Objects.requireNonNull(sample);
this.valueA = Objects.requireNonNull(valueA);
this.valueB = Objects.requireNonNull(valueB);
}
public void funcA() {
sample.setValue(valueA);
sample.print();
}
public void funcB() {
sample.setValue(valueB);
sample.print();
}
コンストラクタでnullではないことを検証して、それをfinalなフィールドに設定します。
これにより、このクラスのオブジェクトはすべてのフィールドがnullではないことが保証されます。
funcA, funcB では null を気にせずに済みます。(チェック不要)
もし、コンストラクタでパラメータの検証をしないと次のようになります。
public class A {
private final Sample sample;
private final String valueA;
private final String valueB;
public A(Sample sample, String valueA, String valueB) {
this.sample = sample;
this.valueA = valueA;
this.valueB = valueB;
}
public void funcA() {
if (sample == null || valueA == null) {
return;
}
sample.setValue(valueA);
sample.print();
}
public void funcB() {
if (sample == null || valueB == null) {
return;
}
sample.setValue(valueB);
sample.print();
}
}
funcA, funcB で null のチェックが必要となります。
毎回のnullチェックは面倒ですし、書き忘れも起きやすいので、おすすめしません。
null以外のパラメータ検証例
今まではnullをチェックする例を見てきました。
null以外の検証ももちろん有効です。
public class Sample {
private final int id;
public Sample(int id) {
if (id < 0) {
throw new IllegalArgumentException("idに負数は許容されません : " + id);
}
this.id = id;
}
}
// IllegalArgumentException: idに負数は許容されません : -1
final var sample = new Sample(-1);
契約プログラミング
public class Sample {
private String value = "";
public void setValue(String value) {
if (value == null) {
throw new IllegalArgumentException("nullは許容していません");
}
this.value = value;
}
public void print() {
final var s = value + " : " + value.length();
System.out.println(s);
}
}
メソッドの先頭でパラメータを検証することを、契約プログラミングの事前条件(precondition)ともいいます。
事前条件以外にも、戻り値の検証である事後条件 (postcondition)というものもあります。
興味のあるかたは、契約プログラミングで検索してみるのもおすすめです。
防御的プログラミング
public class Sample {
private String value = "";
public void setValue(String value) {
this.value = value;
}
public void print() {
if (value == null) {
return;
}
final var s = value + " : " + value.length();
System.out.println(s);
}
}
Wikipediaには防御的プログラミングのページはなさそうでした。(2021/7/4現在)
契約プログラミングよりかは知名度は低いのかも。
Webで検索するといろいろとヒットはするので、興味のあるかたは調べてみるのもおもしろいかもです。
まとめ
本記事では、メソッドのパラメータは使う側でチェックすることをおすすめしました。
そのためのパラメータ検証 (契約プログラミングの事前条件) が特に重要となります。
品質の高いプログラムを作れるように、ぜひ意識していきたいですね。
関連記事
- 例外 vs. 戻り値でエラーチェック
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン
- API 使用例