なぜ「Date の問題点」を知っておくべきか
Java の古い日付・時刻 API(java.util.Date / java.util.Calendar)は、
長年「バグを生みやすい」「直感とズレている」と言われてきました。
だからこそ、Java 8 以降で java.time パッケージ(LocalDate、LocalDateTime、Instant など)が新しく作られています。
「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 とか、ミリ秒の数字
JavagetTime() が返すのは、世界共通の “瞬間” をミリ秒で表したただの数字です。
ここには、
年月日
何時何分何秒
どのタイムゾーンか(日本時間?アメリカ時間?)
といった情報は、一切含まれていません。
年月日・時刻・タイムゾーンは、「表示するときに別のクラス(DateFormat や Calendar)で解釈/変換しているだけ」です。
Date だけ見ても「何時何分か」「どの国の時間か」が分からない
Date インスタンスを 1 個だけ渡されても、
それが「日本時間での 9 時なのか」
「ロサンゼルス時間での 9 時なのか」
は、Date だけでは判定できません。
タイムゾーンは、変換・表示のときの「外部設定」によって決まるからです。
その結果、
同じ Date を東京で表示すると「2025-01-01 09:00」
ロンドンで表示すると「2025-01-01 00:00」
ということが普通に起こります。
「Date を持っている=“日本時間の XX 時”を持っている」と思って使うと、
時差のある環境で痛い目を見ます。
問題点2:Date は「可変(mutable)」なので、思わぬところで書き換えられる
Date は中身を書き換え可能
String や Integer は一度作ったら中身が変わらない「不変(immutable)」なクラスですが、Date は「可変(mutable)」です。
Date date = new Date();
long time = date.getTime();
// 中身を書き換えることができる
date.setTime(time + 24L * 60 * 60 * 1000); // 1日後に変更
JavasetTime で、中身のミリ秒を自由に書き換えできます。
引数やフィールドで「すり替え」が起こる
例えば、こんなクラスがあったとします。
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 ... に変わっている
JavagetWhen() で返ってきた 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 = ...;
として共有してしまうと、負荷がかかったときにだけ変な日付が出てくる…という恐ろしいバグにつながります。
Date と DateFormat の組み合わせは、
基準が 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 の瞬間であって、表示タイムゾーン次第で見え方が変わる」
「可変だから、防御的コピーが必要かもしれない」
という意識で読むと、だいぶ事故が減ります。
