Java好き

カテゴリ: JavaSE標準ライブラリ

JavaSE8から加わったOptional。
ストリームAPIやラムダ式に隠れてしまっているがとても便利。

ただ慣れてないと使いにくい部分はあるので、
使い方をまとめる。

Optionalがよくわからない場合は
こちら→Optional - nullを扱う新たな方法

生成

3つのファクトリメソッドが用意されているので
いずれかを使う。

値がnullであっても空のOptionalにすればよいので、
基本的にはofNullable()、empty()を中心に考える。

メソッド 概要
of() nullの場合、NullPointerExceptionになる。
ofNullable() nullの場合、空のOptionalを返す。
empty() 空のOptionalを返す。

Immutableなので1度中身のオブジェクトを決めてしまったら
入れ替えるのではなく、新たに生成する。

中身のオブジェクトの取得

Optionalから中身のオブジェクトを取得する場合、
それがnullだったときの方針を決めておかないと
うまく取得することができない。

代替値を設定するや例外を投げるといった方針によって、
メソッドを選ぶ。

アンチパターン

JavaDocを見るとget()が目につくので、
isPresent() + get() とやりたくなってしまうが
これをやってしまってはnullチェックしているのと大差ない。

Optional#orElse(null)とする方がマシ。
まずはorElseXXX()の中から検討する。

例 isPresent() + get()はなるべく避ける
String value = null;
Optional<String> optional = Optional.ofNullable(value);
 
String displayValue = null;
if (optional.isPresent())
    displayValue = optional.get();

orElse()

中身がnullの場合でも、そのときの値を指定できる。
orElse(null)も可。

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElse("(デフォルト)");

orElseGet()

orElseとほぼ同じであるが、(ファクトリ)関数オブジェクトを指定できる。
(毎回違うインスタンスを生成できる)

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElseGet(() -> "(デフォルト)");

orElseThrow()

中身がnullの場合、生成した例外をスローする。

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElseThrow(RuntimeException::new);

中身のオブジェクトのメソッドを実行する

中身をいちいち取得しなくても、
中身のオブジェクトのメソッドを実行できる。

ifPresent()

consumerを渡すことで、副作用をともなう処理ができる。
もちろん、中身がnullの場合は何も行われないだけ。

isPresent()とメソッド名が酷似しているので注意。

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
optionalValue.ifPresent(System.out::println);

中身のオブジェクトが保持しているオブジェクト(値)を
取得する

例えば、つぎのような場合

Optional_サンプルクラス図

これも中身のオブジェクトを取得しなくても、
処理することができる。

map()

戻り値がOptionalなので、
そのOptionalに対してまたメソッドを実行できる。

Person person = new Person("田中", "tanaka@example.com", Optional.empty());
Optional<Person> optionalPerson = Optional.ofNullable(person);
 
Optional<String> name = optionalPerson.map(Person::getName);
String displayedName = name.orElse("(未登録)");

flatMap()

mapとの違いは、保持しているオブジェクトがOptionalの場合
そのまま取り出せるというところ。

Person person = new Person("田中", "tanaka@example.com", Optional.empty());
Optional<Person> optionalPerson = Optional.ofNullable(person);
 
Optional<String> twitterAccount = optionalPerson.flatMap(Person::getTwitterAccount);
String displayeTtwitterAccount = twitterAccount.orElse("(未登録)");

日付変換用のクラス。

DateFormatは、
「文字列」⇔「Date」の変換で利用する。

特に「寛大なparse」と「厳密なparse」が
あるので注意する。

基本

「寛大なparse」と「厳密なparse」

文字列からDateに変換する場合、
この2つのモードみたいなものがDateFormatにはある。

違いとしては
カレンダー上の存在しない日付(例えば2011/2/31など)をparse()する場合、
「寛大なparse」では解析できて、「厳密なparse」は例外となる。

日付入力のバリデーションで利用する場合などは、
この違いは大きいので注意する。

デフォルトは「寛大なparse」なので、変更する場合は次のようにする。

dateFormat.setLenient(false);

和暦

"ja", "JP", "JP"のLocaleを指定すると、和暦表示できる。

final String pattern = "GGGGyy年M月d日";
final Locale japaneseLocale = new Locale("ja", "JP", "JP");
DateFormat dateFormat = new SimpleDateFormat(pattern, japaneseLocale);
  
Calendar calendar = new GregorianCalendar(2011, Calendar.JANUARY, 2);
log.debug(dateFormat.format(calendar.getTime()));
出力
平成2312

例外をうっかりキャッチし損ねると、
ログも出力されず原因究明が困難になったりします。

そんなときには、UncaughtExceptionHandlerを
設定しておくと便利です。

例外をキャッチし損ねるのを防ぐ

Thread.UncaughtExceptionHandlerは、
スレッドがキャッチされない例外により終了したときに
呼び出されるメソッドを定義したインタフェース。(ハンドラともいう)

例えば、スタンドアロンなどで例外をキャッチできない場合は、
main()まで戻されて終了する。

そのときThread.UncaughtExceptionHandlerを設定しておけば、
main()を終了する前にUncaughtExceptionHandlerを呼び出してから
終了する。

これを利用することで、うっかり例外をキャッチしそびれても
最後の砦となって、ログ出力などが可能。

メインスレッドにおける設定

メインスレッドでUncaughtExceptionHandlerを設定する場合は、
次のようになる。

例.UncaughtExceptionHandlerの設定
public class UncaughtExceptionHandlerSample {
    private static final Logger log = LoggerFactory.getLogger(UncaughtExceptionHandlerSample.class);
 
    private static final UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER = new UncaughtExceptionHandler() {
 
        /** キャッチできない場合は、ログに書き込む */
        @Override public void uncaughtException(Thread thread, Throwable throwable) {
            log.error("キャッチできない例外発生", throwable);
        }
 
    };
 
    public static void main(String[] args) {
        Thread.currentThread().setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER);
    }
 
}

万が一ということもあるので、
スタンドアロンの場合などは設定しておいた方がいい。

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));