MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

Java8→Java19でいいなと思った機能

はじめに

こんにちは。 マイクロアドでWebエンジニアをしている木田です。 今回は普段Java8を使用しているエンジニアがJava19を触ってみて便利だなと思った点をいくつかご紹介しようとおもいます。

マイクロアドではWebアプリ開発のサーバサイド言語としては以下を使用しています。

  • Java
  • Kotlin

Kotlinについては比較的新しい言語ということもあり、Java8にはない便利な組込APIがいくつも存在しています。
今までは、「Kotlinのほうがシンプルに書けていいな〜」と思ってましたが、
Java19を触ってみて、「あれ、Javaも負けてないぞ!」と思ったので両者を比較しながら紹介します。

※Java19での新機能ではなく、Java19で使用できるJava8にはなかった機能(Java9以降で追加された機能)の紹介になります。

目次

その1 Record Class 【JDK14〜】

概要

  • データを保持するだけのクラス(Java Beans)を簡単に実装できます
  • Kotlinでいうdata classに当たります
  • Kotlinのdata classと違いイミュータブルになります(=setterはない)

使用例

User.java

public record User (
    Integer userId,
    String userName
){}

Main.java

public class Main {
    public static void main(String[] args) {
        User taro = new User(1, "taro");
        taro.userId() // 1
        taro.userName() // "taro"
    }
}

説明

  • 以下は自動生成されます
    • アクセサ(getter)
    • コンストラクタ
    • equals
    • hashCode
  • インスタンスの作成方法はこれまでと同じです
  • getterはgetId(), getName()ではなくid(), name()のように使用します
  • ブロックの中で定義するフィールドは必ずstaticにする必要があります
  • Recordクラスの継承はできません
  • 以下のことは可能です
    • インターフェースの実装
    • ジェネリクスの使用
    • アノテーションの使用

参考

https://docs.oracle.com/en/java/javase/19/language/records.html

その2 テキストブロック 【JDK13〜】

概要

  • 複数行にまたがる文章を簡単に読みやすく記述できます
  • Kotlinにも同じ機能があり、書き方も同じです

使用例

String query = """
    SELECT name, age
    FROM EMP
    WHERE name = 'John'
      AND age > 20""";
System.out.println(query); // 下記のようにインデントを保ったまま出力される
/**
 * SELECT name, age
 * FROM EMP
 * WHERE name = 'John'
 *   AND age > 20
 */

String abc = """
    aaa\
    bbb\
    ccc
    """;
    System.out.println(abc); // aaabbbccc

説明

  • ほとんどの記号のエスケープが不要になります
  • エスケープシーケンスは使用可能です
  • インデントは以下のいずれかの位置が基準となります
    • 一番左側にある非空白文字
    • 一番左側にある終了デリミタ(""")
  • 開始のデリミタ(""")のあとは必ず改行が必要です
  • 改行コードはLFです
  • 末尾の空白は除去されます
  • 「\」を入れることであえて改行させないこともできます

参考

https://blogs.oracle.com/otnjp/post/text-blocks-come-to-java-ja

その3 switch式 【JDK12〜】

概要

  • switchを式で書けます
    • 文と式の大きな違いは、文は結果を返さないのに対し、式は結果を返します。つまりswitchの結果を変数に代入できます。
  • Kotlinでいうwhen式に近いです
  • 「->」で書くことができます

使用例

enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; }
Day day = Day.WEDNESDAY;

// case A -> パターン
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> {
        ... もろもろの処理 ...
        yield 9;
    }
};
System.out.println(numLetters); // 9

// case A: yield ◯◯ パターン
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY : yield 6;
    case TUESDAY                : yield 7;
    case THURSDAY, SATURDAY     : yield 8;
    case WEDNESDAY              :
        ... もろもろの処理 ...
        yield 9;
};
System.out.println(numLetters); // 9

説明

  • 評価結果を返すようになりました
  • case A, B, C, ...と複数のパターンを書くことができます
  • 2通りの書き方ができます
    • case A -> hoge;と書くことができます
      • 「:」と「break」が不要!
    • case A: yield ◯◯
  • switchyieldを使用して値を返すことができます

参考

https://docs.oracle.com/en/java/javase/13/language/switch-expressions.html

その4 ローカル変数の型推論 【JDK10〜】

概要

  • ローカル変数としてのみ利用できます
  • nullは入れることができません
  • Kotlinには「val(再代入不可)」、「var(再代入可能)」があります

使用例

var url = new URL("http://www.oracle.com/"); 
var conn = url.openConnection(); 
var reader = new BufferedReader(
    new InputStreamReader(conn.getInputStream()));

/**
 * 今までの書き方
 * URL url = new URL("http://www.oracle.com/");
 * URLConnection conn = url.openConnection();
 * Reader reader = new BufferedReader(
 *   new InputStreamReader(conn.getInputStream()));
 */

// for文で使用することもできる
var list = List.of(1,2,3);
for (var num : list) {
    System.out.println(num);
}

// try-with-resourcesで使用することもできる
try (var input = new FileInputStream("validation.txt")) {...}

説明

  • varを使用することでコードをスッキリさせることができます
  • しかし、型が明記されていたほうが見やすい場合も多々あるので、考えて使用する必要があります

参考

https://docs.oracle.com/javase/jp/13/language/local-variable-type-inference.html

その5 StreamのtoList() 【JDK16〜】

概要

  • streamの終端処理でList型に変換するとき、今までは.collect(Collectors.toList())を使用していたが、.toList()で済むようになりました
  • ここでは紹介しませんが、Kotlinのコレクションは組込みAPIがすごい豊富です

