日々常々

ふつうのプログラマがあたりまえにしたいこと。

Javaの新しめの機能を知ったかぶれるようになる話 #kanjava

2025-03-24に関ジャバ'25 3月度を開催しました。

kanjava.connpass.com

Java24リリース記念イベントの位置付けではありますが、Java24自体の話はそんなしてないというものになります。

関ジャバ、2025年は毎月やろうとおもっています。よろしくおねがいします。

資料

speakerdeck.com

言い訳

文字もページ数も多いですが、資料に書いていないことを話すセッション。 話したいこと全部入れると早口のリハでも2時間という詰めすぎなものですが、実測は70分でした。 月曜の夜に1時間オーバーでお聞きいただくという……お付き合いありがとうございました。

いやほら、セッション数とか的に時間あるしいいかな?って。ほら、イベントとしては概ね時間通りに終わりましたし?

機能まとめについて

資料の中でも触れているような有用なまとめ情報はすでにあります。 だからと言って価値のないものかと、言うとそんなことないと思うんですよね。

もちろん「自分なりの力点はここ」みたいなのを加えたいところではありますが、 言語化できなくても勝手に何かしらの補正はかかるものなので力む必要はありません。 自分なりの言葉で言ってみるだけでもいいと思うんです。

と言うようなことを話したLTがこちら。

speakerdeck.com

これはセッションのだけど、ブログとかどんなアウトプットでも同じ。

コピペでもしなければ「自分なりの見方といったモデリング」が必ず行われます。

しっかりした先人の情報があると「情報量が減った劣化になっている」と感じるかもしれませんが、情報の取捨選択もモデリングです。 色々雑音はあるかもしれませんが、私はその選択には価値があると思うし、恐れないで見せてくれると嬉しいなって思ってます。

追記: 2025-03-26T00:15

セッション後に「どうやって調べ方とか知ったのか?」と言う質問を貰って、ふわっと「偶然……?」みたいな回答をした気がするんですが、改めて回答するなら「いまこのセッションで知りましたよね」かと思いました。

私自身、過去の関ジャバとかJJUGとかのセッションでJavaソースコードの話やJBSなどの話をしているのを聞いて「そんなものがあるんだ」と知って、実際見てみたところがきっかけだったと思います。

コミュニティのイベントとかで知ったこととかその延長線にあるものをコミュニティに還元して、それがまた他の人にも伝播していくーみたいな。そう言うコミュニティが続くといいなぁ、なんて。

今年の何%が経過したかを求めよう

この手のコードはAIがシャシャッと書いてくれる時代。 あ、冒頭はAIのコード貼ってるけど、本題はそこじゃないです。序盤は適当に流してください。本文書こうと思ったら「それAIでよくね?」と脳内で囁かれたので、添えただけ。

いくつか生成

ChatGPTさんに雑に投げてみます。「今年の何%が経過したかを求めるコードをJavaで書いてください」と。

こちらが Java で今年が何%経過したかを求めるコードです!

// 今日の日付を取得
LocalDate today = LocalDate.now();

// 今年の最初の日と最後の日を取得
LocalDate startOfYear = LocalDate.of(today.getYear(), 1, 1);
LocalDate endOfYear = LocalDate.of(today.getYear(), 12, 31);

// 年の経過日数と年間の日数を計算
long daysPassed = ChronoUnit.DAYS.between(startOfYear, today);
long totalDays = ChronoUnit.DAYS.between(startOfYear, endOfYear) + 1;

// 経過率を計算(小数第2位まで表示)
double percentage = (daysPassed * 100.0) / totalDays;

// 出力
System.out.printf("今年の%.2f%%が経過しました。\n", percentage);

まぁ動く。

他のモデル

JetBrains AIではいくつか使えるので、GPT-4o、Claud 3.5 Sonnet、Gemini Pro 1.5に投げてみます。 モデルを変えると口調も変わるのは面白いです。

GPT-4oはGhatGPTとほぼ同じだったので省略。

Claud 3.5 Sonnet

Javaで今年の経過率を計算するコードを提供いたします。うるう年も考慮した、より正確なバージョンを作成しました:

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class YearProgress {

    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        int year = today.getYear();
        LocalDate start = LocalDate.of(year, 1, 1);
        LocalDate end = LocalDate.of(year, 12, 31);
        long totalDays = ChronoUnit.DAYS.between(start, end) + 1; // うるう年対応
        long daysPassed = ChronoUnit.DAYS.between(start, today) + 1;
        double percentage = (double) daysPassed / totalDays * 100;

        System.out.printf("今年の経過パーセンテージ: %.2f%%%n", percentage);
    }
}

