Date は「ある瞬間」をミリ秒で持つクラス、Calendar は年月日の取り出しと加減算をするクラス、SimpleDateFormat は Date と文字列を変換するクラスです。 Java 8 以降は java.time(LocalDateTime、Instant、DateTimeFormatter など)も用意されていますが、既存のコードを読む・直す・新しいAPIへつなぐ、のどの場面でもこの3つの性質を知っておくと作業が速くなります。

この記事では、相互変換を早見表にまとめ、すべてのコードに実行結果を付けました。さらに、SimpleDateFormat を複数スレッドで共有したときに結果が入れ替わる様子を再現し、DateTimeFormatter へ置き換える手順まで示します。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。コードはすべて java ファイル名.java で実行した結果です。出力をそろえるため、各コードの先頭でタイムゾーンを Asia/Tokyo に固定しています。

Date・Calendar・java.time の変換早見表

基準は 「Date と Instant は直接変換でき、LocalDateTime にするにはタイムゾーン(ZoneId)が要る」 の1点です。zone は ZoneId.of("Asia/Tokyo") または ZoneId.systemDefault() です。

変換元 → 変換先 書き方
Date → Instant date.toInstant()
Instant → Date Date.from(instant)
Date → LocalDateTime LocalDateTime.ofInstant(date.toInstant(), zone)
Date → LocalDate date.toInstant().atZone(zone).toLocalDate()
Date → ZonedDateTime date.toInstant().atZone(zone)
LocalDateTime → Date Date.from(ldt.atZone(zone).toInstant())
LocalDate → Date Date.from(ld.atStartOfDay(zone).toInstant())
Calendar → Date cal.getTime()
Date → Calendar cal.setTime(date)
Calendar → LocalDateTime LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId())
Calendar → ZonedDateTime ((GregorianCalendar) cal).toZonedDateTime()
ZonedDateTime → Calendar GregorianCalendar.from(zdt)
LocalDate ⇔ java.sql.Date java.sql.Date.valueOf(ld) / sqlDate.toLocalDate()
LocalDateTime ⇔ Timestamp Timestamp.valueOf(ldt) / ts.toLocalDateTime()
Instant ⇔ Timestamp Timestamp.from(instant) / ts.toInstant()
Date → java.sql.Date/Timestamp new java.sql.Date(date.getTime()) / new Timestamp(date.getTime())
Date ⇔ 文字列 new SimpleDateFormat("yyyy/MM/dd HH:mm:ss") の format(date) / parse(text)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Date・Calendar・SimpleDateFormatの使い方」

3つのクラスの役割と java.time との対応

クラス 持っている情報 主な用途 java.time で近いもの
java.util.Date エポック(1970-01-01T00:00:00Z)からのミリ秒 瞬間を表す値 Instant
java.util.Calendar ミリ秒+タイムゾーン+暦の各フィールド 年月日の取得・加減算 ZonedDateTime
java.text.SimpleDateFormat パターンとLocale、内部の作業用Calendar Date と文字列の変換 DateTimeFormatter
java.sql.Date 日付だけ(時刻は0に正規化) SQL の DATE 型 LocalDate
java.sql.Timestamp Date+ナノ秒 SQL の TIMESTAMP 型 LocalDateTime/Instant

Date 自身はタイムゾーンを持ちません。 toString() の表示にだけ、実行環境のデフォルトタイムゾーンが使われます。表示が9時間ずれて見えるときは、値ではなく表示に使われたタイムゾーンを確認します。

Date・Calendar・SimpleDateFormat の基本コード