使用例

var list = List.of(1,2,3,4,5,6,7,8,9,10);
var evens = list.stream()
    .filter(e -> e % 2 == 0)
    .toList();
System.out.println(evens); // [2, 4, 6, 8, 10]

説明

  • .collect(Collectors.toList())は長くて直感的ではないと思っていたのですが、今後は短くシンプルな書き方ができます
  • Collectors.toList()をstatic importすれば少し短く書くことはできますが、それよりも.toList()のほうが短くて直感的です

参考

https://docs.oracle.com/javase/jp/16/docs/api/java.base/java/util/stream/Stream.html

その6 jshell 【JDK9〜】

概要

  • JavaのREPL環境です
  • Kotlinでも同様の機能があります

使用例

% jshell
|  JShellへようこそ -- バージョン19.0.1
|  概要については、次を入力してください: /help intro

jshell> 1 + 1
$1 ==> 2

jshell> "test".toUpperCase()
$2 ==> "TEST"

jshell> import java.util.List;

jshell> List.    # ここでtabキーを押しています
class     copyOf(   of(       
jshell> List.of(1,2,3)
$4 ==> [1, 2, 3]

jshell>

説明

  • Javaをちょっと試したいというときに便利です
  • importもできます
  • ピリオドを入力した後にtabキーを押すと、メソッドの候補が表示されます
  • 終了するときは/exitと入力する

参考

https://docs.oracle.com/javase/jp/9/jshell/introduction-jshell.htm

その7 sealedクラス 【JDK15〜】

概要

  • 継承先・インターフェースの実装先を限定できます
  • Kotlinにもあります

使用例

  • sealedクラス Shape.java
public sealed class Shape
    permits Circle, Square, Rectangle {

    }
  • sealedクラスのサブクラス
    Circle.java
public final class Circle extends Shape {
    public float radius;
}

Square.java

public non-sealed class Square extends Shape {
    public double side;
}

Rectangle.java

public sealed class Rectangle extends Shape {
    public double length, width;
}

説明

  • サブクラスは同じファイルにも別のファイルにも書くことができます
    • 同じファイルに書く場合、permits句を省略できます
  • サブクラスには以下の制約があります
    • コンパイル時にsealedクラスからアクセスできる必要があります
    • sealedクラスを直接継承する必要があります
    • サブクラスはfinal, sealed, non-sealedのいずれかの修飾子を付ける必要があります
    • 同じモジュールまたは同じパッケージ配下にある必要があります
  • インターフェースにsealedをつけることもできます

 参考

https://docs.oracle.com/javase/jp/15/language/sealed-classes-and-interfaces.html

その8 instanceofのパターンマッチング 【JDK14〜】

概要

  • instanceofで検証とキャストを同時にできる
  • Kotlinも「is」で同じことができる

使用例

Object obj = "abc";
if (obj instanceof String str) {
    System.out.println(str.toUpperCase()); // ABC
}
if (obj instanceof String str && str.length() == 3) {
    System.out.println(str.toUpperCase() + "は3文字です"); // ABCは3文字です
}

説明

  • 今まではinstanceofで検証した後、if文の中で明示的にキャストする必要があったが、それをまとめて書くことができる
  • instanceofの右側の変数(バインディング変数と言います)のスコープは、instanceofの結果がtrueとなる場所です

参考

https://docs.oracle.com/javase/jp/14/language/pattern-matching-instanceof-operator.html

その9 単一ソース・ファイルの起動  【JDK11〜】

概要

  • 単一ファイルのみの実行の場合、javacコマンドを実行する必要はありません
  • mainメソッドだけを持つMain.javaを作ってちょっとした動作確認をするときには、java Main.javaとするだけでコードを実行できます
  • 2023年5月現在、最新バージョンのKotlin1.8.21にはこのような機能はありません

参考

https://docs.oracle.com/javase/jp/13/docs/specs/man/java.html

その10 仮想スレッド  【JDK19〜】

概要

  • JVMの中でスレッドを作成・管理することで従来のスレッドよりスループットを向上させることができます

    使用例

    ※下記サイトのサンプルを拝借して動かしてみました https://gihyo.jp/article/2022/08/tfc003-java19

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        // 仮想スレッドを使用
        Thread.Builder builder = Thread.ofVirtual();
        long startTime = System.currentTimeMillis();

        main.createThreadPerTask(builder);

        long endTime = System.currentTimeMillis();
        System.out.println((endTime - startTime) / 1000 + "秒かかりました");
    }

    /**
     * 100万個スレッドを作成して並列処理
     * @param builder
     */
    public void createThreadPerTask(Thread.Builder builder) {
        try (var exec = Executors.newThreadPerTaskExecutor(builder.factory())) {
            IntStream.range(0, 1_000_000).forEach(i -> {
                exec.submit(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        throw new IllegalStateException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "タスク終了 for " + i);
                });
            });
        }
    }
}

説明

  • JVMの中でスレッドを作成・管理することで従来のスレッドよりスループットが向上します
  • OS側のスレッドを効率よく使用できるのが仮想スレッドの特徴です
  • 今まではJava側のスレッドとOS側のスレッドを1対1でつないでいたが、仮想スレッドは複数のJava側のスレッドが1つのOS側のスレッドを共有できます
  • 待ちが多くスレッドの切り替えが多い処理に有効

参考

https://blogs.oracle.com/oracle4engineer/post/java-loom-virtual-threads-platform-threads-ja

おわりに

Java20が2023年3月にリリースされました。 LTS版のJava21は2023年9月にリリース予定です。 今後のJavaのさらなる発展に期待です。