LocalDateTime で時刻も扱う

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • LocalDateTime.of(2024, 1, 15, 14, 30) で日付 + 時刻 (タイムゾーンなし)
  • now() で現在時刻、parse("2024-01-15T14:30:00") で ISO 形式
  • タイムゾーンが必要なら ZonedDateTime、時刻だけなら LocalTime
  • plusHours(3) minusMinutes(15) などイミュータブルに加減算

LocalDateTime で時刻も扱う とは

前回学んだ LocalDate に時刻を加えた LocalDateTime を使い、DateTimeFormatter で日付と時刻を任意の形式に書式化する流れを身につけよう。

LocalDate だけでは足りない場面

前のレッスンでは、日付だけを表す java.time.LocalDate を学びました。誕生日や記念日のように「何年何月何日」だけが分かれば十分な場面では LocalDate で困りませんが、業務システムを書いていると「何時何分」までセットで扱いたい場面が一気に増えます。たとえば商品の購入日時、Slack への投稿時刻、サーバーの起動ログ、Android 端末の通知配信時刻など、ほとんどのログ系データは秒単位の時刻を含んでいます。

Web サービスを 1 つ書くだけでも、createdAt updatedAt lastLoginAt のような時刻つきカラムは必ず登場します。日付しか持たない LocalDate を使ってしまうと、後から「あと何時何分か知りたい」となったときに DB のスキーマごと作り直すハメになるので、最初の型選びは重要です。

そこで使うのが java.time.LocalDateTime です。名前のとおり LocalDate (日付) と LocalTime (時刻) を 1 つにまとめたクラスで、年/月/日/時/分/秒/ナノ秒 までを 1 つのオブジェクトで持ち運べます。Java 8 から導入されたモダンな日時 API の中心的存在で、java.util.DateCalendar のような旧 API の代わりに、原則としてこちらを使うのが現代の Java の作法です。

diagram (will load when visible)

本章ではまず LocalDateTime の作り方と中身の取り出し方を確認し、続いて DateTimeFormatter を使って好きな書式の文字列に変換する流れを学びます。最後に、世界中の時刻を扱うときに必要な ZonedDateTime OffsetDateTime への入口も少しだけ覗いておきましょう。

LocalDateTime.of(...) でインスタンスを作る

LocalDateTime には new を使いません。Stringint と違って、コンストラクタは private になっていて、static なファクトリメソッド経由で生成するのが Java の新しい日時 API の流儀です。一番よく使うのが of メソッドです。

Java