生成・整形・解析・フィールド取得・加減算・比較を1本にまとめた完成形です。Date は setTime で書き換えられる(可変)ので、他へ渡すときは new Date(d.getTime()) で値をコピーすると安心です。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Basics {
    public static void main(String[] args) throws ParseException {
        // 出力をそろえるため、タイムゾーンを日本時間に固定
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));

        // 1. Date:1970-01-01T00:00:00Z からのミリ秒を1つ持つだけのクラス
        Date epoch = new Date(0L);
        Date d = new Date(1791003909123L);
        System.out.println("epoch      = " + epoch);
        System.out.println("d          = " + d);
        System.out.println("getTime()  = " + d.getTime());

        // 2. SimpleDateFormat:Date と文字列の変換
        SimpleDateFormat f = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
        System.out.println("format     = " + f.format(d));
        Date parsed = f.parse("2026/10/03 14:05:09.123");
        System.out.println("parse      = " + parsed.getTime());
        System.out.println("曜日つき   = " + new SimpleDateFormat("yyyy年M月d日(E) a h時m分", Locale.JAPAN).format(d));

        // 3. Calendar:年月日の取り出しと加減算
        Calendar cal = Calendar.getInstance();
        cal.setTime(d);
        System.out.println("YEAR       = " + cal.get(Calendar.YEAR));
        System.out.println("MONTH      = " + cal.get(Calendar.MONTH) + "  (0始まり。10月は9)");
        System.out.println("DAY        = " + cal.get(Calendar.DAY_OF_MONTH));
        System.out.println("DAY_OF_WEEK= " + cal.get(Calendar.DAY_OF_WEEK) + "  (日曜=1 … 土曜=7)");
        System.out.println("HOUR_OF_DAY= " + cal.get(Calendar.HOUR_OF_DAY));

        cal.add(Calendar.DAY_OF_MONTH, 30);
        System.out.println("30日後     = " + f.format(cal.getTime()));
        cal.add(Calendar.MONTH, -2);
        System.out.println("2か月前    = " + f.format(cal.getTime()));

        // 4. 日付を指定して作る(月は定数か「月-1」)
        Calendar c2 = Calendar.getInstance();
        c2.clear();
        c2.set(2026, Calendar.OCTOBER, 3, 14, 5, 9);
        System.out.println("set        = " + f.format(c2.getTime()));

        // 5. 比較
        Date a = f.parse("2026/10/03 00:00:00.000");
        Date b = f.parse("2026/10/04 00:00:00.000");
        System.out.println("before     = " + a.before(b) + ", after = " + a.after(b)
                + ", compareTo = " + a.compareTo(b) + ", equals = " + a.equals(b));
    }
}

実行結果:

epoch      = Thu Jan 01 09:00:00 JST 1970
d          = Sat Oct 03 14:05:09 JST 2026
getTime()  = 1791003909123
format     = 2026/10/03 14:05:09.123
parse      = 1791003909123
曜日つき   = 2026年10月3日(土) 午後 2時5分
YEAR       = 2026
MONTH      = 9  (0始まり。10月は9)
DAY        = 3
DAY_OF_WEEK= 7  (日曜=1 … 土曜=7)
HOUR_OF_DAY= 14
30日後     = 2026/11/02 14:05:09.123
2か月前    = 2026/09/02 14:05:09.123
set        = 2026/10/03 14:05:09.000
before     = true, after = false, compareTo = -1, equals = false

読み取るポイントは2つです。

  • new Date(0L) が 09:00 と表示されるのは、UTC の 0時を日本時間で表示しているためです。
  • Calendar.MONTH は 0 始まり(10月が 9)、DAY_OF_WEEK は日曜が 1(土曜が 7)です。

Date/Calendar/java.sql/java.time を相互変換するコード

