Java好き

カテゴリ: JavaSE

nullを扱う新たな方法として
JavaSE8からOptionalというクラスが
java.utilパッケージに加わった。
(他の言語ではすでにあった)

Javaではnullのオブジェクトに対して、
フィールドの参照やメソッドの呼び出しを行うと
すぐにNullPointerExceptionになってしまう。

なのでnullの扱いには注意が必要で、
nullをうまく扱えるかが初心者との境目だったりもする。

普通にnullチェックをしたり、NullObjectを作ってみたり
という方法があるが、Optionalはこれらとはまた違う方法。

慣れないと何がいいのかピンとこないが、
なるべくわかりやすくOptionalの良さをまとめる。

Optionalがnullを安全に扱う仕組み

Optionalは対象となるオブジェクトを保持する(だけ)。
JavaDocにも「コンテナ・オブジェクト」と書かれている。

Optionalクラス図

保持したオブジェクトを取得・利用する場合は、
必ずメソッドを介する。

そのメソッドは保持したオブジェクトがnullであっても
セーフティ(安全)な設計になっている。(get()を除く)

また、Optional自体はNullObjectになっているので
nullになることがない(させない)。
値が未定の場合でもOptional.empty()を割り当てればよい。

このように、

  • 対象となるオブジェクトがnullでもメソッドを介すので、安全
  • Optional自体もnullになることがないので、安全

ということで、Optionalはnullを安全に扱うことができる。

Optionalがnullを安全に扱う例
String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElse("(デフォルト)");
 
// 「(デフォルト)」と表示
System.out.println(displayValue);

vs NullObject

仕組みとしては上記の通りだが、
ほかの方法(NullObject)と比べてみる。

nullを扱う工夫された方法として、
NullObjectという方法(パターン)がある。

それはそれでよいが、
Optionalは「NullObjectを定義しなくてもいい」
という点で優れている。

NullObjectでは一番最小の実装であっても、
オブジェクトの生成やファクトリメソッドは必要。

1,2個であれば何ら問題ないが、
いくつもあると面倒。

その点で言うとOptionalは、
新たにコードを実装する必要がない。

また、nullのときのメソッドの挙動も問題があれば
オーバーライドしなくてはいけない。

一方Optionalは、
nullのときの挙動もそのオブジェクトを利用する人が決めることになるので
使う人がその状況で決めればいい。

Optional自体がNullObjectであるということからもわかるように
OptionalとNullObjectでは活躍の舞台が微妙に違うかもしれない。

ただ、nullを扱うという点では
NullObjectよりも手間がかからないのは
明らかなことがわかる。

フィールドにもメソッドの引数・戻り値にもOptionalを使う

仕組みや良さはこんなところ。
ではどのように使っていくかになるが、
意味的にnullがある可能性があればどこでも使える。

例えば、次のようなフィールドなどである。

public class Person {
    String name;
    String mailAddress;
    Optional<String> twitterAccount;
}

この例では、
Personは名前とメールアドレスは必ずあるが
ツイッターアカウントはあるかどうかわからないので
Optionalとしている。

Optionalとしていることで、
必須項目ではないことが伝わる。

他にもメソッドの引数、戻り値なども利用できる。

public interface PersonDao {
 
    Optional<Person> find(String name);
 
}

Daoなんかでもよく、
結果オブジェクトを必ず取得できるか否かの挙動で
迷うことがある。

そんなことで迷うよりも
Optionalにしてクライアント側で
挙動を決めさせればよい。

このようにあらゆるところで、
Optionalは利用できる。

あとは使い慣れるだけ

これまでの説明のとおり、
Optionalは直接nullチェックやNullObjectよりも
スマートに扱える。

問題はOptionalのスタイルになれるのに
時間が必要なこと。

Javaでは相当後発になるので
サンプルが少ない。

ただ、慣れてくると無くてはならないものになる。

lomokの@NonNullなどと併用していけば
「nullを渡さない、返さない、保持しないといったこと」が
スマートに徹底できる。

nullを使いたくなったら、Optionalを思い浮かべて欲しい。

Stream.forEach()でMapを作ってはいけない。

toMap()

toMap()は、Mapのインスタンスを生成する。
そのとき、キーと値をそれぞれ生成するためのFunctionを指定する。

シグニチャ
public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
                                                    Function<? super T,? extends U> valueMapper)
toMap()の例
Map<Integer, String> expected = new HashMap<>();
expected.put(1, "1");
expected.put(2, "2");
expected.put(3, "3");
 
Integer[] numbers = { 1, 2, 3 };
Stream<Integer> stream = Arrays.stream(numbers);
Map<Integer, String> actual = stream.collect(Collectors.toMap(i -> i, String::valueOf));
assertThat(actual, is(expected));

