はじめに
- 本来はあまり発生するような話ではないので、読み物としてお楽しみいただければと思います。
ある日の会社でのできごと
後輩 「UTCからJSTに変換する際に、冬場に該当する月(12月~3月)だと正しく変換できない。JavaのDate().getTimezoneOffset()を使っている。なんでDateだとうまくいかないのかは不明!」
我 「(今時、java.util.Date使ってることなんてあるのかな…いにしえのコードなのか?はたまた海の外から納品されたレアなパターン踏んじゃったのかな...) 理由があることをきちんと説明しないといけない!(謎の使命感)」
日付の取り扱い(歴史)
Java8以前の話
- 日付を表すのに、
java.util.Date
、計算するために、java.util.Calendar
を使っていました。-
java.util.Date
-
Java 日付
でググると先頭に出てくる代表的なクラス。特定のタイミングの日時を表現しているクラス。- これがウワサの 一番最初に出てくるJava8のDateクラスのjavadoc
- JDK1.0でリリースされ、大半のコンストラクタやメソッドがJDK1.1で非推奨になってしまったという悲しい歴史を持つクラスです。(会話の中に出てきた、
getTimeZoneOffset()
も含まれます。)- こちらの記事がとても勉強になります。
- JDK1.0でリリースされ、大半のコンストラクタやメソッドがJDK1.1で非推奨になってしまったという悲しい歴史を持つクラスです。(会話の中に出てきた、
- これがウワサの 一番最初に出てくるJava8のDateクラスのjavadoc
-
-
java.sql.
パッケージ- PreparedStatementで利用。
- あと、ORマッパー等でも利用。
-
java.sql.Date
- 日付を取り扱いたい場合に利用。
-
java.sql.Time
- 時刻を取り扱いたい場合に利用。
-
java.sql.Timestamp
- 日時を取り扱いたい場合に利用。
-
java.util.Calendar
- 日付の演算、取得、比較等、日付の操作で利用する。
-
java.text.SimpleDateFormat
- 日付のフォーマットと解析を行う。
-
JDK1.0は1996年、JDK1.1は1997年にリリースされています。
改善されるきっかけになるJava8は2014年…
Java8以降の話
-
JSR 310: Date and Time API がリリースされ、
新しいAPIの最終的な目標は、シンプルに使えることである。
とあるように、日付の取り扱いがずいぶん楽になりました。-
楽になったポイントの一例
- 年月日を指定してインスタンスを作るのが楽になった。
- Calendarクラスの月は0始まり(ここ、一度は誰もがハマったはず)だったのが、1始まりになった。
- 配列利用での利便性を考え、0始まり(月そのものではなく、インデックス)だった。
- 日付の計算のためにCalendarクラスを使う必要がなくなった。
-
java.time.LocalDate
- 日付を表すクラス。 (年、月、日 (yyyy-MM-dd))
-
java.time.LocalTime
- 時間を表すクラス。 (時、分、秒、ナノ秒 (HH-mm-ss-ns))
-
java.time.LocalDateTime
- 日付と時刻の両方を表すクラス。 (yyyy-MM-dd-HH-mm-ss-ns) ※タイムゾーンなし
-
java.time.ZonedDateTime
- 日付と時刻の両方を表すクラス。 (yyyy-MM-dd-HH-mm-ss-ns) ※タイムゾーンあり
-
(説明だけだと分かりづらいので)実際のコード
システム日付取得
-
Java8以前
// 引数なしのコンストラクタは非推奨にはなっていない Date systemDate = new Date(); // このまま出力しちゃうと、日付以外の情報も出ちゃう ex> Sun Dec 06 22:18:35 JST 2023 System.out.println(systemDate); // yyyy-MM-dd等、フォーマットした状態で出したい場合はひと手間必要。 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // これでようやく2023-12-06が出力される System.out.println(sdf.format(systemDate);
-
Java8以降
// これだけ!行数半分! LocalDate systemDate = LocalDate.now(); System.out.println(systemDate);
指定した日付でのインスタンス生成
-
Java8以前
// Dateクラスにも年月日指定をできるコンストラクタは存在しますが、非推奨なのでCalendarを利用する。 Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, 2023); // 月は0始まりなので、1引かないといけない calendar.set(Calendar.MONTH, 11); calendar.set(Calendar.DAY_OF_MONTH, 6); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); System.out.println(sdf.format(calendar.getTime()));
-
Java8以降
// 12月が12なのがありがたい… LocalDate targetDate = LocalDate.of(2023, 12, 6); System.out.println(targetDate);
日付の加算・減算
-
Java8以前
Date now = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(now); // 1か月前 calendar.add(Calendar.MONTH, -1); // 1日後 calendar.add(Calendar.Date, 1);
-
Java8以降
LocalDate now = LocalDate.now(); // 1か月前 now.minusMonths(1); // 1日後 now.plusDays(1);
文字列から日付を生成
-
Java8以前
// parseメソッドはParseExceptionをthrowするので例外処理の対処が必要です。 SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); Date date = sdf.parse("2023/12/06"); System.out.println(sdf.format(date));
SimpleDateFormatはスレッドセーフでないので、利用する際は注意が必要。(意図しないところで値が書き換えられる可能性があります。)
-
Java8以降
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); LocalDate targetDate = LocalDate.parse("2023/12/0", formatter); System.out.println(targetDate);
DateTimeFormatterはスレッドセーフです。
後輩とのその後
java.util.Dateの非推奨メソッドである旨と、java.text.ZonedDateTimeの利用で解消する旨を簡単に説明し、下記のソースコードを提示しました。
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ZoneDateTimeTest {
public static void main(String[] args) {
// 元のUTCの時刻文字列
String utcTime = "2023-09-30T15:00:00.000+0000";
// ZonedDateTimeでparseできるフォーマット(この形じゃないとできない)★今回、ここでちょっとハマったのはここだけの話。
String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS+SSSS";
// パース出来るフォーマットでUTCの時刻をJSTの時刻に変換
ZonedDateTime utc = ZonedDateTime.parse(time, DateTimeFormatter.ofPattern(dateFormat).withZone(ZoneId.of("UTC")));
ZonedDateTime jst = utc.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("utc=" + utc);
System.out.println("jst=" + jst);
// 標準出力の結果は下記になる
// utc=2023-09-30T15:00Z[UTC]
// jst=2023-10-01T00:00+09:00[Asia/Tokyo]
}
}
帰ってきた返信は…
「色々と確認ありがとうございます!実はJavaではなくjavascriptでした。Javaと似ていて混乱してしまいました。」
問題のjavascriptですが某SaaSサービスが提供しているものでしたが、確かにメソッド名とか、Javaっぽかったですが…
~しばしの沈黙~
あれ、Javaの話じゃなかったのかよ…(なんか私滑ってない?
なんか滑って終わる感じがなんともですが、歴史を再認識できてよかったことにしたいです。
[2023/12/06 18:49 追記]
※今回はこのソースコードを使う場面が出てこなかったので事なきを得ましたが、java.timeパッケージのjavadoc に「可能であれば、タイムゾーンがない単純なクラスを使用することをお薦めします。 タイムゾーンを広範に使用すると、アプリケーションがかなり複雑になる傾向があります。」とあるので、安易に使う事はおすすめしません。(@skrb さん、ありがとうございました。)
安易に使うと呪われます。
呪われる件については、下記がとても勉強になりました。
タイムゾーン呪いの書 (知識編)
タイムゾーン呪いの書 (実装編)
タイムゾーン呪いの書 (Java 編)
まとめ
- 日付の操作は、
Date and Time API
を使いましょう。 - なんかおかしいな、と思ったら非推奨かどうかを確認しましょう。(今回は、そもそも…の話ではありましたが。)
おまけ(日付の話で思い出したことがあったので。)
昔(おそらくJava5くらいのころ)に、java.util.Date
のcompareToメソッドを使って、java.sql.Timestamp
と比較したところ、Timestampの秒以下が無視されて、意図しない結果になったことがあったなと思い出して、試してみたところ。20で実行すると思った結果が返ってきてびっくりしました。
System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(20000)));
System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(10999)));
これも8で動きが変わったのかと思いつつ、いつ時点で治ったのか…(1.8.0_211時点では治っていない…)
[2023/12/06 17:24 追記] 9で修正が入ったようです。@ktgw0316 さん、ありがとうございます。
windows + r、cmdでプロンプトでないんですね…(commandでした)
JShellはやっぱり便利ですね…