早見表の変換をすべて実行します。ミリ秒までの Date と、ナノ秒まで持つ Timestamp の違いも出力で確認できます。

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class Convert {
    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
        ZoneId tokyo = ZoneId.of("Asia/Tokyo");
        Date date = new Date(1791003909123L);
        System.out.println("元の Date                   : " + date);

        // --- Date → java.time ---
        Instant instant = date.toInstant();
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, tokyo);
        LocalDate ld = instant.atZone(tokyo).toLocalDate();
        ZonedDateTime zdt = instant.atZone(tokyo);
        System.out.println("Date → Instant              : " + instant);
        System.out.println("Date → LocalDateTime        : " + ldt);
        System.out.println("Date → LocalDate            : " + ld);
        System.out.println("Date → ZonedDateTime        : " + zdt);

        // --- java.time → Date ---
        System.out.println("Instant → Date              : " + Date.from(instant));
        System.out.println("LocalDateTime → Date        : " + Date.from(ldt.atZone(tokyo).toInstant()));
        System.out.println("LocalDate → Date            : " + Date.from(ld.atStartOfDay(tokyo).toInstant()));
        System.out.println("ZonedDateTime → Date        : " + Date.from(zdt.toInstant()));

        // --- Calendar ---
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        System.out.println("Calendar → Date             : " + cal.getTime());
        System.out.println("Calendar → LocalDateTime    : "
                + LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId()));
        System.out.println("Calendar → ZonedDateTime    : " + ((GregorianCalendar) cal).toZonedDateTime());
        GregorianCalendar fromZdt = GregorianCalendar.from(zdt);
        System.out.println("ZonedDateTime → Calendar    : " + fromZdt.getTime()
                + " / MONTH=" + fromZdt.get(Calendar.MONTH));

        // --- java.sql.Date(日付だけ) ---
        java.sql.Date sqlDate = java.sql.Date.valueOf(ld);
        System.out.println("LocalDate → sql.Date        : " + sqlDate);
        System.out.println("sql.Date → LocalDate        : " + sqlDate.toLocalDate());
        System.out.println("util.Date → sql.Date        : " + new java.sql.Date(date.getTime()));
        System.out.println("sql.Date → util.Date        : " + new Date(sqlDate.getTime()));

        // --- java.sql.Timestamp(日時、ナノ秒まで) ---
        LocalDateTime nano = LocalDateTime.of(2026, 10, 3, 14, 5, 9, 123456789);
        Timestamp ts = Timestamp.valueOf(nano);
        System.out.println("LocalDateTime → Timestamp   : " + ts);
        System.out.println("Timestamp → LocalDateTime   : " + ts.toLocalDateTime());
        System.out.println("Timestamp → Instant         : " + ts.toInstant());
        System.out.println("Instant → Timestamp         : " + Timestamp.from(instant));
        System.out.println("util.Date → Timestamp       : " + new Timestamp(date.getTime()));
    }
}

実行結果:

元の Date                   : Sat Oct 03 14:05:09 JST 2026
Date → Instant              : 2026-10-03T05:05:09.123Z
Date → LocalDateTime        : 2026-10-03T14:05:09.123
Date → LocalDate            : 2026-10-03
Date → ZonedDateTime        : 2026-10-03T14:05:09.123+09:00[Asia/Tokyo]
Instant → Date              : Sat Oct 03 14:05:09 JST 2026
LocalDateTime → Date        : Sat Oct 03 14:05:09 JST 2026
LocalDate → Date            : Sat Oct 03 00:00:00 JST 2026
ZonedDateTime → Date        : Sat Oct 03 14:05:09 JST 2026
Calendar → Date             : Sat Oct 03 14:05:09 JST 2026
Calendar → LocalDateTime    : 2026-10-03T14:05:09.123
Calendar → ZonedDateTime    : 2026-10-03T14:05:09.123+09:00[Asia/Tokyo]
ZonedDateTime → Calendar    : Sat Oct 03 14:05:09 JST 2026 / MONTH=9
LocalDate → sql.Date        : 2026-10-03
sql.Date → LocalDate        : 2026-10-03
util.Date → sql.Date        : 2026-10-03
sql.Date → util.Date        : Sat Oct 03 00:00:00 JST 2026
LocalDateTime → Timestamp   : 2026-10-03 14:05:09.123456789
Timestamp → LocalDateTime   : 2026-10-03T14:05:09.123456789
Timestamp → Instant         : 2026-10-03T05:05:09.123456789Z
Instant → Timestamp         : 2026-10-03 14:05:09.123
util.Date → Timestamp       : 2026-10-03 14:05:09.123

