🍺

最近の Java がどんな雰囲気なのかみてみた

2021/12/03に公開

こんにちは! LAPRAS でエンジニアをしています @Chanmoro です!

LAPRAS Advent Calendar 2021 3日目の記事です。

https://qiita.com/advent-calendar/2021/lapras

最近はスクラムチームでのプロダクトオーナーの役割が中心なので業務でコードをほとんど書いてないのですが、たまには何か技術的な記事などアウトプットせねばと思いましたので書きます。

しばらく見ないうちにだいぶ進化していた Java

今年の9月に Java17 がリリースされたというのを見て、最後に Java を触っていたのは4〜5年前で Java7 か 8 あたりでほぼ知識が止まっていたので 「そういえば最近の Java の雰囲気はどんな感じなんだろう」 とふと気になったので軽く調べてみました。

新しめのシンタックスを使ってコードを書いてみる

細かく全ての新機能を調べるほどのパワーはなかったのですが、Java 8 以降に入ったいくつかの新機能を見てみて最近のシンタックスならクローラーっぽい処理が書きやすくなってるかもしれないと思ったので試してみました。
Zenn の Scrap 一覧ページにアクセスして、Scrap のリンクの一覧を取得する処理を書いてみてます。

Scrap 一覧から各 Scrap の情報を取得して以下のような構造のオブジェクトに変換します。

class Scrap {
    String title;
    String relativePath;
    Boolean isOpen;
}

コードはこんな感じになりました。

import org.jsoup.Jsoup;

record Scrap(String title, String relativePath, Boolean isOpen) {
    // Scrap 記事の情報を保持するクラス
}

public class Application {
    public static void main(String[] args) throws Exception {
        // scrap 一覧ページにアクセス
        var document = Jsoup.connect("https://zenn.dev/chanmoro?tab=scraps").get();

        // scrap をパースして Scrap オブジェクトを生成する
        var scrapElements = document.select("div[class^='ScrapRow_container_']");
        var scraps = scrapElements.stream().map((scrapElement -> {
            var scrapLink = scrapElement.selectFirst("a");
            var title = scrapLink.text();
            var scrapPath = scrapLink.attr("href");
            var isOpen = switch (scrapElement.selectFirst("div[class^='ScrapRow_meta_'] *[class^='TinyBadge_badge_']").text()) {
                case "Open" -> true;
                default -> false;
            };
            return new Scrap(title, scrapPath, isOpen);
        }));

        // print
        scraps.forEach(System.out::println);
    }
}

実行するとこんな出力になります。

$ ./gradlew run

> Task :run
Scrap[title=Gradle で空のプロジェクトを作成し gradle run できるようにする, relativePath=/chanmoro/scraps/66dcdf002e089e, isOpen=true]
Scrap[title=git diff がある場合もしくは untracked なファイルがある場合にエラーの終了コードを返すコマンド, relativePath=/chanmoro/scraps/06c972692f98df, isOpen=false]
Scrap[title=Python で selenium を使ったときに ResourceWarning: unclosed のログが出る時の対処, relativePath=/chanmoro/scraps/d7f08a34fb7afb, isOpen=false]

BUILD SUCCESSFUL in 2s
2 actionable tasks: 2 executed

昔の Java しか知らなかった僕からすると、パッと見 「え?あなた誰???」 というくらい全然違う見た目になった気がします。

上記のコードではこれらを使ってみました。

  • var を使った変数宣言
  • switch 式
  • Record クラス (2021/12/3 追記)

var を使った変数宣言

var を使った変数宣言は Java 10 から正式に追加されたようです。
以下のように変数を var を使って宣言すると型推論により型が判定されるので、明示的に型を書く必要がありません。

var document = Jsoup.connect("https://zenn.dev/chanmoro?tab=scraps").get();

var の登場以前は変数の型を明示する必要があったのでこんな感じで書くことになります。
以前はこの例のように変数名を型名と同じにするような場合でクラス名が長いと無駄に記述量が増えてしまいましたが、こういったコードが var によって削減できるのはとても良いですね。

Document document = Jsoup.connect("https://zenn.dev/chanmoro?tab=scraps").get();

switch 式

switch 式は Java 14 より正式に追加されたようです。
以下のように switch 式により値を返すことができるようになりました。

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

また、以前の switch 文では以下のように case のブロックごとに break を書く必要がありましたが、アロー構文 -> により break を省略することができるようになりました。

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...
        break;
    default:
        int temp3 = ...
}

今回のコードの例では、Scrap の各リンクに表示されている Open Closed のステータスを取得して Boolean 値の isOpen に変換する処理のところで switch 式を (ほぼ無理矢理) 使ってみました。

これにより以前は if や switch で分岐して代入するような書き方をしていたものがよりスッキリ書けるようになりました。
クローラーでよく書かれるような処理では細かいところで分岐が必要になることが多いので、こういった書き方ができるようになるのは非常に便利ですね。

Record クラス (2021/12/3 追記)

Record クラスは Java 16 より正式に追加されたようです。
Record クラスは以下のように record と書いて定義します。

record Point(int x, int y) {}

そうすると以下のようなクラスと同等のクラスが定義できます。とても便利!

class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() { return x; }
    int y() { return y; }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

DTO 的なオブジェクトを定義したい時、過去には Lombok を使って記述を省略していたのですが、それと似たようなことが Record クラスで実現できます。

※この記事の公開後に @yamadamn さんが twitter で教えてくださいました!ありがとうございます!!
https://twitter.com/yamadamn/status/1466664664063168513?s=20

感想

今回試してみた新しめのシンタックスはどちらも冗長なコードを減らして記述量を少なくできるものでした。
LAPRAS に入ってからは Python での開発がほとんどなので今となってはすっかり慣れてしまいましたが、最近の Java はよりスクリプト言語っぽく書ける見た目になってきているなと感じました。
特に衝撃だったのは var での変数宣言ですが、Java の開発では IntelliJ や Eclipse などの強力な IDE があるのでそれらを使っている場合はツールでのサポートが得られますし、型推論に頼っても実際にはそんなに困る場面はなさそうだなと思います。

久しぶりに Java を触ってみて、コンパイラが怒ってくれる安心感はとてもある一方で「例外ハンドルしてないと怒られるんだった」とか「stream の中での例外処理大変だな」とかの Java っぽさを思い出せた気がしました。

今回書いたコードの一式はこちらに置いてあります。
https://github.com/Chanmoro/modern-java-sandbox

明日以降も LAPRAS Advent Calendar 2021 の記事をお楽しみに!
https://qiita.com/advent-calendar/2021/lapras

Discussion