import java.time.LocalDateTime; public class Demo { public static void main(String[] args) { LocalDateTime dt = LocalDateTime.of(2026, 5, 19, 14, 30); System.out.println(dt); // 2026-05-19T14:30 } }

of(年, 月, 日, 時, 分) の順番で渡します。秒やナノ秒まで指定したいときは of(年, 月, 日, 時, 分, 秒)of(年, 月, 日, 時, 分, 秒, ナノ秒) というオーバーロードもあります。月は 1〜12 の整数 で、LocalDate のときと同じく 0 始まり ではないので素直に渡せます。Java の月は本来 1 始まりですが、旧 API の Calendar.MONTH0 が 1 月という罠があったので、新 API では混乱を断ち切るためにキッチリ 1 始まりに統一されています。

LocalDateTime.now() を呼ぶと、コードを実行した瞬間の日時をそのまま取得できます。テストや実運用では「今」を取りたいことが多いので、まず now() で動きを確認してから of(...) で固定値に切り替える、という順番で覚えると忘れにくいです。

生成後は getYear() getMonthValue() getDayOfMonth() getHour() getMinute() といったゲッターで各フィールドを取り出せます。たとえば dt.getMonthValue()int5dt.getMonth()enumMAY を返すなど、欲しい型に応じて使い分けます。

書式化の主役 DateTimeFormatter

System.out.println(dt) の出力は 2026-05-19T14:30 のような ISO_LOCAL_DATE_TIME 形式で、間に T が挟まる独特の形をしています。これは機械可読には便利ですが、画面に表示するなら 2026-05-19 14:30 のようにスペース区切りにしたいことが多いはずです。そこで登場するのが java.time.format.DateTimeFormatter です。

Java

import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class Demo { public static void main(String[] args) { LocalDateTime dt = LocalDateTime.of(2026, 5, 19, 14, 30); DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); String text = dt.format(fmt); System.out.println(text); // 2026-05-19 14:30 } }

肝になるのが ofPattern("yyyy-MM-dd HH:mm") の中身です。記号の意味は次のとおりです。

  • yyyy — 4 桁の年 (例 2026)
  • MM — 0 埋めした月 2 桁 (例 05)
  • dd — 0 埋めした日 2 桁 (例 19)
  • HH — 24 時間制の時 2 桁 (例 14)
  • mm — 0 埋めした分 2 桁 (例 30)
  • ss — 0 埋めした秒 2 桁 (HH:mm:ss で時分秒)

大文字小文字で意味が変わるので、mmMMhh12 時間制HH24 時間制 といった違いを覚えておきましょう。次の節で詳しく触れますが、ここを間違えると「月のはずが分になっている」「14 時のはずが 02 と出る」など、しらっと壊れたまま動いてしまうので要注意です。

diagram (will load when visible)

逆向きに「文字列を LocalDateTime に戻したい」ときは、LocalDateTime.parse(text, fmt) を使います。書式が合わないと DateTimeParseException が飛ぶので、入力値の検証もここで兼ねられます。今回の課題では format 側だけを使うので、parse は次のレッスンで深掘りします。

ZonedDateTime OffsetDateTime の予告

LocalDateTime は便利ですが、名前のとおり ローカル なので、タイムゾーンの情報を持っていません。日本国内で完結するシステムでは困りませんが、海外サーバーと通信したり、世界中のユーザーがいる SaaS を作るときは「これは日本時間? それとも UTC?」が大問題になります。

Java

import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.ZoneId; import java.time.OffsetDateTime; import java.time.ZoneOffset; public class Demo { public static void main(String[] args) { LocalDateTime local = LocalDateTime.of(2026, 5, 19, 14, 30); ZonedDateTime tokyo = local.atZone(ZoneId.of("Asia/Tokyo")); OffsetDateTime utc = local.atOffset(ZoneOffset.UTC); System.out.println(tokyo); // 2026-05-19T14:30+09:00[Asia/Tokyo] System.out.println(utc); // 2026-05-19T14:30Z } }

ZonedDateTimeAsia/Tokyo のような 地域名つきタイムゾーン を持ち、夏時間 (DST) の有無まで自動で扱ってくれます。OffsetDateTime+09:00 のような 固定オフセット だけを持ち、DBTIMESTAMP WITH TIME ZONE カラムへの保存などで重宝します。本レッスンでは深入りしませんが、「LocalDateTime は時差なし、ZonedDateTime / OffsetDateTime は時差あり」という対応関係だけ頭に入れておきましょう。

業務では「UTC で保存して、表示するときだけ各ユーザーのタイムゾーンに変換する」がほぼ定石です。LocalDateTime をそのまま DB に入れると、東京と New York の社員で同じレコードが違う時刻に見えてしまうので、内部表現としては OffsetDateTimeInstant を選ぶことが多いです。

よくある間違い

LocalDateTimeDateTimeFormatter で初学者が引っかかる定番ミスを 3 つまとめます。バグレポート がきたときの解像度がぐっと上がるので、ぜひ目を通しておきましょう。

  • yyyyYYYY を取り違える — 小文字 yyyy が普通の暦年 (2026)、大文字 YYYYweek-based-year という年またぎ週の特殊な年を表します。たとえば 2025-12-29 (月) のように年末が月曜から始まる週だと、YYYY2026 を返すことがあります。請求書や領収書を作るときに混入すると、「なぜか 1 件だけ翌年扱い」というバグになるので、 を出したいときは必ず小文字 yyyy を使います
  • MM (月) と mm (分) を取り違えるyyyy-mm-dd hh:MM のように書いてしまうと、月のはずの位置に分が、分のはずの位置に月が入り、2026-30-19 02:05 のような壊れた文字列ができあがります。日本語で書くと「月は大文字 MM、分は小文字 mm」、と語呂で覚えるのが安全です
  • SimpleDateFormat を使ってしまう — 旧 API の java.text.SimpleDateFormat は今でも import できますが、スレッドセーフでないため、ServletSpring のように複数リクエストが同時に走る環境で共有変数として持つと、たまに壊れた文字列を返すという最悪のバグになります。新規コードでは必ず DateTimeFormatter を使い、SimpleDateFormat は触らないのが鉄則です

もう 1 つ細かい話として、HH (24 時間制) と hh (12 時間制) の混同もよく見ます。14 時 30 分"hh:mm" で出力すると 02:30 になってしまい、AM/PM が分からなくなります。24 時間制で表示したいなら大文字 HH 一択です。

過去に有名な事例として、SimpleDateFormatstatic final で共有していた金融系システムが、同時アクセス時に日付が混線して数百万円の決済ズレを起こした、というインシデントがあります。DateTimeFormatterimmutable でスレッドセーフなので、static final で共有しても安全です。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 予約システムの時間 ─ 「2024-01-15 14:30 の予約」のような業務的時刻
  • ログのタイムスタンプ ─ サーバー時刻として LocalDateTime.now() を記録
  • スケジューラ ─ 「毎日 03:00 にバッチ実行」のような時刻ベースのトリガー
  • 契約の有効期限 ─ 「契約終了日時 2025-12-31 23:59:59」を扱う

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • タイムゾーン無視 ─ サーバーが UTC でアプリが JST だと 9 時間ズレる。本番では ZonedDateTime か明示変換
  • Date を混ぜているjava.util.DateLocalDateTime の混在は事故のもと。プロジェクト全体で統一
  • DB 永続化で LocalDateTime ─ DB のタイムゾーン設定とアプリの設定が合っていないとズレる。TIMESTAMP WITH TIME ZONE か UTC 統一
  • now() をテストでハードコード ─ Clock を注入してモックできるようにする (Clock.fixed(...))

パフォーマンス考慮事項

  • 生成は軽量 ─ 数百 ns/回、気にしないでよい
  • フォーマット解析が重めDateTimeFormatter を毎回作らず static final で共有
  • スレッドセーフLocalDateTimeDateTimeFormatter もイミュータブル
  • 変換コストtoLocalDate() toLocalTime() は新インスタンス生成、ループ内では注意
この章のポイント

ここまでの要点 LocalDateTime は日時 (タイムゾーンなし)。サーバとアプリの TZ を意識し、必要なら ZonedDateTime を使う。DateTimeFormatter は再利用。

やってみよう

それでは本日の課題に取り組みましょう。やることは次のとおりです。

  1. import java.time.LocalDateTime;import java.time.format.DateTimeFormatter; を書く
  2. Solution.formatDateTime(int year, int month, int day, int hour, int minute) の中で LocalDateTime.of(year, month, day, hour, minute) を呼んで日時オブジェクトを作る
  3. DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") でフォーマッタを用意する
  4. dt.format(fmt) の結果を return する

戻り値の例は formatDateTime(2026, 5, 19, 14, 30)"2026-05-19 14:30"formatDateTime(2024, 1, 1, 0, 0)"2024-01-01 00:00" になります。月や日や時間が 1 桁のときに 0 で埋まる点に注目してください。MM dd HH mm がそれぞれ 2 桁固定なので、明示的に String.format などを呼ばなくても DateTimeFormatter が自動で 0 埋めしてくれます。

慣れてきたら、パターンを "yyyy 年 MM 月 dd 日 (E) HH 時 mm 分" に変えて日本語表記にしたり、withLocale(Locale.JAPAN) を組み合わせて (月) のような曜日表示を出してみたりしてください。DateTimeFormatter の表現力は非常に高く、ほとんどのフォーマットがパターン 1 行で書けるようになります。

次のレッスンでは、LocalDateTime 同士の差分を Duration で計算したり、plusDays minusHours のような操作で時間を進めたり戻したりする使い方を学んでいきます。まずはここで「作る → 書式化する」の基本リズムをしっかり手に馴染ませておきましょう。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は Duration で時間差を計算 で、Duration で時間差を計算 を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. LocalDateTime の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. LocalDateTime とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. ファイル先頭で import java.time.LocalDateTime;import java.time.format.DateTimeFormatter; を書くこと
  2. LocalDateTime.of(year, month, day, hour, minute) で日時オブジェクトを生成すること (SimpleDateFormat などの旧 API は使わない)
  3. DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") を使って書式化し、その結果を return すること

入出力例

test-cases.txt

formatDateTime(2026, 5, 19, 14, 30)"2026-05-19 14:30" formatDateTime(2024, 1, 1, 0, 0)"2024-01-01 00:00" formatDateTime(2025, 12, 31, 23, 59)"2025-12-31 23:59" formatDateTime(2024, 2, 29, 12, 0)"2024-02-29 12:00"

ヒント

main.java
main.java
学習モード

メモ

LocalDateTime で時刻も扱う

⌘S で保存