LocalDate → Date は「その日の 0時0分」になります(atStartOfDay(zone))。また java.sql.Date の表示は日付だけ、Timestamp は valueOf(LocalDateTime) で作るとナノ秒(.123456789)まで保たれ、Date から作るとミリ秒(.123)までになります。

java.sql.Date と Timestamp で知っておきたい2つの動き

import java.sql.Timestamp;
import java.util.Date;
import java.util.TimeZone;

public class SqlDateTrap {
    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));

        // 1. java.sql.Date は toInstant() が使えない
        java.sql.Date sqlDate = java.sql.Date.valueOf("2026-10-03");
        try {
            System.out.println(sqlDate.toInstant());
        } catch (UnsupportedOperationException e) {
            System.out.println("sqlDate.toInstant() → " + e);
        }
        // 直し方:toLocalDate() を使う
        System.out.println("toLocalDate()       → " + sqlDate.toLocalDate());

        // 2. Date と Timestamp の equals は向きで結果が変わる
        Date d = new Date(1791003909123L);
        Timestamp ts = new Timestamp(1791003909123L);
        System.out.println("d.equals(ts)   = " + d.equals(ts));
        System.out.println("ts.equals(d)   = " + ts.equals(d));
        System.out.println("d.compareTo(ts)= " + d.compareTo(ts));
        System.out.println("getTime 同士   = " + (d.getTime() == ts.getTime()));

    }
}

実行結果:

sqlDate.toInstant() → java.lang.UnsupportedOperationException
toLocalDate()       → 2026-10-03
d.equals(ts)   = true
ts.equals(d)   = false
d.compareTo(ts)= 0
getTime 同士   = true
動き 理由 こうするとよい
sqlDate.toInstant() が UnsupportedOperationException SQL の DATE には時刻の成分が無いため、常にこの例外を投げる仕様(公式Javadoc) toLocalDate() を使う
d.equals(ts) は true、ts.equals(d) は false Timestamp の equals は Timestamp 以外に対して true を返さない仕様(公式Javadoc) getTime() 同士、または compareTo で比べる

SimpleDateFormat を複数スレッドで共有すると結果が入れ替わる

SimpleDateFormat は、解析や整形の途中経過をインスタンス内の Calendar に書き込みながら処理します。そのため static final で1つだけ作り、複数スレッドから同時に呼ぶと、あるスレッドの途中経過に別のスレッドの値が混ざります。公式Javadoc にも「Date formats are not synchronized. It is recommended to create separate format instances for each thread.」と書かれています。

次のコードは、8スレッドで合計20,000回、「parse して format し直す」処理を行います。本来は入力と同じ文字列に戻るはずです。

