the KodeLab

Java 8 日付・時刻 API 入門|LocalDateTime / Instant / Duration 実用サンプル集

5,539 文字 14 分で読めます
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 を想定しています。

パターン意味
y2023
yyyy年(同上)2023
yy年(2 桁)23
Y週ベースの年2023
uISO 週ベースの年2023
M月(0 埋めなし)3
MM月(0 埋め)03
d日(0 埋めなし)9
dd日(0 埋め)09
h12 時間制の時(0 埋めなし)8
hh12 時間制の時(0 埋め)08
H24 時間制の時(0 埋めなし)8
HH24 時間制の時(0 埋め)08
m分(0 埋めなし)1
mm分(0 埋め)01
s秒(0 埋めなし)5
ss秒(0 埋め)05
S秒の小数点以下 1 桁0
SS秒の小数点以下 2 桁(最大 9 桁まで)00
a午前・午後(ロケール依存)午前