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型を返して、2番めはOptionに対して副作用を起こしている感じですね。
また、後者のものは呼び出している側で動作を決めています。
そんなわけでよく考えずに抽象クラスとしてOptionを定義してみます。

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を隠そうとしているのに意味が無い気がするので、「値が存在する」と「値が存在していない」事を表すクラスを作って場合分けしてそれを返すようにしましょう。

    public static <T> Option<T> ofNullable(final T value) {
        if(value == null)
            return new None<T>();
        return new Some<T>(value);
    }
package option;
final class Some<T> extends Option<T> {

    final T _value;

    Some(final T value){
        _value = value;
    }

    @Override
    public void ifPresent() { }
}
package option;
final class None<T> extends Option<T> {
    @Override
    public void ifPresent() { }
}

次にifPresentですが、大人しくインターフェースを渡してそれで処理すれば良さそうです。
最近めっきり仕事で使う機会の無いC#を忘れないためにもActionと名前を付けておきましょう。

package functional;
public interface Action<T> {
    public void call(final T value);
}
public abstract void ifPresent(final Action<T> action);
@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())); // 値が存在する場合のみ実行

が動かせますね!

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型になっている点だけ気をつければよさそうです。

package functional;
public interface Func<T, U> {
    public U apply(final T value);
}
public abstract <U> Option<U> map(final Func<T, U> fun);
@Override
public <U> Option<U> map(final Func<T, U> fun) {
    return new Some<U>(fun.apply(_value));
}
@Override
public <U> Option<U> map(final Func<T, U> fun) {
    return new None<U>();
}

使う側はこんな感じ

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>

こんな感じでやってみましょう。

    public abstract <R> Option<R> flatMap(Func<T, Option<R>> fun);
    @Override
    public <R> Option<R> flatMap(Func<T, Option<R>> fun) {
        return fun.apply(get());
    }
    @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);                                                             
}    
    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 { }
    @Override
    public boolean isPresent() {
        return true;
    }

    @Override
    public T get() {
        return _value;
    }
    @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);

悩むところもない感じなのでサクサクと。

    public abstract Option<T> filter(Func<T, Boolean> fun);
    @Override
    public Option<T> filter(Func<T, Boolean> fun) {
        if(fun.apply(get())) {
            return this;
        }
        return new None<T>();
    }
    @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が使えない状況でもやろうと思えば(見た目はどうであれ)大抵の事はできちゃいますね。
GitHubjava optionで検索すると137件とこれと似たような練習やもっと実用性のある実装など提供されていたりします。
まぁ個人の遊びとしてはこんなもんでしょうか。
どうせ仕事では使えないので気分転換にしかならないんですけどね。

忘れてたわけでない最後の値を取り出す…ですが別に抽象メソッドにする必要がなかったものなどを引き上げるなどリファクタリングしたの出して終わりにしましょう。
べ、別に面倒になったとかそんなわけでは。

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();
}
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;
    }
}
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がスッキリです。

package functional;

public interface Action1<T> {
    public void call(final T value);
}
package functional;

public interface Func0<T> {
    T apply();
}
package functional;

public interface Func1<T, U> {
    U apply(final T value);
}
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;
    }
}