今やってるプロジェクトのソースレベルも1.8に上がったことだし、ここらへんでJava 8の新機能をおさらい。
Lambda
Java 8の目玉機能、ラムダ式。まずは無名クラスを使った今までのコールバック。いかにもJavaな長ったらしい記述。
Arrays.sort(array, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareToIgnoreCase(b);
}
});
Java 8のラムダ式を使えばこう。どうせIDEに自動生成させるとはいえ、本当に注目すべき処理だけに集中できるのは嬉しい。
Arrays.sort(array, (a, b) -> a.compareToIgnoreCase(b));
ラムダ式の書式にはいくつかのパターンがある。例えば、引数が1つの場合は引数リストのカッコが不要。逆に本文が1行でない場合は、 ->
の後に {}
でブロックを明示する。
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
// do something 1
// do something 2
}
});
button.addActionListener(event -> {
// do something 1
// do something 2
});
引数がない場合は、次のように空の ()
で引数リストを表す。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// do something
}
});
Thread t = new Thread(() -> {
// do something
});
ラムダ式の裏にある新しい言語仕様は、関数インターフェースというもの。abstractなメソッドが1つだけのインターフェースを関数インターフェースといい、慣習的に @FunctionalInterface
アノーテーションを付ける。
@FunctionalInterface
public interface Runnable {
void run();
}
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
関数インターフェースが要求される場所には、具象クラスを定義する代わりにラムダ式を渡すことができる。これがラムダ式の秘密。
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++のテンプレート触ったことがあると馴染みのある感じの名前。また、プリミティブ型でオートボクシングを避けるために、 IntSupplier
や DoubleFunction
などの特殊化が多数あるのであわせて確認しておくこと。
自分が作るAPIの引数を関数インターフェースにしておけば、そのAPIの利用者はラムダ式を使うことができる。そうすると単に記述量を減らせるだけでなく、遅延評価の恩恵も受けられる。よくあるログ出力の例はこんな感じ。本当にログを出力する必要があるときになって初めてメッセージ本文が生成されることに注意する。
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());
最後に、関数インターフェースには、ラムダ式の代わりに既存のメソッドやコンストラクタの参照を渡すこともできる。
// 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文を使ってこんな感じ。
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
を取得し、メソッドチェーンで処理をつなげていく。
long count = list.stream()
.filter(s -> s.startsWith("C"))
.mapToInt(s -> s.length())
.sum();
この例のように、ストリームを使ったデータ操作は次の3つのパートに分解できる。
- ストリームの作成
stream()
- コレクションや配列から
Stream
を取得する
- 中間操作 (intermediate operator)
-
filter()
,mapToInt()
-
Stream
を絞り込んだり加工したりした新たなStream
を取得する
-
- 終端操作 (terminal operator)
sum()
-
Stream
から最終的な結果を取得する。 終端操作のAPIを実行して初めて 中間操作のAPIも評価される
1つ1つのAPIを細かく見ていっても仕方ないので、ほかの例をいくつか挙げて説明に代える。
String[] words = Stream.of("C", "C++", "Java", "Scala", "Ruby")
.map(s -> s.toUpperCase())
.sorted()
.toArray(String[]::new);
//=> [C, C++, JAVA, RUBY, SCALA]
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}
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()
に置き換えるだけで並列化される。
long count = list.parallelStream()
.filter(s -> s.startsWith("C"))
.mapToInt(s -> s.length())
.sum();
注意点としては、小さなデータセットで並列化してもオーバーヘッドが増えるだけで逆に遅くなる点、データ操作の種類によっては並列化の恩恵を受けられない点、などが挙げられる。実際に計測して判断するのがよいだろう。
Optional
個人的に一番嬉しい機能。値がないかもしれない、という状態を表現する Optional<T>
が導入され、不必要に null
を返さなくて良くなったのと、実装者の意図を明示しやすくなった。しいて言えば、値の存在チェックを言語的に強制する仕組みがあるともっと良いのだけど。
String lang = getConfigValue("lang");
if (lang == null) {
lang = "en"
}
Optional<String> value = getConfigValue("lang");
String lang = value.orElse("en");
Optional<String> value = getConfigValue("lang");
String lang = value.orElseGet(() -> {
// more complicated logic
return "en";
});
Optional.isPresent()
を使って呼び出し側で値の存在チェックをすることもできるが、従来のnullチェックと同じになってしまうのでJava 8っぽくない。
String lang = "en";
Optional<String> value = getConfigValue("lang");
if (value.isPresent()) {
lang = value.get();
}
なお、Optional
を返す側では、Optional.of()
や Optional.empty()
を使って生成する。
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
を使ってちゃんとタイムゾーンを意識したコードを書くのが無難だと思う。
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 始まりじゃないよ!
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"
面倒になってきたから最後に夏時間の例だけ載せて終わりにする。
// 太平洋標準時の夏時間開始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
クラスのような専用ユーティリティクラスを別に用意しなくても、インターフェース自身にユーティリティメソッドを定義できる。
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
利便性のためというよりは、既存コードを壊さずにStream APIを導入するにはそうせざるをえなかったという印象が強い。普通に嬉しいけど。
String.join
待望のというか今さらというか、文字列連結用のメソッドが標準搭載された。Apache Commonsを使うから特に困らないんだけど、当たり前にできてほしいことが当たり前にできるようになるのは悪くない。
// いずれも "A, B, C"
String.join(", ", "A", "B", "C");
String.join(", ", new String[]{"A", "B", "C"});
String.join(", ", Arrays.asList("A", "B", "C"));
Base64
これまた今さらなんだけど、Base64のエンコードとデコードが正式にサポートされた。
Base64.Encoder encoder = Base64.getEncoder();
String original = username + ":" + password;
String encoded = encoder.encodeToString(original.getBytes(StandardCharsets.UTF_8));
Files.lines
ファイルをストリームとして読み込むAPIも追加された。ファイルをクローズする必要があるので、try-with-resources構文を使う点に注意する。言ってなかったけど、Stream
は AutoCloseable
を実装しているので、こんなことが可能。
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 に移行したいですね。