import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Unsafe {
    // 1つのインスタンスを全スレッドで共有する
    static final SimpleDateFormat SHARED = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws Exception {
        String[] inputs = {"2026-10-03", "1999-01-31", "2000-02-29", "2024-12-25"};
        AtomicInteger ok = new AtomicInteger();
        AtomicInteger wrong = new AtomicInteger();
        Map<String, Integer> errors = new ConcurrentHashMap<>();
        Map<String, String> wrongSamples = new ConcurrentHashMap<>();

        ExecutorService pool = Executors.newFixedThreadPool(8);
        for (int i = 0; i < 20000; i++) {
            String in = inputs[i % inputs.length];
            pool.submit(() -> {
                try {
                    // parse して format し直すと、本来は入力と同じ文字列に戻る
                    String out = SHARED.format(SHARED.parse(in));
                    if (out.equals(in)) {
                        ok.incrementAndGet();
                    } else {
                        wrong.incrementAndGet();
                        if (wrongSamples.size() < 5) wrongSamples.put(in + " → " + out, "");
                    }
                } catch (Exception e) {
                    String key = e.getClass().getName() + ": " + e.getMessage();
                    errors.merge(key.length() > 70 ? key.substring(0, 70) + "…" : key, 1, Integer::sum);
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MINUTES);

        int errorCount = errors.values().stream().mapToInt(Integer::intValue).sum();
        System.out.println("正しい結果   : " + ok.get());
        System.out.println("誤った日付   : " + wrong.get());
        System.out.println("例外         : " + errorCount);
        System.out.println("--- 誤った日付の例 ---");
        wrongSamples.keySet().forEach(System.out::println);
        System.out.println("--- 例外の種類(件数の多い順に5つ) ---");
        errors.entrySet().stream()
                .sorted((a, b) -> b.getValue() - a.getValue())
                .limit(5)
                .forEach(e -> System.out.println(e.getValue() + "件 " + e.getKey()));
    }
}

実行結果(タイミングに左右されるため、件数と内容は実行のたびに変わります):

正しい結果   : 19572
誤った日付   : 411
例外         : 17
--- 誤った日付の例 ---
1999-01-31 → 2026-10-03
1999-01-31 → 2000-02-29
2024-12-25 → 2024-12-31
2026-10-03 → 1999-01-31
2024-12-25 → 1999-01-31
--- 例外の種類(件数の多い順に5つ) ---
6件 java.lang.NumberFormatException: For input string: ""
2件 java.lang.NumberFormatException: empty String
1件 java.lang.ArrayIndexOutOfBoundsException: Index -10 out of bounds for …
1件 java.lang.NumberFormatException: For input string: "E10"
1件 java.lang.NumberFormatException: For input string: "2244E40"

例外が出るのは全体のごく一部で、それよりずっと多い 411件が「例外にならず、別の日付になって返る」点が重要です。1999-01-31 → 2026-10-03 のように、他のスレッドが扱っていた日付がそのまま返ってきます。例外も NumberFormatException: For input string: "" や ArrayIndexOutOfBoundsException など、日付とは結び付きにくい種類です。

直し方:DateTimeFormatter に置き換える

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Safe {
    // 直し方1:DateTimeFormatter は不変なので static で共有できる
    static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("uuuu-MM-dd");
    // 直し方2:SimpleDateFormat をスレッドごとに1つ持つ
    static final ThreadLocal<SimpleDateFormat> TL =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    // 直し方3:共有したまま synchronized で1スレッドずつ使う
    static final SimpleDateFormat LOCKED = new SimpleDateFormat("yyyy-MM-dd");

    interface Converter { String apply(String in) throws Exception; }

    static void run(String label, Converter conv) throws Exception {
        String[] inputs = {"2026-10-03", "1999-01-31", "2000-02-29", "2024-12-25"};
        AtomicInteger ok = new AtomicInteger();
        AtomicInteger ng = new AtomicInteger();
        ExecutorService pool = Executors.newFixedThreadPool(8);
        for (int i = 0; i < 20000; i++) {
            String in = inputs[i % inputs.length];
            pool.submit((Callable<Void>) () -> {
                try {
                    if (conv.apply(in).equals(in)) ok.incrementAndGet(); else ng.incrementAndGet();
                } catch (Exception e) {
                    ng.incrementAndGet();
                }
                return null;
            });
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MINUTES);
        System.out.printf("%-32s 正しい結果=%d 誤り・例外=%d%n", label, ok.get(), ng.get());
    }

    public static void main(String[] args) throws Exception {
        run("DateTimeFormatter(共有)", in -> LocalDate.parse(in, DTF).format(DTF));
        run("ThreadLocal<SimpleDateFormat>", in -> TL.get().format(TL.get().parse(in)));
        run("synchronized + SimpleDateFormat", in -> {
            synchronized (LOCKED) { return LOCKED.format(LOCKED.parse(in)); }
        });
    }
}

実行結果:

DateTimeFormatter(共有)            正しい結果=20000 誤り・例外=0
ThreadLocal<SimpleDateFormat>    正しい結果=20000 誤り・例外=0
synchronized + SimpleDateFormat  正しい結果=20000 誤り・例外=0
方法 向いている場面 補足
DateTimeFormatter を static final で共有 新しく書くコード、置き換えられるコード 公式Javadoc に「This class is immutable and thread-safe.」と明記
ThreadLocal<SimpleDateFormat> Date を受け取るAPIを変えられないとき スレッドごとに1インスタンス
synchronized で囲む 呼び出し回数が少ない箇所 同時に1スレッドだけが使う

Date のまま受け渡す必要がある箇所は、LocalDateTime.ofInstant(date.toInstant(), zone).format(DTF) のように、境界で java.time に変換してから整形すると、共有のフォーマッタを安心して使えます。

つまずきやすい7つの動きを出力で確認する

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

public class Pitfalls {
    static final SimpleDateFormat OUT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws ParseException {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));

        System.out.println("[1] Calendar の月は0始まり");
        Calendar c = Calendar.getInstance();
        c.clear();
        c.set(2026, 10, 3);                       // 10 は「11月」
        System.out.println("set(2026, 10, 3)               → " + OUT.format(c.getTime()));
        c.set(2026, Calendar.OCTOBER, 3);         // 定数を使う
        System.out.println("set(2026, Calendar.OCTOBER, 3) → " + OUT.format(c.getTime()));
        c.set(2026, 10 - 1, 3);                   // または 月-1
        System.out.println("set(2026, 10 - 1, 3)           → " + OUT.format(c.getTime()));
        System.out.println("get(MONTH) + 1                 → " + (c.get(Calendar.MONTH) + 1));

        System.out.println("[2] Calendar は既定で範囲外の値を繰り上げる(lenient)");
        Calendar len = Calendar.getInstance();
        len.clear();
        len.set(2026, Calendar.FEBRUARY, 30);
        System.out.println("2月30日 (lenient=true)  → " + OUT.format(len.getTime()));
        Calendar strict = Calendar.getInstance();
        strict.clear();
        strict.setLenient(false);
        strict.set(2026, Calendar.FEBRUARY, 30);  // ここでは例外にならない
        try {
            strict.getTime();                     // 値を計算するときに検査される
        } catch (IllegalArgumentException e) {
            System.out.println("2月30日 (lenient=false) → " + e);
        }

        System.out.println("[3] SimpleDateFormat も既定で繰り上げる");
        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
        System.out.println("parse(\"2026-02-30\")   → " + OUT.format(f.parse("2026-02-30")));
        System.out.println("parse(\"2026-13-01\")   → " + OUT.format(f.parse("2026-13-01")));
        System.out.println("parse(\"2026-10-03abc\")→ " + OUT.format(f.parse("2026-10-03abc")));
        f.setLenient(false);
        try {
            f.parse("2026-02-30");
        } catch (ParseException e) {
            System.out.println("setLenient(false)     → " + e + " (errorOffset=" + e.getErrorOffset() + ")");
        }

        System.out.println("[4] yyyy で2桁の年を読むと紀元26年になる");
        System.out.println("yyyy/MM/dd で \"26/10/03\" → " + OUT.format(new SimpleDateFormat("yyyy/MM/dd").parse("26/10/03")));
        System.out.println("yy/MM/dd   で \"26/10/03\" → " + OUT.format(new SimpleDateFormat("yy/MM/dd").parse("26/10/03")));
        System.out.println("yy/MM/dd   で \"47/10/03\" → " + OUT.format(new SimpleDateFormat("yy/MM/dd").parse("47/10/03")));

        System.out.println("[5] YYYY(週の年)と yyyy(暦の年)");
        SimpleDateFormat in = new SimpleDateFormat("yyyy-MM-dd");
        for (String s : new String[] {"2026-12-26", "2026-12-27", "2026-12-31", "2027-01-01"}) {
            Date d = in.parse(s);
            System.out.println(s + " → yyyy-MM-dd=" + new SimpleDateFormat("yyyy-MM-dd").format(d)
                    + " / YYYY-MM-dd=" + new SimpleDateFormat("YYYY-MM-dd").format(d));
        }

        System.out.println("[6] MM(月)と mm(分)、HH(0-23)と hh(1-12)、dd(日)と DD(年内通算日)");
        Date d = OUT.parse("2026-10-03 14:05:09");
        System.out.println("yyyy-MM-dd HH:mm:ss → " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d));
        System.out.println("yyyy-mm-dd HH:MM:ss → " + new SimpleDateFormat("yyyy-mm-dd HH:MM:ss").format(d));
        System.out.println("yyyy-MM-dd hh:mm:ss → " + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(d));
        System.out.println("yyyy-MM-DD HH:mm:ss → " + new SimpleDateFormat("yyyy-MM-DD HH:mm:ss").format(d));

        System.out.println("[7] 月末の加算と add / roll");
        Calendar e = Calendar.getInstance();
        e.clear();
        e.set(2026, Calendar.JANUARY, 31);
        e.add(Calendar.MONTH, 1);
        System.out.println("1/31 に add(MONTH, 1)        → " + OUT.format(e.getTime()));
        e.add(Calendar.MONTH, 1);
        System.out.println("さらに add(MONTH, 1)         → " + OUT.format(e.getTime()));
        Calendar r = Calendar.getInstance();
        r.clear();
        r.set(2026, Calendar.DECEMBER, 31);
        r.roll(Calendar.DAY_OF_MONTH, 1);
        System.out.println("12/31 に roll(DAY_OF_MONTH,1)→ " + OUT.format(r.getTime()));
        r.set(2026, Calendar.DECEMBER, 31);
        r.add(Calendar.DAY_OF_MONTH, 1);
        System.out.println("12/31 に add(DAY_OF_MONTH,1) → " + OUT.format(r.getTime()));

    }
}