その +1 ってうるう年対応なの……?

Gemini Pro 1.5

かしこまりました。Javaで今年の経過パーセンテージを計算するコードを、シンプルさと正確さを両立させて記述しました。

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;

public class YearProgress {
    public static void main(String[] args) {
        // 現在の日時を取得(ミリ秒まで)
        LocalDateTime now = LocalDateTime.now();
        
        // 今年の開始時点
        LocalDateTime startOfYear = LocalDateTime.of(now.getYear(), 1, 1, 0, 0, 0);
        
        // 今年の終了時点
        LocalDateTime endOfYear = LocalDateTime.of(now.getYear(), 12, 31, 23, 59, 59, 999999999);
        
        // 今年の総時間(ナノ秒)
        double totalNanos = ChronoUnit.NANOS.between(startOfYear, endOfYear);
        
        // 現在までの経過時間(ナノ秒)
        double elapsedNanos = ChronoUnit.NANOS.between(startOfYear, now);
        
        // パーセンテージを計算
        double percentage = (elapsedNanos / totalNanos) * 100;
        
        // 結果を表示(小数点第6位まで)
        System.out.printf("今年は %.6f%% 経過しました。%n", percentage);
        
        // より詳細な情報を表示
        System.out.println("現在時刻: " + now.format(java.time.format.DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH時mm分ss秒")));
        System.out.println("年初からの経過時間: " + String.format("%,d", elapsedNanos) + " ナノ秒");
        System.out.println("今年の総時間: " + String.format("%,d", (long)totalNanos) + " ナノ秒");
    }
}

ナノ秒ときた。 日数とは指定してないからまぁうんって感じではあるが、うん。

irof

自分をこの枠に入れるなと。 聞く前にjshellで書いてました。

(LocalDate.now().getDayOfYear() * 100) / LocalDate.now().with(TemporalAdjusters.lastDayOfYear()).getDayOfYear()

このブログ書こうとしたのがふと「今年何日経ったっけ?」で。

このポストをするために "%d/%d".formatted(LocalDate.now().getDayOfYear(), LocalDate.now().with(TemporalAdjusters.lastDayOfYear()).getDayOfYear()) を書いてから「これくらいのはもう人間が書くものじゃないんだよなぁ」と思いながら冒頭に行った感じです。

本文

生成AIの生成したものを貼って終わりにするのも一興なのかもだけど、見ながらコメント書いたら全然違うのになるのよね。過去のブログとか入力して「私の書きそうなこと書いて」ってやったらできるのかもしれないけど、趣味だから書かせてほしいです。そのうちこう言うの書いても「AIでいいじゃん」って言われそう。ままならないね。

TemporalAdjuster

TemporalAdjuster さんとか with メソッドとか。あまり見ないけど、知らないと使わないは違うから知って使えるようになっておくと何かと便利です。

とはいえ1月1日や12月31日のために TemporalAdjusters を使う必要はないと思います。 LocalDate.of(2025, 1, 1) の方が読みやすいし。でも LocalDate.of(today.getYear(), 1, 1) は読みやすいと思わない。これやるなら today.with(MonthDay.of(1, 1)) じゃない?まぁ MonthDay も存在知られてなかったりするからやらなくていいけど、引数の対称性が取れていないのはもにょります。コードレビューとかでも言わない程度の個人の範囲だけど。 MonthDay なしなら withMonth & withDayOfMonth とかある。

並べてみる。

LocalDate.parse("2025-01-01");
LocalDate.of(2025, 1, 1);
LocalDate.of(today.getYear(), 1, 1);
today.with(MonthDay.of(1, 1));
today.withMonth(1).withDayOfMonth(1);
today.with(firstDayOfYear());

正直「どうでもいいこと」だけど、自分ならどれを選ぶかとか、その理由とかは自分の中で持っておくことをお勧めしたいです。それをコーディング規約にするとか、コードレビューの観点にないのに指摘するとかは個人的には微妙領域。もちろん「今年」であることが重要でそのコードが繰り返し実行されるものなら 2025 のハードコーディングのだけはNGです。なお「どうでもいいこと」に鉤括弧をつけているのは 緊急時の規律 - 日々常々 とか 表出した事象を叩き潰してはいけない - 日々常々 とかと同様の私の中の特殊コンテキストの言葉だから。そのうち書くかもしれません。

ちなみに最後の firstDayOfYear()TemporalAdjusters さんです。 static-importを前提につくられてるAPIなのでこうですが、しなかったら today.with(TemporalAdjusters.firstDayOfYear()) と文字数も最長になり読みにくくもあるダサいAPIになります。

あ、2月の月末日には TemporalAdjusters.lastDayOfMonth() が使えるので、月末日が必要な時はぜひ思い出してあげてください。 3月1日から minusDays(1) でもいいけどさ。ダサいじゃん。

getDayOfXxx

getDayOfYear() は「その年の日数」です。月の満ち欠けを無視して年を基準に生きる人なら「今日は何日だっけ?」には「X月X日」ではなく「X日」と答えるのが当然で、そういう人が使う日付取得メソッドがこれです。そんな人見たことないけど。

LocalDate には getDayOf から始まるメソッドが3つあります。JSLAPI仕様を見ても良いのですが、jshellさんで getDayOf まで書いてtabを押したら出てきます。素敵機能。

たぶん多くの人は「 LocalDate から日付を取得したいだけなのになんで getDay() じゃなく getDayOfMonth() なんてしなきゃいけないんだ」と面倒に思っていたりするのではないでしょうか。私は面倒に思ってます。だいたいの文脈において月の満ち欠けを前提にしていきているのが地球人類ですから、 getDay() と言う素直なメソッドで「今日は何日?」に応えてほしいと感じるでしょう。ちなみに今日は8日です。

一週間が重要な業務システムだと getDayOfWeek() は地味に役立ちます。他の2メソッドは int 返ってくるのに、この子は enum DayOfWeek を返してくるところとか我が道を行っていてオシャレですよね。 getDayOfWeek() と似たものに getMonth() があって、この子も enum Month が返ってきます。でこっちには int を返す getMonthValue() があったりします。 getDayOfWeekValue() はない。要らないけど。欲しくなったら DayOfWeekgetValue() したげればいいです。なんかで使ったことあるけど、なんだっけな。忘れました。

今年の経過日数 / 今年の日数

LocalDate.now().getDayOfYear() / LocalDate.now().with(TemporalAdjusters.lastDayOfYear()).getDayOfYear()

これやると 0 になるんですよね。

で「あー小数点だから double 使わなきゃねー」とかで出てくるんだけど、業務コードで double を安全に使ってその安全な状態を維持し続けるのは至難の業。 double は小数点数を扱いますが、固定小数点数でなく浮動小数点数で、そのことを分からないなら使うべきじゃないし、わかっているならそもそもほとんどの場合で使わないと思います。

なので今回みたいな単発書き捨てスクリプトならいいかもだけど、業務では dobule とかは存在しなかったものくらいに扱って、常に BigDecimal を使って丸モードに意識を向けるべきだと思ってます。これは先にあげたような人に押し付けないものじゃなく「規約にすべき」くらい強く思っていることです。私がコーディング規約に関わったところでは入ってるんじゃないかな。コードレビューでも出てきたら警戒レベル上げるし。

まとめ

こういう、役に立つようなたたないような話が技術力に繋がると思っています。裏付け考えてみたり、調べてみたり、動かしたり。 こう言うのを楽しめる人たちと会話できると楽しいって思います。楽しめるべきとなんて思ってないので、楽しめない人がいてもいいと思います。押し付ける気はさらさらありません。棲み分けでいいと思います。

とは言え今後はAIにお任せになっていくので、このまんまの技術は要らないものになっていくんだろうなぁとは思っています。 AIネイティブはAIネイティブで同系統の必要な要素を抽出した別の技術を身につけていくことになるだろうから、過渡期を超えた後の人たちの心配は要らないかなと思ってます。 この過渡期の真っ最中はどうしたらいいんだろうなぁとは思っていますが、。

おまけ:AIの話

私は有償のはJetBrainsAIしか使っておらず、Junieさんは順番待ち。他は手を出していません。なので、まるっとお任せ系(なんて表現が妥当なの?)の手触りはわかっていません。 そっち系で「世界が変わった」みたいなのを横目に見つつ、レイトマジョリティである私はいつごろその世界に行くんだろうなぁって思いながら過ごしてます。 その試行錯誤する暇あったらブランチマイニングしてたいし。 その辺りを知らずにあまりAIのことを語っても的外れになる確信だけはあんだけど、的外れでもいいかぁって思いながら言及してます。

感覚だと これくらい(画像のポストしたツリー) ですが、どうなるやら。腰の重いJTCと揶揄されるようなところが無視できないくらいのビジネスインパクトを与えられればもっと早まるかもですけど。

ちなみに今回のはAIに「 TemporalAdjusters 使わないの?」「 getDayOfYear() は?」とか聞いたら、いろんな言い訳しながら私が書いたのと同じようなコードにはなりはします。聞かないと出してきてくれないです。これに限らず、AIを使う側が存在を知らないと引き出せない系は結構あると思います。これは人相手でも同じなのでそういうものなんでしょうけど。 TemporalAdjusters & getDayOfYear() が良くて between が悪いってことではないですが。ちなみに最初に出してきたコードと TemporalAdjusters & getDayOfYear() では当日の扱いが違うので結果が違ってきます。テストコード書いてそれ通す系のAIならいいんですが、単にそれっぽく動くものだとこの違いがあることを言わずに挙動変えてくるので、その辺が迂闊な人に「AI使えばいいじゃん」みたいなことを業務でやるのは……と思ったけど、その辺が迂闊な人はAI使わなくてもたいして変わらないから別に気にしなくていいやって思いました。

おまけ:AIで仕事はなくならない

「AI使ってるのにまだ仕事しなきゃいけないんだけど!」って嘆き(ネタ)が聞こえたので、ざっと描いたもの。

今までできていなかった本来の仕事ができるようになります。AIを使うことで偶有的複雑さを持ち込むことになるので、その仕事も新たに増えるだろうけど。

アジャイルサムライはいまでも読み返すと「それな」ってなります。システム開発をしていない業界の人にも紹介してみたら「仕事でめっちゃ役立ってる」って言ってて、そうよねーって思いました。

Java21に上げたらテスト通らないんだが

まだJava17の子がいたので、雑にJava21に上げてみたらテスト落ちたの。 「なんでそんなとこ落ちんの?」って感じだったのでメモ。

何変わったんだろ

正規表現\b の扱いが変わってたっぽいです。

Java17でやるとこうで、

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

jshell> "あ".matches(".\\b")
$1 ==> true

Java21だとこうなります。

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

jshell> "あ".matches(".\\b")
$1 ==> false

わお。

ちなみに "a".matches(".\\b") はどっちも true です。

いつ変わったんだろ

API仕様を見比べるとJava18 までは 単語境界 だったのが、 Java19単語境界 : (?:(?<=\w)(?=\W)|(?<=\W)(?=\w)) (単語以外の文字が単語文字を省略するロケーション) に変わっていました。

多分この辺なんだろうなぁと思いつつ、SDKMAN!にもJava18-20はもうないので、挙動での確認は面倒だなぁと。 別にバージョン特定しなくてもいいかなぁと思いながらコミットログ眺めてたらこれが目に入りました。API仕様と同時の19でよさそう。

とりあえず17から21にしたら変わっちゃいます。正規表現を使っているとこのテストは見直しとくといいかもです。

なおAPI仕様の説明は Java24でまた変わる みたいです。

ちなみに

UNICODE_CHARACTER_CLASS したらJava21でもJava18以前と同じ結果の true にはなります。ここに関しては。他も変わるからまぁあれだ。

// Java17, 21どちらも同じ
jshell> Pattern.compile(".\\b", Pattern.UNICODE_CHARACTER_CLASS).matcher("あ").matches()
$2 ==> true


// java21
jshell> Pattern.compile(".\\b").matcher("あ").matches()
$3 ==> false

// java17
jshell> Pattern.compile(".\\b").matcher("あ").matches()
$3 ==> true

もともとUNICODE_CHARACTER_CLASS つけなきゃ \w"あ" とかに使えなかったわけで。

// Java17, 21どちらも同じ
jshell> Pattern.compile("\\w", Pattern.UNICODE_CHARACTER_CLASS).matcher("あ").matches()
$4 ==> true

// Java17, 21どちらも同じ
jshell> Pattern.compile("\\w").matcher("あ").matches()
$5 ==> false

\w\b の対応が取れていなかったのがちゃんと取れるようになったようです。

まとめ

雑に正規表現使ってると踏むかもしれない挙動変更がJava19にありました。

挙動変更は更新追ってなければテストでしか気づけないから、やっぱテストは大事。

バージョン跨いだテストは早めに回しておきたいところ。できるところはやってるんだけど、Gradleのバージョン上げれてなくてできてなかったんだよね……

宣伝

Java24が2025-03-18にGA予定です。

Java24なので次回関ジャバは3/24に行います。

kanjava.connpass.com

Java24の話をしつつも、17以降で「これは実務に影響あったなぁ」とか22以降の「これは使いそうだなぁ」みたいな話をしようと思っています。