Java 8から17までの機能を素早く見る

はじめに

このページでは、Java 7以降に追加された新機能を紹介し、2020年秋にリリースされたJava 15まで、Javaの各バージョンでの大きな改善点を見ていく。Javaは現在、ラムダ式、関数型プログラミング、varによる型推論、シンプルな生成方法による不変コレクション、複数行文字列をしっかりサポートしている。また、データクラス(record)やsealedクラスなど、新しく興味深い実験的機能もある。そして、時間効率よく使えるJava REPLについても説明する。

Java 8 (LTS)

関数型プログラミング

Java 8では、関数型プログラミングとラムダ式が言語機能として追加された。関数型プログラミングの2つの中心的なパラダイムは、不変の値と関数の重要性の向上である。データ変換処理はパイプラインを通過する。各段階で入力を受け取り、新しい出力にマッピングする。関数型プログラミングは、JavaのStreamOptional(null-safe monads)で利用できる。

ラムダ式

public static void main(String[] args) {

    Runnable runner = () -> { System.out.println("Hello Lambda!"); };
    runner.run(); //Hello Lambda!
}

Streams

一般的なコンピュータープログラムでは、値のリストを使い、それぞれの値を頻繁に変換することがよくある。Java 8以前は変換にforループを使う必要があったが、現在は次のようにStreamを使えるようになった。

Stream.of("hello", "great")
    .map(s -> s + " world")
    .forEach(System.out::println);
> hello world
> great world

map関数は、Streamのすべての要素に適用されるラムダを入力として受け取る。

Streamは変換を通じてListSetMapに対して動作する。Streamによってコード内のループをほとんどなくすことができる。

Optional

Javaでよく発生する問題の1つがNull Pointer Exceptionだった。そこでJavaはOptionalを導入した。これはNullの場合とNullではない場合の参照を包むモナド(monad)である。Optionalに更新を適用するには、関数の形で実行できる。

Optional.of(new Random().nextInt(10))
    .filter(i -> i % 2 == 0)
    .map(i -> "number is even: " + i)
    .ifPresent(System.out::println);
number is even: 6

上の例では、乱数を生成し、それをOptionalオブジェクトでラップして、偶数の場合にだけ値を出力する。

Date and Time API

LocalDateLocalTimeLocalDateTimeDateTimeFormatterZonedDateTimeなど、日付に関連する便利なクラスが多く追加された。

Java 9

JShell

ついにJavaにもREPLであるJShell機能がJava 9から導入された。このJShellを使うと、Javaの完全なクラスを書いてコンパイルしなくても、プレビューのようにテストできる。つまり、1つずつコマンドを実行し、すぐに結果を確認できる。簡単な例は次のとおりである。

$ <JDK>/bin/jshell
jshell> System.out.println("hello world")
hello world

JavaScriptやPythonのようなインタープリター言語に慣れた人は長い間REPLを利用してきたが、Javaにはこれまでこの機能がなかった。JShellでは、変数だけでなく、複数行の関数、クラス、ループの実行など、より複雑なエンティティも定義できる。また、自動補完をサポートしており、特定のJavaクラスが提供する正確なメソッドを知りたい場合に役立つ。

不変(Immutable)コレクションのファクトリメソッド

Listの簡単な初期化はJavaでは長い間欠けていたが、ついに追加された。以前は次のようにする必要があった。

jshell> List<Integer> list = Arrays.asList(1, 2, 3, 4)
list ==> [1, 2, 3, 4]

これが次のように単純化された。

jshell> List<Integer> list = List.of(1, 2, 3, 4)
b ==> [1, 2, 3, 4]

この便利なof(...)メソッドは、ListSetMapに含まれている。1行で簡単な不変オブジェクトを作成できる。

Java 10

varによる型推論

Java 10では、変数の型を省略できる新しいvarキーワードが追加された。

jshell> var x = new HashSet<String>()
x ==> []

jshell> x.add("apple")
$1 ==> true