実行結果:

[1] Calendar の月は0始まり
set(2026, 10, 3)               → 2026-11-03 00:00:00
set(2026, Calendar.OCTOBER, 3) → 2026-10-03 00:00:00
set(2026, 10 - 1, 3)           → 2026-10-03 00:00:00
get(MONTH) + 1                 → 10
[2] Calendar は既定で範囲外の値を繰り上げる(lenient)
2月30日 (lenient=true)  → 2026-03-02 00:00:00
2月30日 (lenient=false) → java.lang.IllegalArgumentException: MONTH: 1 -> 2
[3] SimpleDateFormat も既定で繰り上げる
parse("2026-02-30")   → 2026-03-02 00:00:00
parse("2026-13-01")   → 2027-01-01 00:00:00
parse("2026-10-03abc")→ 2026-10-03 00:00:00
setLenient(false)     → java.text.ParseException: Unparseable date: "2026-02-30" (errorOffset=10)
[4] yyyy で2桁の年を読むと紀元26年になる
yyyy/MM/dd で "26/10/03" → 0026-10-03 00:00:00
yy/MM/dd   で "26/10/03" → 2026-10-03 00:00:00
yy/MM/dd   で "47/10/03" → 1947-10-03 00:00:00
[5] YYYY(週の年)と yyyy(暦の年)
2026-12-26 → yyyy-MM-dd=2026-12-26 / YYYY-MM-dd=2026-12-26
2026-12-27 → yyyy-MM-dd=2026-12-27 / YYYY-MM-dd=2027-12-27
2026-12-31 → yyyy-MM-dd=2026-12-31 / YYYY-MM-dd=2027-12-31
2027-01-01 → yyyy-MM-dd=2027-01-01 / YYYY-MM-dd=2027-01-01
[6] MM(月)と mm(分)、HH(0-23)と hh(1-12)、dd(日)と DD(年内通算日)
yyyy-MM-dd HH:mm:ss → 2026-10-03 14:05:09
yyyy-mm-dd HH:MM:ss → 2026-05-03 14:10:09
yyyy-MM-dd hh:mm:ss → 2026-10-03 02:05:09
yyyy-MM-DD HH:mm:ss → 2026-10-276 14:05:09
[7] 月末の加算と add / roll
1/31 に add(MONTH, 1)        → 2026-02-28 00:00:00
さらに add(MONTH, 1)         → 2026-03-28 00:00:00
12/31 に roll(DAY_OF_MONTH,1)→ 2026-12-01 00:00:00
12/31 に add(DAY_OF_MONTH,1) → 2027-01-01 00:00:00
番号 起きること こうするとよい
1 set(2026, 10, 3) が 11月3日になる Calendar.OCTOBER などの定数、または「月 - 1」で指定。取り出しは get(MONTH) + 1
2 2月30日が 3月2日に繰り上がる setLenient(false) にすると getTime() の時点で IllegalArgumentException
3 parse("2026-02-30") が通り、末尾の abc も無視される setLenient(false) で ParseException。末尾の abc は設定に関係なく読み飛ばされるので、parse(text, ParsePosition) の getIndex()(この例では 10)と文字列の長さを比べる
4 yyyy で 26/10/03 を読むと紀元26年 2桁の年は yy。yy は「生成時点の80年前〜20年後」に収まる世紀に解釈される(47 → 1947)
5 YYYY だと 2026-12-27 が 2027-12-27 YYYY は週の年。暦の年は小文字の yyyy
6 mm と MM、hh と HH、DD と dd の取り違え 月は MM、分は mm、24時間制は HH、日は dd
7 1/31 の1か月後は 2/28、さらに1か月後は 3/28 月末を保ちたいときは getActualMaximum(DAY_OF_MONTH) で月末日を設定し直す。roll は上の位を変えない

