LocalDateTime で時刻も扱う
このレッスンで分かること
LocalDateTime.of(2024, 1, 15, 14, 30)で日付 + 時刻 (タイムゾーンなし)now()で現在時刻、parse("2024-01-15T14:30:00")で ISO 形式- タイムゾーンが必要なら
ZonedDateTime、時刻だけならLocalTimeplusHours(3)minusMinutes(15)などイミュータブルに加減算
LocalDateTime で時刻も扱う とは
前回学んだ LocalDate に時刻を加えた LocalDateTime を使い、DateTimeFormatter で日付と時刻を任意の形式に書式化する流れを身につけよう。
LocalDate だけでは足りない場面
前のレッスンでは、日付だけを表す java.time.LocalDate を学びました。誕生日や記念日のように「何年何月何日」だけが分かれば十分な場面では LocalDate で困りませんが、業務システムを書いていると「何時何分」までセットで扱いたい場面が一気に増えます。たとえば商品の購入日時、Slack への投稿時刻、サーバーの起動ログ、Android 端末の通知配信時刻など、ほとんどのログ系データは秒単位の時刻を含んでいます。
Web サービスを 1 つ書くだけでも、
createdAtupdatedAtlastLoginAtのような時刻つきカラムは必ず登場します。日付しか持たないLocalDateを使ってしまうと、後から「あと何時何分か知りたい」となったときにDBのスキーマごと作り直すハメになるので、最初の型選びは重要です。
そこで使うのが java.time.LocalDateTime です。名前のとおり LocalDate (日付) と LocalTime (時刻) を 1 つにまとめたクラスで、年/月/日/時/分/秒/ナノ秒 までを 1 つのオブジェクトで持ち運べます。Java 8 から導入されたモダンな日時 API の中心的存在で、java.util.Date や Calendar のような旧 API の代わりに、原則としてこちらを使うのが現代の Java の作法です。
本章ではまず LocalDateTime の作り方と中身の取り出し方を確認し、続いて DateTimeFormatter を使って好きな書式の文字列に変換する流れを学びます。最後に、世界中の時刻を扱うときに必要な ZonedDateTime OffsetDateTime への入口も少しだけ覗いておきましょう。
LocalDateTime.of(...) でインスタンスを作る
LocalDateTime には new を使いません。String や int と違って、コンストラクタは 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.MONTH は 0 が 1 月という罠があったので、新 API では混乱を断ち切るためにキッチリ 1 始まりに統一されています。
LocalDateTime.now()を呼ぶと、コードを実行した瞬間の日時をそのまま取得できます。テストや実運用では「今」を取りたいことが多いので、まずnow()で動きを確認してからof(...)で固定値に切り替える、という順番で覚えると忘れにくいです。
生成後は getYear() getMonthValue() getDayOfMonth() getHour() getMinute() といったゲッターで各フィールドを取り出せます。たとえば dt.getMonthValue() は int の 5、dt.getMonth() は enum の MAY を返すなど、欲しい型に応じて使い分けます。
書式化の主役 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で時分秒)
大文字小文字で意味が変わるので、mm は 分、MM は 月、hh は 12 時間制、HH は 24 時間制 といった違いを覚えておきましょう。次の節で詳しく触れますが、ここを間違えると「月のはずが分になっている」「14 時のはずが 02 と出る」など、しらっと壊れたまま動いてしまうので要注意です。
逆向きに「文字列を 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
}
}ZonedDateTime は Asia/Tokyo のような 地域名つきタイムゾーン を持ち、夏時間 (DST) の有無まで自動で扱ってくれます。OffsetDateTime は +09:00 のような 固定オフセット だけを持ち、DB の TIMESTAMP WITH TIME ZONE カラムへの保存などで重宝します。本レッスンでは深入りしませんが、「LocalDateTime は時差なし、ZonedDateTime / OffsetDateTime は時差あり」という対応関係だけ頭に入れておきましょう。
業務では「
UTCで保存して、表示するときだけ各ユーザーのタイムゾーンに変換する」がほぼ定石です。LocalDateTimeをそのままDBに入れると、東京とNew Yorkの社員で同じレコードが違う時刻に見えてしまうので、内部表現としてはOffsetDateTimeかInstantを選ぶことが多いです。
よくある間違い
LocalDateTime と DateTimeFormatter で初学者が引っかかる定番ミスを 3 つまとめます。バグレポート がきたときの解像度がぐっと上がるので、ぜひ目を通しておきましょう。
yyyyとYYYYを取り違える — 小文字yyyyが普通の暦年 (2026)、大文字YYYYはweek-based-yearという年またぎ週の特殊な年を表します。たとえば2025-12-29(月) のように年末が月曜から始まる週だと、YYYYは2026を返すことがあります。請求書や領収書を作るときに混入すると、「なぜか 1 件だけ翌年扱い」というバグになるので、年を出したいときは必ず小文字yyyyを使いますMM(月) とmm(分) を取り違える —yyyy-mm-dd hh:MMのように書いてしまうと、月のはずの位置に分が、分のはずの位置に月が入り、2026-30-19 02:05のような壊れた文字列ができあがります。日本語で書くと「月は大文字MM、分は小文字mm」、と語呂で覚えるのが安全ですSimpleDateFormatを使ってしまう — 旧 API のjava.text.SimpleDateFormatは今でもimportできますが、スレッドセーフでないため、ServletやSpringのように複数リクエストが同時に走る環境で共有変数として持つと、たまに壊れた文字列を返すという最悪のバグになります。新規コードでは必ずDateTimeFormatterを使い、SimpleDateFormatは触らないのが鉄則です
もう 1 つ細かい話として、HH (24 時間制) と hh (12 時間制) の混同もよく見ます。14 時 30 分 を "hh:mm" で出力すると 02:30 になってしまい、AM/PM が分からなくなります。24 時間制で表示したいなら大文字 HH 一択です。
過去に有名な事例として、
SimpleDateFormatをstatic finalで共有していた金融系システムが、同時アクセス時に日付が混線して数百万円の決済ズレを起こした、というインシデントがあります。DateTimeFormatterはimmutableでスレッドセーフなので、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.DateとLocalDateTimeの混在は事故のもと。プロジェクト全体で統一- DB 永続化で
LocalDateTime─ DB のタイムゾーン設定とアプリの設定が合っていないとズレる。TIMESTAMP WITH TIME ZONEか UTC 統一 now()をテストでハードコード ─ Clock を注入してモックできるようにする (Clock.fixed(...))
パフォーマンス考慮事項
- 生成は軽量 ─ 数百 ns/回、気にしないでよい
- フォーマット解析が重め ─
DateTimeFormatterを毎回作らずstatic finalで共有 - スレッドセーフ ─
LocalDateTimeもDateTimeFormatterもイミュータブル - 変換コスト ─
toLocalDate()toLocalTime()は新インスタンス生成、ループ内では注意
ここまでの要点
LocalDateTime は日時 (タイムゾーンなし)。サーバとアプリの TZ を意識し、必要なら ZonedDateTime を使う。DateTimeFormatter は再利用。
やってみよう
それでは本日の課題に取り組みましょう。やることは次のとおりです。
import java.time.LocalDateTime;とimport java.time.format.DateTimeFormatter;を書くSolution.formatDateTime(int year, int month, int day, int hour, int minute)の中でLocalDateTime.of(year, month, day, hour, minute)を呼んで日時オブジェクトを作るDateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")でフォーマッタを用意する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 つができることを確認しましょう。
- LocalDateTime の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. LocalDateTime とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.time.LocalDateTime;とimport java.time.format.DateTimeFormatter;を書くこと LocalDateTime.of(year, month, day, hour, minute)で日時オブジェクトを生成すること (SimpleDateFormatなどの旧 API は使わない)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"