Java 8 新機能つまみぐい

  • 269
    Like
  • 0
    Comment
More than 1 year has passed since last update.

今やってるプロジェクトのソースレベルも1.8に上がったことだし、ここらへんでJava 8の新機能をおさらい。

Lambda

Java 8の目玉機能、ラムダ式。まずは無名クラスを使った今までのコールバック。いかにもJavaな長ったらしい記述。

Java7
Arrays.sort(array, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareToIgnoreCase(b);
    }
});

Java 8のラムダ式を使えばこう。どうせIDEに自動生成させるとはいえ、本当に注目すべき処理だけに集中できるのは嬉しい。

Java8
Arrays.sort(array, (a, b) -> a.compareToIgnoreCase(b));

ラムダ式の書式にはいくつかのパターンがある。例えば、引数が1つの場合は引数リストのカッコが不要。逆に本文が1行でない場合は、 -> の後に {} でブロックを明示する。

Java7
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent event) {
        // do something 1
        // do something 2
    }
});
Java8
button.addActionListener(event -> {
    // do something 1
    // do something 2
});

引数がない場合は、次のように空の () で引数リストを表す。

Java7
Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        // do something
    }
});
Java8
Thread t = new Thread(() -> {
    // do something
});

ラムダ式の裏にある新しい言語仕様は、関数インターフェースというもの。abstractなメソッドが1つだけのインターフェースを関数インターフェースといい、慣習的に @FunctionalInterface アノーテーションを付ける。

Java8
@FunctionalInterface
public interface Runnable {
    void run();
}
Java8
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

関数インターフェースが要求される場所には、具象クラスを定義する代わりにラムダ式を渡すことができる。これがラムダ式の秘密。

Java8
Comparator<String> comparator =
        (a, b) -> Integer.compare(a.length(), b.length());

Java 8には多数の関数インターフェースがあらかじめ定義されているので、独自のものを定義する前に使えるものがないか確認する。

関数インターフェース メソッドシグネチャ 説明
Supplier<T> T get() T型の出力を返す
Consumer<T> void accept(T) T型の入力を受け取って処理する
BiConsumer<T, U> void accept(T, U) T型とU型の入力を受け取って処理する
Function<T, R> R apply(T) T型の入力を受け取ってR型の出力を返す
BiFunction<T, U, R> R apply(T, U) T型とU型の入力を受け取ってR型の出力を返す
UnaryOperator<T> T apply(T) T型の入力を受け取って同じT型の出力を返す → Function<T, T>
BinaryOperator<T> T apply(T, T) T型の入力2つを受け取って同じT型の出力を返す → BiFunction<T, T, T>
Predicate<T> boolean test(T) T型の入力を受け取って真偽値を返す
BiPredicate<T, U> boolean test(T, U) T型とU型の入力を受け取って真偽値を返す

C++のテンプレート触ったことがあると馴染みのある感じの名前。また、プリミティブ型でオートボクシングを避けるために、 IntSupplierDoubleFunction などの特殊化が多数あるのであわせて確認しておくこと。

自分が作るAPIの引数を関数インターフェースにしておけば、そのAPIの利用者はラムダ式を使うことができる。そうすると単に記述量を減らせるだけでなく、遅延評価の恩恵も受けられる。よくあるログ出力の例はこんな感じ。本当にログを出力する必要があるときになって初めてメッセージ本文が生成されることに注意する。

Java8
public class Logger {
    public void log(Level level, Supplier<String> msg) {
        if (isLoggable(level)) {
            write(level, msg.get());
        }
    }
};

logger.log(Level.INFO, () -> "log message: " + someTimeConsumigMethod());

最後に、関数インターフェースには、ラムダ式の代わりに既存のメソッドやコンストラクタの参照を渡すこともできる。

Java8
// e -> System.out.println(e)
button.addActionListener(System.out::println);

// (a, b) -> a.compareToIgnoreCase(b)
Arrays.sort(array, String::compareToIgnoreCase);

// x -> new Person(x)
names.stream().map(Person::new);

Stream

Java 8のもう一つの目玉機能、ストリーム。今までだとデータ集合を操作するときのコードはfor文を使ってこんな感じ。

Java7
List<String> list = Arrays.asList("C", "C++", "Java", "Scala", "Ruby");

int count = 0;
for (String elem : list) {
    if (elem.startsWith("C")) {
        count += elem.length();
    }
}

これがJava 8だと次の通り。 stream() メソッドで Collection から Stream を取得し、メソッドチェーンで処理をつなげていく。

Java8
long count = list.stream()
                 .filter(s -> s.startsWith("C"))
                 .mapToInt(s -> s.length())
                 .sum();

