こんにちは、馬場です。
2ヶ月ほど前に「比較:並行処理 - Java とScala とGo -」という、プログラムを比較する記事を書きました。この記事中のプログラム、ScalaではOptionを使っていたのですが、Java 8 のプログラムの方では「null」の処理を書いていたところ、レビューアに指摘を受けました。
「Java 8 の方のプログラムでOptional を使っていないのは、わざとですか?」
... すみません。そうでした。Javaは8からOptionalが使えるようになったんですよね。そこで、nullを多用しているプログラムをOptionalを活用するように書き換えてみました。
変更前 - null 判定をしているプログラム
以前の記事で紹介した二分探索木を表すTree クラスは以下のようになっていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
package tree; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * 整数の値をもつ二分探索木。 * left Treeに含まれる値はすべてvalue より小さく right Treeに含まれる値はすべてvalue以上。 * */ public class Tree { private Tree left; private Integer value; private Tree right; /** * 値がvalue 、子どもを持たないTreeを作成する * * @param value ノードの値 */ public Tree(int value) { this.value = value; } public Tree getLeft() { return left; } public Tree getRight() { return right; } public Integer getValue() { return value; } /** * ランダムにTreeを生成する。 生成されるTreeは coef,2coef,3coef,... ,10coefの10の値を保持する。 * * @param coef Treeに含まれる数を生成するための係数 * @return coef, 2coef, ... 10coef の10個の数字を保持するTree。形はバラバラ */ public static Tree createTree(int coef) { //ランダムにTree を生成できるよう、seeds の順番をバラバラにする Integer[] seeds = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; List<Integer> seedList = Arrays.asList(seeds); Collections.shuffle(seedList); Tree tree = null; for (int seed : seedList) { tree = insert(tree, (1 + seed) * coef); } return tree; } /** * 二分探索木の構造を保つように、Tree tree に値value を追加する。 * * @param tree Tree * @param value 追加する値 * @return valueを追加した新しいTree */ private static Tree insert(Tree tree, int value) { if (tree == null) { return new Tree(value); } if (value < tree.value) { tree.left = insert(tree.left, value); } else { tree.right = insert(tree.right, value); } return tree; } public String toString() { String string = ""; if (left != null) { string += left.toString() + " "; } string += value; if (right != null) { string += " " + right.toString(); } return "(" + string + ")"; } public static void main(String[] args) { System.out.println(Tree.createTree(1)); System.out.println(Tree.createTree(1)); System.out.println(Tree.createTree(1)); } } |
ポイントは、leftもrightもnullかもしれないという点です。ですので、leftやrightを取り扱うプログラムでは気をつけて忘れずにnull の場合の処理を書かなくてはいけません。
変更後 - Optional を利用する
ここで、このnullかもしれないleftやrightをOptionalに変更します。これによりleftやrightがない場合があるということを明示的に示すことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
package tree.with.optional; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; /** * 整数の値をもつ二分探索木。 * left Treeに含まれる値はすべてvalue より小さく right Treeに含まれる値はすべてvalue以上。 */ public class Tree { // left を持たないTree もあるのでOptional private Optional<Tree> left = Optional.empty(); private int value; // rightを持たないTreeもあるのでOptional private Optional<Tree> right = Optional.empty(); /** * 値がvalue 、子どもを持たないTreeを作成する * * @param value ノードの値 */ public Tree(int value) { this.value = value; } public Optional<Tree> getLeft() { return left; } public Optional<Tree> getRight() { return right; } public int getValue() { return value; } /** * ランダムにTreeを生成する。 生成されるTreeは coef,2coef,3coef,... ,10coefの10の値を保持する。 * * @param coef Treeに含まれる数を生成するための係数 * @return coef, 2coef, ... 10coef の10個の数字を保持するTree。形はバラバラ */ public static Tree createTree(int coef) { // ランダムにTree を生成できるよう、seeds の順番をバラバラにする Integer[] seeds = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; List<Integer> seedList = Arrays.asList(seeds); Collections.shuffle(seedList); //最初のTree は値を持たないので、empty Optional<Tree> tree = Optional.empty(); //seed の値を一つずつtreeに追加していく for (int seed : seedList) { tree = Optional.of(insert(tree, (1 + seed) * coef)); } return tree.get(); } /** * 二分探索木の構造を保つように、Optional<Tree> treeOption に値value を追加する。 * * @param treeOption TreeのOptional * @param value 追加する値 * @return valueを追加した新しいTree。メソッドの戻り値はemptyにはならないので、 * Tree(Optional ではない) */ private static Tree insert(Optional<Tree> treeOption, int value) { //treeOptionがemptyでなければ、値を追加するメソッドを呼び出し、自身を返す //emptyならvalueを一つだけもつtreeを返す return treeOption.map(t -> t.insert(value)).orElse(new Tree(value)); } /** * 二分探索木の構造を保つようにvalue を追加する * @param value * @return 自分 */ private Tree insert(int value) { if (value < this.value) { this.left = Optional.of(insert(this.left, value)); } else { this.right = Optional.of(insert(this.right, value)); } return this; } public String toString() { String string = left.map(Tree::toString).orElse(""); string += value; string += right.map(Tree::toString).orElse(""); return "(" + string + ")"; } public static void main(String[] args) { System.out.println(Tree.createTree(1)); System.out.println(Tree.createTree(1)); System.out.println(Tree.createTree(1)); } } |
Optionalのメリット・デメリット
Optional で書き換えたプログラムをみて、みなさんどう感じたでしょうか?正直はじめてみる場合「わかりにくい」「めんどくさい」と感じるのではないでしょうか。実際、Optionalは「これを使うとコードがすっきりする」というものではありません。
Optionalの利用のメリットはその値はないかもしれないということをコードに明示できることです。上の例では、取得したleftやrightはOptionalなので、気をつけずともemptyの場合の処理の記述を強制させられてしまうのです。プログラムの背景や意思を正確に後続の開発者に伝えるという意味でかなり強力です。コメントに「nullかもしれないから気をつけて」と書くよりずっと伝わります。
Scalaをがりがり書いていたときも、最初はOption(=ScalaのOptional)めんどくさーいと思いました。しかし実装を進めていくと「これは間違いなくnullではないんだ」という安心感に変わってきました。Java 8を利用している人はstreamだけでなく、Optional もぜひお試しください。
補足
JavaのnullかもしれないオブジェクトをOptionalに変換するときはofNullableメソッドを使いましょう。
1 2 |
Object somethingThatCanBeNull = someMethod(); Optional<Object> optional = Optional.ofNullable(somethingThatCanBeNull); |
紹介したプログラムでは of メソッドを使っていますがこれはnullではないTree が返されることがわかっているからです。ofにnullを渡すとNullPointerExceptionが発生してとても釈然としない結果になります。