Java | Java 標準ライブラリ:Date の問題点

Java Java
スポンサーリンク

なぜ「Date の問題点」を知っておくべきか

Java の古い日付・時刻 API(java.util.Date / java.util.Calendar)は、
長年「バグを生みやすい」「直感とズレている」と言われてきました。

だからこそ、Java 8 以降で java.time パッケージ(LocalDateLocalDateTimeInstant など)が新しく作られています。

「Date はダメ、java.time を使え」とだけ聞いても、
なぜダメなのかが分からないと、既存コードを読むときに困ります。

ここでは、Date の何が具体的に危険で、どこでつまづきやすいかを、
初心者向けにかみ砕いて整理していきます。


問題点1:Date は「日付」じゃない(実質ただの“ミリ秒”)

Date の本当の中身

java.util.Date は「日付」と名前がついていますが、
実態は「1970-01-01 00:00:00 UTC からの経過ミリ秒」を持つだけのクラスです。

例えば:

Date now = new Date();
System.out.println(now.getTime()); // 1700000000000 とか、ミリ秒の数字
Java

getTime() が返すのは、世界共通の “瞬間” をミリ秒で表したただの数字です。

ここには、

年月日
何時何分何秒
どのタイムゾーンか(日本時間?アメリカ時間?)

といった情報は、一切含まれていません。

年月日・時刻・タイムゾーンは、「表示するときに別のクラス(DateFormatCalendar)で解釈/変換しているだけ」です。

Date だけ見ても「何時何分か」「どの国の時間か」が分からない

Date インスタンスを 1 個だけ渡されても、

それが「日本時間での 9 時なのか」
「ロサンゼルス時間での 9 時なのか」

は、Date だけでは判定できません

タイムゾーンは、変換・表示のときの「外部設定」によって決まるからです。

その結果、

同じ Date を東京で表示すると「2025-01-01 09:00」
ロンドンで表示すると「2025-01-01 00:00」

ということが普通に起こります。

「Date を持っている=“日本時間の XX 時”を持っている」と思って使うと、
時差のある環境で痛い目を見ます。


問題点2:Date は「可変(mutable)」なので、思わぬところで書き換えられる

Date は中身を書き換え可能

StringInteger は一度作ったら中身が変わらない「不変(immutable)」なクラスですが、
Date は「可変(mutable)」です。

Date date = new Date();
long time = date.getTime();

// 中身を書き換えることができる
date.setTime(time + 24L * 60 * 60 * 1000); // 1日後に変更
Java

setTime で、中身のミリ秒を自由に書き換えできます。

引数やフィールドで「すり替え」が起こる

例えば、こんなクラスがあったとします。

class Event {
    private Date when;

    Event(Date when) {
        this.when = when;
    }

    public Date getWhen() {
        return when;
    }
}
Java

このクラスを使う側:

Date d = new Date();
Event e = new Event(d);

Date got = e.getWhen();
got.setTime(0L); // ここで書き換え

System.out.println(e.getWhen()); // 中身が 1970-01-01 ... に変わっている
Java

getWhen() で返ってきた Date を書き換えると、
Event の内部状態も同時に変わってしまいます。

防御的コピー(コンストラクタと getter で new Date(date.getTime()) するなど)をしていなければ、
「外部から簡単に内部状態を変えられる」危険な設計になります。

不変な LocalDate / LocalDateTime と違って、
Date は「渡した先でいつでも変えられてしまう」という前提を常に意識しなければなりません。


問題点3:古いメソッドの仕様が直感とズレていてバグ源(しかも多くが非推奨)

月が 0 始まり、年が 1900 起点という罠

Date には、昔の名残のメソッドがたくさんあります(多くは @Deprecated)。

例えば:

Date date = new Date();

// 推奨されない古いメソッドたち
int year = date.getYear();   // 1900 年からの差分(2025年なら 125 が返る)
int month = date.getMonth(); // 0 始まり(1月が 0、2月が 1)
int day = date.getDate();    // 日にちは 1 始まり
Java

初心者がこれを見ると、

new Date(2025, 1, 1) で 2025 年 1 月 1 日かな?」

と思いがちですが、実際はこういう仕様です。

Date d = new Date(125, 0, 1); // 2025年1月1日

// 年:1900 + 125 = 2025
// 月:0 → 1 月
Java

年は 1900 を基準にした「差分」
月は 0 始まり
日付だけ 1 始まり

という、非常に直感に反する設計です。

当然、うっかり new Date(2025, 1, 1) と書くと「西暦 3925 年 2 月 1 日」みたいな、
意味不明な日付になってしまいます。

これらの古いコンストラクタや getter/setter は、今は全部 @Deprecated で「使うな」と言われていますが、
古い本や記事、既存コードには普通に登場します。

そういうコードを読むときに、
「年は 1900 基準」「月は 0 始まり」という変な仕様を知らないと、絶対に混乱します。

Date は日付と時刻とタイムゾーン感覚がごちゃまぜになりやすい

Date 自体にはタイムゾーンの概念がないのに、
toString() すると「ローカルタイムゾーンでの表示」になってしまいます。

Date now = new Date();
System.out.println(now); // 実行環境のタイムゾーンに依存した文字列
Java

同じミリ秒値の Date でも、

JVM のデフォルトタイムゾーンが日本なら「JST の日付・時刻」で表示
サーバーが UTC 設定なら「UTC の日付・時刻」で表示

という違いが出ます。

「Date の文字列表現=世界共通の絶対時間」だと思うと、ひどく騙されます。
本質はただの「UTC の瞬間(ミリ秒)」なので、
表示のタイムゾーン指定をきちんと管理しないと、見る人・環境によって表示が変わるのが非常にやっかいです。