この例のように、ストリームを使ったデータ操作は次の3つのパートに分解できる。

  1. ストリームの作成
    • stream()
    • コレクションや配列から Stream を取得する
  2. 中間操作 (intermediate operator)
    • filter(), mapToInt()
    • Stream を絞り込んだり加工したりした新たな Stream を取得する
  3. 終端操作 (terminal operator)
    • sum()
    • Stream から最終的な結果を取得する。 終端操作のAPIを実行して初めて 中間操作のAPIも評価される

1つ1つのAPIを細かく見ていっても仕方ないので、ほかの例をいくつか挙げて説明に代える。

Java8
String[] words = Stream.of("C", "C++", "Java", "Scala", "Ruby")
                       .map(s -> s.toUpperCase())
                       .sorted()
                       .toArray(String[]::new);

//=> [C, C++, JAVA, RUBY, SCALA]
Java8
IntSummaryStatistics stats = IntStream.generate(() -> (int)(Math.random() * 100))
                                      .filter(n -> n >= 80)
                                      .distinct()
                                      .limit(3)
                                      .summaryStatistics();

//=> IntSummaryStatistics{count=3, sum=279, min=84, average=93.000000, max=98}
Java8
List<String> fizzBuzz = IntStream.rangeClosed(1, 100)
                                 .mapToObj(n ->
                                         (n % 15 == 0) ? "FizzBuzz" :
                                         (n % 3 == 0) ? "Fizz" :
                                         (n % 5 == 0) ? "Buzz" :
                                         String.valueOf(n))
                                 .collect(Collectors.toList());

//=> [1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, ...

すでに例にも出てきたが、ラムダ式と同様に IntStream, DoubleStream といったプリミティブ型用の特殊化がある。他にも、グループ化など高度なデータ操作もできるのだが、私はここに挙げたくらいのコードまでしか一目見て理解できる気がしない。他にどんな中間操作、終端操作APIがあるのかはJavadocを参照。

Stream APIだと宣言的な記述で可読性が上がるという意見もあるけど、個人的には慣れの問題だと思っている。本当に重要なのは、実装の詳細をライブラリ側に任せられるので、容易に並列化できるという点。 stream()parallelStream() に置き換えるだけで並列化される。

Java8
long count = list.parallelStream()
                 .filter(s -> s.startsWith("C"))
                 .mapToInt(s -> s.length())
                 .sum();

注意点としては、小さなデータセットで並列化してもオーバーヘッドが増えるだけで逆に遅くなる点、データ操作の種類によっては並列化の恩恵を受けられない点、などが挙げられる。実際に計測して判断するのがよいだろう。

Optional

個人的に一番嬉しい機能。値がないかもしれない、という状態を表現する Optional<T> が導入され、不必要に null を返さなくて良くなったのと、実装者の意図を明示しやすくなった。しいて言えば、値の存在チェックを言語的に強制する仕組みがあるともっと良いのだけど。

Java7
String lang = getConfigValue("lang");
if (lang == null) {
    lang = "en"
}
Java8
Optional<String> value = getConfigValue("lang");
String lang = value.orElse("en");
Java8
Optional<String> value = getConfigValue("lang");
String lang = value.orElseGet(() -> {
    // more complicated logic
    return "en";
});

Optional.isPresent() を使って呼び出し側で値の存在チェックをすることもできるが、従来のnullチェックと同じになってしまうのでJava 8っぽくない。

Java8
String lang = "en";
Optional<String> value = getConfigValue("lang");
if (value.isPresent()) {
    lang = value.get();
}

なお、Optional を返す側では、Optional.of()Optional.empty() を使って生成する。

Java8
public Optional<String> getConfigValue(String key) {
    if (!containsKey(key)) {
        return Optional.empty();
    }
    return Optional.of(getValue(key));
}

また、null になりうる値をラップする場合は、Optional.ofNullable() を使うこと。 Optional.of(null) は、NullPointerException になってしまう。

Date and Time

Java 8では日付を扱うAPIも拡充され、今まで Joda-Time なしでは面倒だった操作が標準でできるようになった。タイムゾーン情報をもたない LocalDateTime とタイムゾーン情報をもつ ZonedDateTime があるのだけど、 ZonedDateTime を使ってちゃんとタイムゾーンを意識したコードを書くのが無難だと思う。

Java8
ZonedDateTime now = ZonedDateTime.now();
  //=> 2015-05-24T23:13:17.254+09:00[Asia/Tokyo]