5番の YYYY は、年末年始の数日だけ結果が変わるため、ふだんのテストでは見つけにくい取り違えです。今回の実行では 2026-12-27(日曜)から YYYY の出力が 2027 になりました。

よくある質問

Q. Date は非推奨ですか? A. Date クラス自体は非推奨ではありません。非推奨なのは getYear() や new Date(年, 月, 日) など、年月日を直接扱う一部のメソッドとコンストラクタです。年月日の取り出しは Calendar か java.time で行います。

Q. Date を LocalDateTime にするとき、なぜ ZoneId が必要ですか? A. Date は世界共通の「瞬間」で、LocalDateTime は「どこかの地域のカレンダーと時計の値」だからです。同じ瞬間でも東京では 14:05、UTC では 05:05 になるため、どの地域の表示にするかを ZoneId で指定します。

Q. SimpleDateFormat をローカル変数で毎回作れば共有の問題は起きませんか? A. 起きません。メソッド内で作ったインスタンスは他のスレッドから見えないためです。上と同じ20,000回の処理を毎回 new する形でも実行し、すべて正しい結果になりました。

まとめ

  • Date はミリ秒、Calendar は暦の計算、SimpleDateFormat は文字列変換を担当する
  • Date ⇔ Instant は直接、LocalDateTime へは ZoneId を指定して変換する
  • java.sql.Date は LocalDate、Timestamp は LocalDateTime/Instant と valueOf・toLocalDate などで行き来できる
  • SimpleDateFormat を共有すると、例外よりも「別の日付が返る」件数のほうが多い(今回の実行では 411件と17件)
  • DateTimeFormatter は不変でスレッドセーフなので static final で共有できる
  • Calendar の月は0始まり、既定は lenient。setLenient(false) で範囲外の日付を検出できる
  • yyyy と YYYY、MM と mm、HH と hh は意味が異なるので、パターンは小さなコードで出力を見てから使う

参考・出典

  • Java SE 21 API ドキュメント「SimpleDateFormat」(Synchronization、パターン文字、2桁の年の解釈) https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/text/SimpleDateFormat.html
  • Java SE 21 API ドキュメント「DateTimeFormatter」(スレッドセーフの記述) https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/format/DateTimeFormatter.html
  • Java SE 21 API ドキュメント「java.sql.Date」(toInstant の仕様) https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/java/sql/Date.html
  • Java SE 21 API ドキュメント「java.sql.Timestamp」(equals の注意書き) https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/java/sql/Timestamp.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Date・Calendar・SimpleDateFormatの使い方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?