問題点4:DateFormat / SimpleDateFormat と組み合わせるとスレッドセーフじゃない

「フォーマット・パース担当」は Date とは別クラス

日付文字列との相互変換には、昔から DateFormat / SimpleDateFormat が使われてきました。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

Date date = fmt.parse("2025-01-01 10:00:00"); // 文字列 → Date
String s = fmt.format(date);                  // Date → 文字列
Java

ここでも、Date 自体にはタイムゾーン情報はなく、
SimpleDateFormat の持つタイムゾーン設定・ロケール設定で解釈されます。

SimpleDateFormat が「スレッドセーフではない」

SimpleDateFormat は、マルチスレッド環境ではそのまま共有して使えません

1 つの SimpleDateFormat インスタンスを複数スレッドから同時に parse / format すると、
内部状態が壊れて「おかしな日時」になったり、ParseException が異様に多発したりします。

Web アプリなどで、

private static final SimpleDateFormat FMT = ...;

として共有してしまうと、負荷がかかったときにだけ変な日付が出てくる…という恐ろしいバグにつながります。

DateDateFormat の組み合わせは、

基準が UTC
表示はローカルタイムゾーン
フォーマッタはスレッドセーフでない

と、問題が複合的に絡むので、
真面目にマルチスレッド・多タイムゾーン対応しようとするととても扱いづらいのが現実です。


問題点5:Date だけでは「日付だけ」「時刻だけ」「期間」などを表しにくい

「日付だけ欲しい」のに「時刻」までくっついてくる

誕生日や営業日など、「日付だけ」が欲しい場面は多いですよね。

しかし、Date は本質的に「瞬間(日時)」であり、
「2025-01-01 の 00:00:00.000 (UTC 換算でどこか)」という形で表現されることになります。

「日付だけ扱いたい」のに、
タイムゾーンの解釈次第で日付がズレたり、
equals で比較したときに微妙な時刻差で false になったり、と非常に不便です。

それに対して LocalDate は、
「タイムゾーンに依存しない、純粋な日付」として扱えます。

「期間」「時間だけ」などを別々に扱えない

Date 一つでは、

「2 時間」という期間
「朝 9 時」という“時刻のみ”
「1 か月」という期間

といった概念は表現できません。

全部、瞬間(ミリ秒)に押し込めようとすると、

期間は milliseconds の差分として手計算
時刻だけ扱いたいのに、どこの日の何時かまで一緒にくっつく

といった、無理矢理感のある設計になります。

java.time 系の API は、これをきちんと分解しています。

LocalDate … 日付のみ
LocalTime … 時刻のみ
LocalDateTime … 日付+時刻
Instant … 1970-01-01T00:00:00Z からの瞬間
Duration … 時間単位の「期間」
Period … 年月日単位の「期間」

日付/時刻/期間などの概念をちゃんと分けて表現できるのに比べて、
Date は「何でもミリ秒に押し込んでしまう」ため、
何を表しているのかコードだけではとても読み取りづらいのが問題です。


じゃあ Date は全部捨てるべき?既存コードとどう付き合うか

新しく書くコードでは基本「java.time」を使う

新規コードを書くなら、基本は

Date / Calendar / SimpleDateFormat は使わない
LocalDate / LocalDateTime / ZonedDateTime / Instant / DateTimeFormatter など、java.time 系を使う

この方針でほぼ困りません。

java.time

不変(immutable)
明確な型分け(日付だけ、時刻だけ、期間など)
タイムゾーンが型として明示的(ZonedDateTime など)

なので、Date の抱えていた問題の大部分が、設計レベルで解消されています。

既存コードの Date と安全にやり取りする

とはいえ、既存ライブラリや古いコードは Date を使っています。

その場合の現実的な付き合い方は、

外部 API から受け取った Date は、入ってきたらすぐ Instant / LocalDateTime などに変換して内部では java.time で扱う
外部 API に渡すときだけ、Date.from(instant) などで一時的に Date に変換する

というスタイルです。

例えば、

Date legacyDate = ...; // どこかから来た
Instant instant = legacyDate.toInstant();

// 日本時間の LocalDateTime に変換
LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Tokyo"));
Java

逆に、LocalDateTime から Date への変換はこんな感じです。

LocalDateTime ldt = ...;
ZoneId zone = ZoneId.of("Asia/Tokyo");

Instant instant = ldt.atZone(zone).toInstant();
Date date = Date.from(instant);
Java

こうやって、境界でだけ Date を使い、中では java.time で扱うようにすると、
Date の持つ危険性に引きずられずに済みます。


まとめ:Date の問題点を自分の中でどう整理しておくか

java.util.Date の問題を初心者向けに一言でまとめると、

名前のわりに中身は“ただのミリ秒”、しかも可変で、古い仕様とタイムゾーンと周辺クラスのせいでバグの温床

です。

特に意識しておきたいのは次の点です。

  • Date は「UTC からのミリ秒」であって、「日本時間の 2025/01/01 9:00」そのものではない
  • Date は可変なので、渡した先・返した先で簡単に書き換えられてしまう
  • 古いコンストラクタや getYear/getMonth などは仕様が歪で、今は非推奨
  • 文字列変換にはスレッドセーフでない SimpleDateFormat が絡みがちで、マルチスレッドで壊れやすい
  • 「日付だけ」「時刻だけ」「期間」などの表現が苦手で、何でもミリ秒に押し込む設計になっている

新しいコードでは java.time を使い、
既存コードで Date に出会ったら、

「これは UTC の瞬間であって、表示タイムゾーン次第で見え方が変わる」
「可変だから、防御的コピーが必要かもしれない」

という意識で読むと、だいぶ事故が減ります。

タイトルとURLをコピーしました