Java 8 日付・時刻 API 入門|LocalDateTime / Instant / Duration 実用サンプル集
Java 8 以前は、日時を記録する方法として Unix タイムスタンプを long で持つか、java.util.Date を使うのが一般的でした。Java 8 で導入された java.time パッケージは、日付だけ・時刻だけ・期間・タイムゾーンつきの瞬間などを、別々の型でクリーンに扱えます。本記事では新しい型の全体像と、実際に書くことになる操作のサンプルを紹介します。
Java 8 以前
従来のやり方
Java 8 以前の選択肢は大きく 2 つです。ひとつは Unix タイムスタンプ(POSIX 時間 / Epoch 時間)を long で保持する方法。System.currentTimeMillis() でミリ秒精度の Unix タイムスタンプが取得できます。
もうひとつは java.util.Date を使う方法です。Date の内部も Unix タイムスタンプで、getTime() で取り出せます。人間が読める文字列に変換するには java.text.SimpleDateFormat と組み合わせるのが定番でした。
従来 API の問題点
これらのアプローチには以下のような弱点がありました。
- 可変性:
Date#setTime()で中身のタイムスタンプを書き換えられます。フィールドをfinalにしても参照が不変になるだけで、オブジェクト自体はイミュータブルになりません。マルチスレッドや関数型スタイルのコードで意図しない変更が起きやすく、バグの温床になります。 - タイムゾーンの扱い:Unix タイムスタンプは暗黙的に UTC ですが、
java.util.Date自体はタイムゾーンを持ちません。変換のたびに OS のタイムゾーンが使われ、ミスが発生しやすいです。 - 精度:
java.util.Dateはミリ秒までしか保持できません。 - 日付のみ・時刻のみが表現できない:「日付だけ」「時刻だけ」を持つ型がありません。JPA の Entity では
TemporalTypeで DB 側のDATE/TIME/TIMESTAMPのどれに対応するかを別途指定する必要がありました。
Java 8 で導入された API
Java 8 では java.time パッケージが追加され、文字列処理用に java.time.format もついてきました。Date の代わりに使うのが LocalDateTime、日付だけを扱う LocalDate、時刻だけを扱う LocalTime です。
「Local」という接頭辞はタイムゾーン情報を持たない「ローカルタイム」を表します。タイムゾーンを扱いたい場合は、地理的なタイムゾーン(例:Asia/Tokyo)を持つ ZonedDateTime、あるいは UTC+9 のような固定オフセットを持つ OffsetDateTime を使います。
さらに、タイムライン上の 1 点を表す Instant、期間(時間の長さ)を表す Duration も追加されました。最近の Spring Data JPA や Hibernate は Local* 系の型をネイティブサポートしており、SQL の DATE / TIME / TIMESTAMP に自動マッピングされます。Entity のフィールド型としても Date の代わりにそのまま使えます。
以下では各種の操作サンプルを紹介します。
LocalDateTime を作る
直接生成する方法と、ISO 8601 形式の文字列から生成する方法です。カスタム形式でパースしたい場合は後述の「文字列から LocalDate へ」を参照してください。
import java.time.LocalDateTime;
// 現在時刻から生成
LocalDateTime.now();
// ISO 8601 形式の文字列から生成
LocalDateTime.parse("2023-03-12T08:35:00");
LocalDateTime を LocalDate / LocalTime に変換する
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.LocalTime;
LocalDateTime dateTime = LocalDateTime.now();
// 日付のみ
LocalDate date = dateTime.toLocalDate();
// 時刻のみ
LocalTime time = dateTime.toLocalTime();
Unix タイムスタンプを LocalDateTime に変換する
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
// 秒
long tsSec = 1678550618L;
LocalDateTime fromSec = LocalDateTime.ofInstant(
Instant.ofEpochSecond(tsSec), ZoneId.systemDefault());
// ミリ秒
long tsMs = 1678550618900L;
LocalDateTime fromMs = LocalDateTime.ofInstant(
Instant.ofEpochMilli(tsMs), ZoneId.systemDefault());
LocalDateTime を Unix タイムスタンプに変換する
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
LocalDateTime dateTime = LocalDateTime.now();
// ミリ秒で出力
long ms = ZonedDateTime.of(dateTime, ZoneId.systemDefault())
.toInstant()
.toEpochMilli();
文字列から LocalDate へ
このセクションはコード量が多いので、var を使ってすっきり書きます。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
// 例 1
var str1 = "2023-03-12";
var formatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
var date1 = LocalDate.parse(str1, formatter1);
// 例 2
var str2 = "2023年03月12日";
var formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
var date2 = LocalDate.parse(str2, formatter2);
// 例 3 — パターン内に固定文字列を入れる(シングルクォートで囲む)
var str3 = "ログ 2023/03/12";
var formatter3 = DateTimeFormatter.ofPattern("'ログ' yyyy/MM/dd");
var date3 = LocalDate.parse(str3, formatter3);
DateTimeFormatter のパターン一覧は本記事の末尾にあります。
文字列から LocalDateTime へ
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
String str = "2023-03-12 15:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime dateTime = LocalDateTime.parse(str, formatter);
LocalDate を文字列にフォーマットする
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
String str = date.format(formatter);
// LocalDateTime でも同じ formatter で日付部分だけ出力できる
LocalDateTime dateTime = LocalDateTime.now();
String strFromDateTime = dateTime.format(formatter);
期間(実行時間)を計測する
プログラムの実行時間の計測が代表的な用途です。
import java.time.Instant;
import java.time.Duration;
// 開始時刻
Instant begin = Instant.now();
// ...時間のかかる処理...
// 終了時刻
Instant end = Instant.now();
// 2 つの時刻から Duration を作る
Duration duration = Duration.between(begin, end);
long seconds = duration.toSeconds();
long millis = duration.toMillis();
long nanos = duration.toNanos();
日付・時刻の加減算
java.time 系の型はイミュータブルなので、加減算は元のオブジェクトを変更せず、新しいオブジェクトを返します。必ず戻り値を受け取ってください。
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
LocalDateTime dateTime = LocalDateTime.now();
// 方法 A — 名前付きメソッド
LocalDateTime a = dateTime
.plusYears(1)
.plusMonths(1)
.plusWeeks(1)
.plusDays(1)
.plusHours(1)
.plusMinutes(1)
.plusSeconds(1)
.plusNanos(1);
// 方法 B — plus(amount, unit)
LocalDateTime b = dateTime
.plus(1, ChronoUnit.YEARS)
.plus(1, ChronoUnit.MONTHS)
.plus(1, ChronoUnit.WEEKS)
.plus(1, ChronoUnit.DAYS)
.plus(1, ChronoUnit.HALF_DAYS) // 12 時間
.plus(1, ChronoUnit.HOURS)
.plus(1, ChronoUnit.MINUTES)
.plus(1, ChronoUnit.SECONDS)
.plus(1, ChronoUnit.MILLIS)
.plus(1, ChronoUnit.NANOS);
各 plusXxx には対応する minusXxx があり、plus() にも minus() が対応します。plus() に負の値を渡しても同じ結果になります。Duration は期間を表す型なので、そのまま plus() / minus() の引数として渡せます。
DateTimeFormatter のパターン
DateTimeFormatter のパターン文字は従来の SimpleDateFormat とほぼ同じです。以下は簡易リファレンスです。詳細は Java Doc DateTimeFormatter を参照してください。
「例」欄は 2023-03-09T08:01:05 を想定しています。
| パターン | 意味 | 例 |
|---|---|---|
y | 年 | 2023 |
yyyy | 年(同上) | 2023 |
yy | 年(2 桁) | 23 |
Y | 週ベースの年 | 2023 |
u | ISO 週ベースの年 | 2023 |
M | 月(0 埋めなし) | 3 |
MM | 月(0 埋め) | 03 |
d | 日(0 埋めなし) | 9 |
dd | 日(0 埋め) | 09 |
h | 12 時間制の時(0 埋めなし) | 8 |
hh | 12 時間制の時(0 埋め) | 08 |
H | 24 時間制の時(0 埋めなし) | 8 |
HH | 24 時間制の時(0 埋め) | 08 |
m | 分(0 埋めなし) | 1 |
mm | 分(0 埋め) | 01 |
s | 秒(0 埋めなし) | 5 |
ss | 秒(0 埋め) | 05 |
S | 秒の小数点以下 1 桁 | 0 |
SS | 秒の小数点以下 2 桁(最大 9 桁まで) | 00 |
a | 午前・午後(ロケール依存) | 午前 |