9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaAdvent Calendar 2023

Day 6

[初心者向け] 日付の取り扱いでビックリした話

Last updated at Posted at 2023-12-05

はじめに

  • 本来はあまり発生するような話ではないので、読み物としてお楽しみいただければと思います。

ある日の会社でのできごと

後輩 「UTCからJSTに変換する際に、冬場に該当する月(12月~3月)だと正しく変換できない。JavaのDate().getTimezoneOffset()を使っている。なんでDateだとうまくいかないのかは不明!」

我 「(今時、java.util.Date使ってることなんてあるのかな…いにしえのコードなのか?はたまた海の外から納品されたレアなパターン踏んじゃったのかな...) 理由があることをきちんと説明しないといけない!(謎の使命感)」

日付の取り扱い(歴史)

Java8以前の話

日付を取り巻くクラス.png

  • 日付を表すのに、java.util.Date 、計算するために、java.util.Calendar を使っていました。
    • java.util.Date

    • 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以降の話

日付を取り巻くクラス_2.png

  • 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 さん、ありがとうございます。

  • 1.5.0
    java_5.jpg

windows + r、cmdでプロンプトでないんですね…(commandでした)

  • 1.8.0_211
    java_1_8.png

  • 20
    java_20.png

JShellはやっぱり便利ですね…

9
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?