キー重複の対処

Mapを生成するときに気をつけなくはいけないのが
キーの重複。

2つの引数をとるtoMapではキーの重複が発生すると
例外となってしまう。

× キーが重複するのでIllegalStateExceptionになる
Integer[] numbers = { 1, 2, 2 };
Stream<Integer> stream = Arrays.stream(numbers);
stream.collect(Collectors.toMap(i -> i, String::valueOf));

これはメソッドの定義の問題。
2つの引数をとるtoMap()では
キー重複が発生すると例外にする用に定義されている。

もちろんキーの重複があっても、
Mapは生成することは当然可能。

mergerを指定するtoMap()を用いて、
自分でmergerを設定すれば回避できる。

シグニチャ
public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
                                                    Function<? super T,? extends U> valueMapper,
                                                    BinaryOperator<U> mergeFunction)

Stream.forEach()でCollectionを作ってはいけない

CollectorsではCollectionインスタンスに
リダイレクトするためのCollectorが3つ用意されている。

メソッド 概要
toList() ListのインスタンスにするためのCollectorを返す。
インスタンスは、実装で決定される。
toSet() SetのインスタンスにするためのCollectorを返す。
インスタンスは、実装で決定される。
toCollection() CollectionのインスタンスにするためのCollectorを返す。
インスタンスは、supplierで指定する。

StreamからforEach()でコレクションを作成することもできるが
バグの原因にもなるのでやってはいけない。

Stream.forEach()でCollectionを作ってはいけない

Stream処理後にListやMapにしたいことがある。

このときに(JDK7)以前の思考で、
forEachを利用して生成したいと考えてしまうが、
これをやってはいけない。

×よくない例
Integer[] numbers = { 1, 2, 3, 4 };
Stream<Integer> stream = Arrays.stream(numbers);
 
List<Integer> actual = new ArrayList<>();
stream.forEach(i -> {
    actual.add(i);
});

これ自体は動作するが、
例えばparallelストリームの場合はバグである。
(ArrayListはスレッドセーフではないため)

こういった場合は、CollectorsのtoCollection()やtoMap()を用いる。

toList() toSet()

toList()とtoSet()はそれぞれ、ListとSetのインスタンスに変換する。
(実装によるが)ArrayListとHashSetのインスタンスになる。

toList()の例
List<Integer> expected = Arrays.asList(1, 2, 3, 4);
 
Integer[] numbers = { 1, 2, 3, 4 };
Stream<Integer> stream = Arrays.stream(numbers);
List<Integer> actual = stream.collect(Collectors.toList());
assertThat(actual, is(expected));

Setインスタンス生成時は注意が必要で、
重複をチェックするためにequals()が評価されるため
大量件数ではparallel()であってもスピードが出ない場合がある。

toCollection()

ArrayListやHashSetではなく
同期に対応したCollectionのインスタンスにしたい場合は
toCollection()を利用する。

シグニチャ
toCollection(Supplier<C> collectionFactory)

toCollection()では、
supplierで生成するCollectionのインスタンスを指定できる。

toCollection()の例
List<Integer> expected = Arrays.asList(1, 2, 3, 4);
 
Integer[] numbers = { 1, 2, 3, 4 };
Stream<Integer> stream = Arrays.stream(numbers);
CopyOnWriteArrayList<Integer> actual = stream.collect(Collectors.toCollection(CopyOnWriteArrayList::new));
assertThat(actual, is(expected));

Collectorはストリームのリダイレクション操作を行うための
インターフェース。

Collectorクラス図

リダイレクション操作を行うためのインターフェース

リダイレクションとは、出力先を変更すること。
Linuxのコマンドなんかでも出てくる概念で、
Linuxコマンドではプログラムの出力結果を違うファイルへと出力したりできる。

Collectorの場合は、入力をStreamの各要素として
「コレクションやMaoとして」出力したり、「何らかの計算処理を挟んで集計して」出力したり
することができる。

Collector自体は処理を行わない

実はCollectorはそれ自体で何らかの処理をすることがない。
Collectorはリダイレクション操作に必要な処理を保持しているだけ。
メソッド群にも処理をするメソッドがない。

実際の操作はStreamが行う。
Collecotrを使う上でのルールみたいなものがあって
それを守って使っている。

CollectorはStreamありきの存在である。

Collectorsに定義済みのものを使う

Collectorのインスタンスはofのstaticメソッドで生成するか
Collectorsクラスに定義済みのものを使う。

Collectorsクラスに大体のものは揃っているので
Collectorを実装する機会はほとんどないかもしれない。

3つの型引数

Collectorは「T」「A」「R」の3つの型引数を受け取る。