上の例では、コンパイラーによってxの型はHashSetであると推測できる。

この機能はボイラープレート(boilerplate)コードを減らし、可読性を向上させるのに役立つ。ただし、いくつかの制限がある。varはメソッド本体の中でのみ使用でき、コンパイラーはコンパイル時に型を推測するため、すべては依然として静的に型付けされる。

ボイラープレートとは? コンピュータープログラミングでボイラープレート、またはボイラープレートコードと呼ぶものは、最小限の変更で複数の場所で再利用され、似た形で繰り返し現れるコードを指す。

Java 11 (LTS)

単一ソースファイルの実行

以前は、1つのファイルで簡単なJavaプログラムを書く場合、まずそのファイルをjavacでコンパイルし、次にjavaで実行する必要があった。Java 11では、1つのコマンドでこの2つの段階を実行できる。

まず、1つのソースファイルMain.javaを作成する。

public class Main {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

これで、コンパイルと実行を1つの段階でまとめて行えるようになった。

$ java ./Main.java
hello world

簡単なスタータープログラムや、1つのJavaクラスだけをテストしたい場合に、この機能によって単一ソースファイルを起動する作業がより簡単になった。

Java 12

Switch式

Java 12でSwitch式が登場した。ここでは、この式が以前のswitch文とどのように違うのかを見ていく。

以前のswitch文はプログラムの流れを定義する。

jshell> var i = 3
jshell> String s;
jshell> switch(i) {
    ...>    case 1: s = "one"; break;
    ...>    case 2: s = "two"; break;
    ...>    case 3: s = "three"; break;
    ...>    default: s = "unknown number";
    ...> }
jshell> s
s ==> "three"

対照的に、新しいSwitch式は値を返す。

jshell> var i = 3;
jshell> var x = switch(i) {
    ...>    case 1 -> "one";
    ...>    case 2 -> "two";
    ...>    case 3 -> "three";
    ...>    default -> "unknown number";
    ...> };
x ==> "three"

この新しいSwitch式が一種のマッピング関数であることに注目してほしい。1つの入力(ここではi)と1つの出力(ここではx)がある。この機能は実際にはパターンマッチング機能であり、Javaを関数型プログラミングの原則と互換性のあるものにするのに役立つ。同様のswitch文は、以前からScalaでも利用できた。

注意すべき点がいくつかある。

