JavaでOptional
Java8 "Optional" ~ これからのnullとの付き合い方 ~ - Qiita
を見て、残念ながら我々はJava6を強いられている環境なのでそれっぽいものを作る。
何番煎じかは知らないし元々の実装を見れば早いけど想像力を鍛えるため何も見ない。
Optional<String> hogeOpt = Optional.ofNullable(getHoge()); // 値をラップする hogeOpt.ifPresent(hoge -> System.out.println(hoge.length())); // 値が存在する場合のみ実行
最初はこれですね。
同じような雰囲気でOptional型にラップできるようにします。
また、個人的な好みからクラス名はOptionにします。
いきなり書いてもいいですが少し真面目に型から考えます。
1番目はT型の引数を受け取ってOption
また、後者のものは呼び出している側で動作を決めています。
そんなわけでよく考えずに抽象クラスとしてOptionを定義してみます。
- Option.java
package option; public abstract class Option<T> { public static <T> Option<T> ofNullable(final T value) { return null; } public abstract void ifPresent(); }
ofNullableでnullを返してしまうとnullを隠そうとしているのに意味が無い気がするので、「値が存在する」と「値が存在していない」事を表すクラスを作って場合分けしてそれを返すようにしましょう。
- Option.java
public static <T> Option<T> ofNullable(final T value) { if(value == null) return new None<T>(); return new Some<T>(value); }
- Some.java
package option; final class Some<T> extends Option<T> { final T _value; Some(final T value){ _value = value; } @Override public void ifPresent() { } }
- None.java
package option; final class None<T> extends Option<T> { @Override public void ifPresent() { } }
次にifPresentですが、大人しくインターフェースを渡してそれで処理すれば良さそうです。
最近めっきり仕事で使う機会の無いC#を忘れないためにもActionと名前を付けておきましょう。
- Action.java
package functional; public interface Action<T> { public void call(final T value); }
- Option.java
public abstract void ifPresent(final Action<T> action);
- Some.java
@Override public void ifPresent(final Action<T> action) { action.call(_value); }
Noneは何もしないのでメソッドの引数だけ書き換えておきます。
Java8にした時にConsumerに置き換えるなりそのままラムダ式に置き換えるなりすればスッキリするでしょう。
ここまでで最初の例の
Optional<String> hogeOpt = Optional.ofNullable(getHoge()); // 値をラップする hogeOpt.ifPresent(hoge -> System.out.println(hoge.length())); // 値が存在する場合のみ実行
が動かせますね!
- Main.java
package com.company; import functional.Action; import option.Option; public class Main { public static void main(String[] args) { Option<String> hogeOpt = Option.ofNullable(getHoge()); hogeOpt.ifPresent(new Action<String>() { @Override public void call(String hoge) { System.out.println(hoge.length()); // => 4 } }); Option<String> nullOpt = Option.ofNullable(getNull()); nullOpt.ifPresent(new Action<String>() { @Override public void call(String value) { System.out.println(value.length()); // 何も起こらない } }); } private static String getHoge() { return "Hoge"; } private static String getNull() { return null; } }
見た目がだいぶ残念ですがしょうがない。我々は与えられた環境で頑張るだけです。
さて、ifPresentだけでは値を返せない使えない置物状態なので次の例に進みましょう。
Optional<String> hogeOpt = Optional.ofNullable(getHoge());
Optional<Integer> lengthOpt = hogeOpt.map(hoge -> hoge.length());
次はmapメソッドですが、これもC#に習ってFuncなんてインターフェースを追加すればサクっと終わりそうですね。
戻り値がOptional型になっている点だけ気をつければよさそうです。
- Func.java
package functional; public interface Func<T, U> { public U apply(final T value); }
- Option.java
public abstract <U> Option<U> map(final Func<T, U> fun);
- Some.java
@Override public <U> Option<U> map(final Func<T, U> fun) { return new Some<U>(fun.apply(_value)); }
- None.java
@Override public <U> Option<U> map(final Func<T, U> fun) { return new None<U>(); }
使う側はこんな感じ
- Main.java
Option<Integer> lengthOpt = hogeOpt.map(new Func<String, Integer>() { @Override public Integer apply(String hoge) { return hoge.length(); } }); Option<Integer> lengthNullOpt = nullOpt.map(new Func<String, Integer>() { @Override public Integer apply(String value) { return value.length(); } });
実行してもぬるぽしません。素敵!
さて次はflatMapです。
// opt1とopt2に値が存在する場合のみ、処理結果を返す Optional<String> opt1 = getHogeOpt(); Optional<String> opt2 = getHogeOpt(); Optional<String> opt3 = opt1.flatMap(str1 -> opt2.flatMap(str2 -> { return Optional.of(str1 + str2); }));
Option<T>型がOption<U>型を受け取ってなんかする関数を受け取るみたいですね。
flatMap<T> : Func<T, Option<R>> -> Option<R>
こんな感じでやってみましょう。
- Option.java
public abstract <R> Option<R> flatMap(Func<T, Option<R>> fun);
- Some.java
@Override public <R> Option<R> flatMap(Func<T, Option<R>> fun) { return fun.apply(get()); }
- None.java
@Override public <R> Option<R> flatMap(Func<T, Option<R>> fun) { return new None<R>(); }
自己カプセル化しちゃってるついでに例の中でofが使われているのでついでに基本的なメソッド?にあるof、isPresent、getの3つを実装しましょう。
http://qiita.com/shindooo/items/815d651a72f568112910#2-6
// Optionalオブジェクトを生成するofメソッドは、引数がnullだとヌルポを投げる… Optional<String> hogeOpt = Optional.of(getHoge()); if (hogeOpt.isPresent()) { // 値が存在しているか? => 従来のnullチェックと同じことをしている… // 値を取得するgetメソッドは、値が存在していない場合実行時例外を投げる…(NoSuchElementException) String hoge = hogeOpt.get(); System.out.println(hoge); }
- Option.java
public static <T> Option<T> of(final T value) { if(value == null) throw new NullPointerException("valueがnullだよ"); return new Some<T>(value); } public abstract boolean isPresent(); public abstract T get() throws NoSuchElementException;
- NoSuchElementException.java
package option; public class NoSuchElementException extends RuntimeException { }
- Some.java
@Override public boolean isPresent() { return true; } @Override public T get() { return _value; }
- None.java
@Override public boolean isPresent() { return false; } @Override public T get() { throw new NoSuchElementException(); }
ココらへんは見たまんまですね。
さて使う側です。
final Option<String> opt1 = Option.ofNullable(getHoge()); final Option<String> opt2 = Option.ofNullable(getHoge()); final Option<String> optNull = Option.ofNullable(getNull()); Option<String> opt3 = opt1.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return opt2.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); // => HogeHoge return Option.of(str1 + str2); } }); } }); Option<String> otp4 = opt1.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return optNull.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); // => 何も出力されない return Option.of(str1 + str2); } }); } }); Option<String> opt5 = optNull.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return opt1.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); // => 何も出力されない return Option.of(str1 + str2); } }); } }); Option<String> otp6 = optNull.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return optNull.flatMap(new Func<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); // => 何も出力されない return Option.of(str1 + str2); } }); } });
Some×Some,Some×None,None×Some,None×Noneを試してみても実際にprintlnが実行されるのはSome×Someのopt3のみ。
ぬるぽをいい感じに排除できてますね。
次はfilterです。
http://qiita.com/shindooo/items/815d651a72f568112910#2-4
Optional<String> hogeOpt = Optional.ofNullable(getHoge()); Optional<String> filteredOpt = hogeOpt.filter(hoge -> hoge.length() >= 2);
悩むところもない感じなのでサクサクと。
- Option.java
public abstract Option<T> filter(Func<T, Boolean> fun);
- Some.java
@Override public Option<T> filter(Func<T, Boolean> fun) { if(fun.apply(get())) { return this; } return new None<T>(); }
- None.java
@Override public Option<T> filter(Func<T, Boolean> fun) { return this; }
使う側も単純
Option<String> filteredOpt = opt1.filter(new Func<String, Boolean>() { @Override public Boolean apply(String value) { return value.length() > 2; } }); System.out.println(opt1 == filteredOpt); // => true
自分自身を返しているので比較してもちゃんとtrueが出力されます。
- まとめ
Java8が使えない状況でもやろうと思えば(見た目はどうであれ)大抵の事はできちゃいますね。
GitHubでjava optionで検索すると137件とこれと似たような練習やもっと実用性のある実装など提供されていたりします。
まぁ個人の遊びとしてはこんなもんでしょうか。
どうせ仕事では使えないので気分転換にしかならないんですけどね。
忘れてたわけでない最後の値を取り出す…ですが別に抽象メソッドにする必要がなかったものなどを引き上げるなどリファクタリングしたの出して終わりにしましょう。
べ、別に面倒になったとかそんなわけでは。
- Option.java
package option; import functional.Action1; import functional.Func0; import functional.Func1; public abstract class Option<T> { public static <T> Option<T> ofNullable(final T value) { if(value == null) return new None<T>(); return new Some<T>(value); } public void ifPresent(final Action1<T> action1) { if(isPresent()) action1.call(get()); } public <U> Option<U> map(final Func1<T, U> fun) { if(isPresent()) { return Option.of(fun.apply(get())); } return new None<U>(); } public <R> Option<R> flatMap(Func1<T, Option<R>> fun) { if(isPresent()) { return fun.apply(get()); } return new None<R>(); } public Option<T> filter(Func1<T, Boolean> fun) { if(isPresent() && fun.apply(get())) { return this; } return new None<T>(); } public T orElse(T defaultValue) { if(isPresent()) { return get(); } return defaultValue; } public T orElseGet(Func0<T> def) { if(isPresent()) { return get(); } return def.apply(); } public static <T> Option<T> of(final T value) { if(value == null) throw new NullPointerException("valueがnullだよ"); return new Some<T>(value); } public abstract boolean isPresent(); public abstract T get(); }
- Some.java
package option; final class Some<T> extends Option<T> { final T _value; Some(final T value){ _value = value; } @Override public boolean isPresent() { return true; } @Override public T get() { return _value; } }
- None.java
package option; final class None<T> extends Option<T> { @Override public boolean isPresent() { return false; } @Override public T get() throws NoSuchElementException { throw new NoSuchElementException(); } }
- NoSuchElementException.java
package option; public class NoSuchElementException extends RuntimeException { }
of, get, isPresentは使いにくいですが実装側からしたらコレ以上調子のいいやつはいませんね。
おかげでSomeとNoneがスッキリです。
- Action1.java
package functional; public interface Action1<T> { public void call(final T value); }
- Func0.java
package functional; public interface Func0<T> { T apply(); }
- Func1.java
package functional; public interface Func1<T, U> { U apply(final T value); }
- Main.java
package com.company; import functional.Action1; import functional.Func0; import functional.Func1; import option.Option; public class Main { public static void main(String[] args) { Option<String> hogeOpt = Option.ofNullable(getHoge()); // 値をラップする hogeOpt.ifPresent(new Action1<String>() { @Override public void call(String hoge) { System.out.println(hoge.length()); // 値が存在する場合のみ実行 } }); Option<Integer> lengthOpt = hogeOpt.map(new Func1<String, Integer>() { @Override public Integer apply(String hoge) { return hoge.length(); } }); Option<String> nullOpt = Option.ofNullable(getNull()); nullOpt.ifPresent(new Action1<String>() { @Override public void call(String value) { System.out.println(value.length()); } }); Option<Integer> lengthNullOpt = nullOpt.map(new Func1<String, Integer>() { @Override public Integer apply(String value) { return value.length(); } }); final Option<String> opt1 = Option.ofNullable(getHoge()); final Option<String> opt2 = Option.ofNullable(getHoge()); final Option<String> optNull = Option.ofNullable(getNull()); Option<String> opt3 = opt1.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return opt2.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); return Option.of(str1 + str2); } }); } }); Option<String> otp4 = opt1.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return optNull.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); return Option.of(str1 + str2); } }); } }); Option<String> opt5 = optNull.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return opt1.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); return Option.of(str1 + str2); } }); } }); Option<String> otp6 = optNull.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str1) { return optNull.flatMap(new Func1<String, Option<String>>() { @Override public Option<String> apply(final String str2) { System.out.println(str1 + str2); return Option.of(str1 + str2); } }); } }); Option<String> filteredOpt = opt1.filter(new Func1<String, Boolean>() { @Override public Boolean apply(String value) { return value.length() > 2; } }); System.out.println(opt1 == filteredOpt); // => true String hoge = opt1.orElse("デフォルト値"); String hoge2 = opt1.orElseGet(new Func0<String>() { @Override public String apply() { return "コストの高いデフォルト値"; } }); } private static String getHoge() { return "Hoge"; } private static String getNull() { return null; } }