基本的に「T」と「R」に注意する。
(基本的に真ん中のAは気にしなくていい。?ということも多い。
内部の実装者だけが気にすればいい。)

「終端操作」だけまとめる。

Streamの操作には「中間操作」と「終端操作」があり、
ここでは「終端操作」だけをまとめる。

一覧表にすると次のようになる。

分類 終端操作 概要
集める collect 集合にする
toArray 配列にする
副作用 forEach、forEachOrdered 副作用を起こさせる
数える count 件数を数える
集計 reduce リデュースする
条件判定 anyMatch allMatch noneMatch 条件を判定する
比較 max min 最大値・最小値を返す
取り出す findFirst findAny 要素を返す

必ずStreamオブジェクトを返す「中間操作」と異なり、
「終端操作」は戻り値が多岐にわたる。

collection array void boolean Optionalと
それぞれ違う。

「終端操作」は一度終端操作が終わったStreamに対しては
実行できないので注意する。

collect

<R,A> R collect(Collector<? super T,A,R> collector)

ストリームの要素を集合として出力(生成)する。

引数のcollectorについては、
基本的なものはCollectorsに用意されているので
それを利用する。

List<Integer> expected = Arrays.asList(1, 2, 3, 4);
 
Integer[] numbers = { 1, 2, 3, 4 };
List<Integer> actual = Arrays.stream(numbers).collect(Collectors.toList());
assertThat(actual, is(expected));

toArray

<A> A[] toArray(IntFunction<A[]> generator)

ストリームの要素を配列として出力(生成)する。

collectで集合は作成できるので
配列はこれで作れる。

引数に配列を生成するIntFunctionを
渡せばよい。

Integer[] expected = { 1, 2, 3, 4 };
 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
Integer[] actual = numbers.stream().toArray(Integer[]::new);
assertThat(actual, is(expected));

forEach、forEachOrdered

void forEach(Consumer<? super T> action)

終端操作として、副作用を発生させる場合に用いる。

以前(JavaSE7)のfor文の感覚からすると
forEachで全部作りたくなってしまうが
それはガマンしなくてはいけない。

それをやってしまうと処理をforEachに押し込んでいるだけなので
意味が伝わりにくく、並列化もしにくい。

中間操作の組合せを検討するべき。

List<Integer> expected = Arrays.asList(2, 3, 4);
 
List<AtomicInteger> numbers = Arrays.asList(new AtomicInteger(1), new AtomicInteger(2), new AtomicInteger(3));
Consumer<AtomicInteger> action = number -> number.incrementAndGet();
numbers.stream().forEach(action);
 
// 比較するために変換
List<Integer> actual = numbers.stream().map(AtomicInteger::intValue).collect(Collectors.toList());
assertThat(actual, is(expected));

count

long count()

ストリームの要素数を返す。

long expected = 4;
 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
long actual = numbers.stream().count();
assertThat(actual, is(expected));

reduce

T reduce(T identity,BinaryOperator<T> accumulator)

map/reduceでおなじみのreduce。

計算結果が次の引数になるかたちで、
累積して処理(集計)することが可能なメソッド。

次のような処理が行われるイメージ。

T result = identity;
result = accumulator(result,element1);
result = accumulator(result,element2);
result = accumulator(result,element3);
return result;
Integer expected = 24;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
 
BinaryOperator<Integer> accumulator = (a, b) -> a * b;
Integer actual = numbers.stream().reduce(1, accumulator);
assertThat(actual, is(expected));
                

anyMatch allMatch noneMatch

boolean anyMatch(Predicate<? super T> predicate)

predicate(条件)にどれだけマッチするか調べる。

結果がtrueになる条件は、
それぞれ次のようになる。

メソッド trueになる条件
anyMatch predicateの評価が1つでもtrueの場合
allMatch predicateの評価が全てtrueの場合
noneMatch predicateの評価が全てfalseの場合
boolean expected = true;
 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
Predicate<Integer> predicate = i -> i == 4;
boolean actual = numbers.stream().anyMatch(predicate);
assertThat(actual, is(expected));

max min

Optional<T> max(Comparator<? super T> comparator)

compareで比較した結果の最大値や最小値を返す。

結果はOptionalで返ってくるのが
JavaSE8っぽい。

Integer expected = 4;
 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
Optional<Integer> optional = numbers.stream().max(Integer::compare);
Integer actual = optional.orElse(null);
assertThat(actual, is(expected));

findFirst findAny

Optional<T> findFirst()

要素を取り出す。

maxと同様にOptionalで結果が返ってくる。

Integer expected = 1;
 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
Optional<Integer> optional = numbers.stream().findFirst();
Integer actual = optional.orElse(null);
assertThat(actual, is(expected));