  • セミコロン(;)の代わりに矢印->を使用する。
  • breakは必要ない。
  • 考えられるすべてのcaseが考慮されていれば、defaultは省略できる。
  • Java 12でこの機能を使うには、--enable-preview -source 12を使用する。

Java 13

複数行文字列

JSONやXMLのような長い複数行文字列を定義した経験はあるだろうか。これまでは、すべてを1行にして改行文字\nを入れる必要があり、文字列が読みづらくなっていた。この不便さを解決するため、Java 13では複数行文字列機能が導入された。

public class Main {
    public static void main(String [] args) {
        var s = """
            {
                "recipe": "watermelon smoothie",
                "duration": "10 mins",
                "items": ["watermelon", "lemon", "parsley"]
            }""";
        System.out.println(s);
    }
}

単一ファイル起動でMainメソッドを実行してみよう。

java --enable-preview --source 13 Main.java
{
    "recipe": "watermelon smoothie",
    "duration": "10 mins",
    "items": ["watermelon", "lemon", "parsley"]
}

結果の文字列は複数行になっており、引用符""はそのまま保持され、タブ\tも保持される。

Java 14

データクラスrecord

このページの新機能の中で、最も興味深いのはおそらくこれだろう。ついにJavaにデータクラスが登場した。このクラスはrecordキーワードで宣言され、自動Getter、コンストラクタ、equalsメソッドなどを持つ。つまり、大量のボイラープレートコードを削除できるようになった。

jshell> record Employee (String name, int age, String department) {}
| created record Employee

jshell> var x = new Employee("Anne", 25, "Legal");
x ==> Employee[name=Anne, age=25, department=Legal]

jshell> x.name()
$2 ==> "Anne"

Scalaには似たケースクラスがあり、Kotlinにもデータクラスがある。Javaでは、これまで多くの開発者がLombokを使用していたが、Java 14のrecord機能はその影響を受け、多くの同様の機能を提供している。詳細はBaeldungを参考にしてほしい。

キャストなしのinstanceof

instanceofキーワードは、Javaの以前のバージョンにもすでに存在していた。

Object obj = new String("hello");
if (obj instanceof String) {
    System.out.println("String length: " + ((String)obj).length());
}

ここで惜しい点は、まずobj変数がString型であることを確認し、その後もう一度キャストして長さを返す部分である。

Java 14では、instanceofチェック後にコンパイラーが自動で型を推論し、そのまま使えるようになった。

Object obj = new String("hello");
if (obj instanceof String mystr) {
    System.out.println("String length: " + mystr.length());
}

Java 15

Sealedクラス

sealedキーワードを使用すると、特定のクラスまたはインターフェースを拡張できるクラスを制限できる。

public sealed interface Fruit permits Apple, Pear {
    String getName();
}

public final class Apple implements Fruit {
    public String getName() { return "Apple"; }
}

public final class Pear implements Fruit {
    public String getName() { return "Pear"; }
}

このソースは私たちにどのような助けを与えるのだろうか。Fruitがいくつあるのか分かるようになった。これは完全にサポートされたパターンマッチングに向けた重要な段階である。列挙型のようにクラスを扱えるようになる。このsealed機能は、前述した新しいSwitch式とよく合う。

Java 16

  • 2021年3月16日発表。
  • Vector API: 自動並列処理をサポートする自動Vector APIが追加された。
  • OpenJDKのソースをGitHubで見られるようになった。
  • Java 8で削除されたPermGenの代わりにMetaspace方式をサポートする。
    • 使用されていないHotSpot class-metadataメモリをOSへ迅速に返却し、コストを削減する。

Java 17 (LTS)

  • 2021年9月15日リリース。

番外: Java 8以降のアップデート条件

このページの最後のテーマはライセンスである。OracleがJava 8(無料商用版)のアップデートを停止したことを聞いたことがあるだろう。選択肢は次のとおりである。

  • 新しいOracle JDKバージョンを使用する。Oracleは各リリース後6か月間のみ無料セキュリティアップデートを提供する。
  • 自己責任で以前のJDKバージョンを使用する。
  • 以前のOpenJDK Javaバージョンを使用する。これはオープンソースコミュニティとパートナーを通じて、引き続きセキュリティアップデートを受ける。
  • プレミアムサポートのためにOracleへ支払う。たとえばJava 8は2030年までサポートされる。

次はJDKごとの一時的なOracleサポート期間である。

JDK リリース日 プレミアサポート期限 延長サポート
7 2011.07 2019.07 2020.07
8 2014.03 2019.07 2020.07
9 2017.09 2018.03 -
10 2018.07 2018.09 -
11(LTS) 2018.09 2023.09 2026.09
12 2019.03 2019.09 -
13 2019.09 2020.03 -
14 2020.03 2020.09 -
15 2020.09 2021.03 -
JDK別Oracleサポート予定

Oracleの新しいライセンスモデルは、新しいリリースサイクルの影響を受ける。Oracleは6か月ごとに新しいJavaバージョンを発表する。これにより、Oracleは速いペースでJavaを改善し、実験的機能を通じてより迅速にフィードバックを受け取り、Scala、Kotlin、Pythonなどの最新言語に追いつく助けになる。

ライセンスの詳細については、この文書を参考にする。

おわりに

Javaはこの6年間で大きく進化し、その間に実際には8つの新しいJavaリリースがあった。これらの優れた新機能は、Javaを他のJVMベースの競合(ScalaやKotlin)と比べても競争力のある選択肢にするのに役立つ。

参考