ZonedDateTime t1 = now.withMinute(0).withSecond(0).withNano(0);
  //=> 2015-05-24T23:00+09:00[Asia/Tokyo]

ZonedDateTime t2 = now.with(TemporalAdjusters.lastDayOfMonth());
  //=> 2015-05-31T23:13:17.254+09:00[Asia/Tokyo]

ZonedDateTime t3 = now.plusYears(1).minusHours(2);
  //=> 2016-05-24T21:13:17.254+09:00[Asia/Tokyo]

Calendar と同様に、それぞれのフィールドの値を取得することができる。月が 0 始まりじゃないよ!

Java8
ZonedDateTime t = ZonedDateTime.of(2015, 5, 24, 23, 13, 52, 0, ZoneId.of("Asia/Tokyo"));
t.getYear();         //=> 2015
t.getMonth();        //=> Month.MAY
t.getMonthValue();   //=> 5  (0始まりではない)
t.getDayOfYear();    //=> 144
t.getDayOfMonth();   //=> 24
t.getDayOfWeek();    //=> DayOfWeek.SUNDAY
t.getHour();         //=> 23
t.getMinute();       //=> 13
t.getSecond();       //=> 52
t.getNano();         //=> 0
t.getZone().getId(); //=> "Asia/Tokyo"

面倒になってきたから最後に夏時間の例だけ載せて終わりにする。

Java8
// 太平洋標準時の夏時間開始2時間前 2015-03-08T00:00-08:00[America/Los_Angeles]
ZonedDateTime t = ZonedDateTime.of(2015, 3, 8, 0, 0, 0, 0, ZoneId.of("America/Los_Angeles"));

// 1時間ずつ進めると、2:00 PST になると同時に 3:00 PDT に飛ぶ
ZonedDateTime t1 = t.plusHours(1);  //=> 2015-03-08T01:00-08:00[America/Los_Angeles]
ZonedDateTime t2 = t.plusHours(2);  //=> 2015-03-08T03:00-07:00[America/Los_Angeles]
ZonedDateTime t3 = t.plusHours(3);  //=> 2015-03-08T04:00-07:00[America/Los_Angeles]

// Duration は24時間分、Period はローカル時間で1日分だけ進める
ZonedDateTime next1 = t.plus(Duration.ofDays(1L));  //=> 2015-03-09T01:00-07:00[America/Los_Angeles]
ZonedDateTime next2 = t.plus(Period.ofDays(1));     //=> 2015-03-09T00:00-07:00[America/Los_Angeles]

Misc

他にもJava 8には多くの機能が追加されている。個人的に気になったところだけ紹介。

interfaceのdefault/staticメソッド

今まではインターフェースが実装をもつことはできなかったけど、Java 8からはデフォルト実装やstaticメソッドを提供できるようになった。今後は、 Collections クラスのような専用ユーティリティクラスを別に用意しなくても、インターフェース自身にユーティリティメソッドを定義できる。

Java8
public interface Collection<E> extends Iterable<E> {
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

利便性のためというよりは、既存コードを壊さずにStream APIを導入するにはそうせざるをえなかったという印象が強い。普通に嬉しいけど。

String.join

待望のというか今さらというか、文字列連結用のメソッドが標準搭載された。Apache Commonsを使うから特に困らないんだけど、当たり前にできてほしいことが当たり前にできるようになるのは悪くない。

Java8
// いずれも "A, B, C"
String.join(", ", "A", "B", "C");
String.join(", ", new String[]{"A", "B", "C"});
String.join(", ", Arrays.asList("A", "B", "C"));

Base64

これまた今さらなんだけど、Base64のエンコードとデコードが正式にサポートされた。

Java8
Base64.Encoder encoder = Base64.getEncoder();
String original = username + ":" + password;
String encoded = encoder.encodeToString(original.getBytes(StandardCharsets.UTF_8));

Files.lines

ファイルをストリームとして読み込むAPIも追加された。ファイルをクローズする必要があるので、try-with-resources構文を使う点に注意する。言ってなかったけど、StreamAutoCloseable を実装しているので、こんなことが可能。

Java8
Path path = Paths.get("/path/to/file.txt");
try (Stream<String> lines = Files.lines(path)) {
    lines.forEach(System.out::println);
} catch (IOException e) {
}

まとめ

今回取り上げた以外にも、JavaFX (Swingに代わる新しいGUIツールキット) とか Nashorn (Rhinoに代わる新しいJavaScript実行エンジン) とかもあって、Java 8 は久しぶりに Java 言語を大きく進化させた感じ。

Java 7 のサポートも切れたことだし、ランタイムだけでなくソースレベルもさっさと Java 8 に移行したいですね。