Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Java】Javaの日時/日付に関するAPI(Java7以前)

システムと時刻

地球上に存在する多くのシステムやアプリケーションは、「時刻」という概念に依存します。例えばRDBテーブル上のレコードが更新日時を持つ、WebアプリケーションクライアントがHTTPリクエストを投げた時刻サーバに伝えるなど、例を上げればきりがありません。時刻に関する処理はプログラムの中で記述される機会が極めて高く、時刻を扱うAPIについて熟知しておくことは大きな武器になると考えられます。今回はシステム開発で頻繁に用いられるJavaの日時APIに焦点を絞り、その特徴を調査しつつ纏めたいと思います。

Java7までの日時API

Java7までは、主に以下三つのクラスが日付を扱える代表的なAPIでした。

  • Date
  • Calendar
  • SimpleDateFormat
  • TimeZone

Java8が既に出ている今日では、これらのクラスが提供する機能をTime APIでほぼ互換可能であるため、積極的に使うべきではないという位置づけになっています。それでなくとも、各クラスの実装は幾つかの問題をはらんでいるため、優先的に使用すべきではありません。ただし、既に使われているサーバミドルウェアフレームワーク等がJava7(最悪の場合、それよりもっと古いバージョン)を利用しなければならない場合は、これらのクラスを細心の注意を払いながら使っていくことになります。

Date

Dateは長い間使われているにも関わらず、多くの問題を持つ時刻管理クラスの一つです。Dateのオブジェクトは日付情報をミリ秒精度のUNIXTIMEとして保持します。

// オブジェクトの生成(現在時刻)
Date date = new Date(); // Wed Mar 28 13:23:00 GMT 2018

// 年(!!getYearは非推奨!!)
int year = date.getYear(); // 118
        
// 月(!!getMonthは非推奨!!)
int month = date.getMonth(); // 2 

// 日(!!getDayは非推奨!!)
int day = date.getDay(); // 28

Dateオブジェクトは、標準出力によって日付表記となります。 上記の出力だけみても、

  • getYear()の返戻値が1900からの経過年数として出力される
  • getMonth()の返戻値が表す月数は0開始

という粗悪な特徴を持つことが見て取れます。これらのget関数は今では非推奨となっていますが、getMonthに関してはDateのWrapperクラスであるCalendarですら同じ挙動を示します。

またDateはタイムゾーン情報を持ちません。Date.toStringによって文字列の日時情報を参照した場合、表示される時間はシステムのタイムゾーンに依存します。getDateなどの日付情報を取得するメソッドも同様です。

加えて、Date単体ではメソッドを介しての日時の加減算は不可能であり(set系統も非推奨であるため)、後述するCalendarクラスを使用する必要があります。にもかかわらず、オブジェクトフィールドを直接書き換えることができますが、通常は避けるべきです。

個人的な意見ですが、Date型の運用する際は一貫してUNIXTIME情報を保持させることを徹底すべきです。タイムゾーン情報を持たないため、Date単体での年月日などの情報の更新・取得を行うと時差の問題が発生する可能性があります。日時の更新・取得には後述のCalendarを使うか、Java8であればtime API中のクラスの利用を検討したほうが良いでしょう。

Calendar

CalendarはDateのWrapperクラスであり、Dateの持つ幾つかの問題を解消するのに役立ちます。

// Calendarオブジェクト
Calendar calendar = Calendar.getInstance();

// タイムゾーン指定
calendar.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));

// set
// 年
calendar.set(Calendar.YEAR, 2017);
// 月(0開始)
calendar.set(Calendar.MONTH, 10);
// 日
calendar.set(Calendar.DATE, 12);
// 時(24h)
calendar.set(Calendar.HOUR_OF_DAY, 16);
// 分
calendar.set(Calendar.MINUTE, 0);
// 秒
calendar.set(Calendar.SECOND, 0);

// まとめて指定
calendar.set(2017, 10, 12, 16, 0, 0);

// get
int year = calendar.get(Calendar.YEAR); // 2017
int month = calendar.get(Calendar.MONTH); // 10
int day = calendar.get(Calendar.DATE); // 12
int hour = calendar.get(Calendar.HOUR_OF_DAY); // 16
int minute = calendar.get(Calendar.MINUTE); // 0
int second = calendar.get(Calendar.SECOND); // 0

// Calendar -> Date
Date date = calendar.getTime(); // Sun Nov 12 07:00:00 GMT 2017

// add 
calendar.add(Calendar.YEAR, 1);
calendar.add(Calendar.MONTH, 1);
calendar.add(Calendar.DATE, 1);
calendar.add(Calendar.HOUR_OF_DAY, 1);
calendar.add(Calendar.MINUTE, 1);
calendar.add(Calendar.SECOND, 1);

// Calendar -> Date
date = calendar.getTime(); // Thu Dec 13 08:01:01 GMT 2018

// Date -> Calendar
Date now = new Date(); // Wed Mar 28 15:00:01 GMT 2018
calendar.setTime(now);

Dateほどではありませんが、日付APIとしての使いづらさが目立ちます。

  • 相変わらずgetMonthで取得できる月数は0開始
  • getsetaddなどの日付操作の殆どにフラグ指定を要する
  • 日付情報をオブジェクト生成時に設定できないため、毎回setterを呼ぶ必要がある

Calendarで日付を管理する場合、可能であればタイムゾーンを明示的に指定するのが望ましいです。タイムゾーンをシステムの稼働環境に合わせたい場合もTimeZone.getDefault()を渡しておくと丁寧で良いかもしれません。

SimpleDateFormat

SimpleDateFormatは、日付フォーマットのパース・フォーマッティングをタイムゾーンを考慮しながら行います。例えば2018-02-03という文字列を日本時間としてDateに変換したり、逆にDateの持つUNIXTIMEからyyyy/MM/ddのフォーマットに即したGMT時刻文字列を生成することもできます。

// 現在時刻
Date date = new Date(); // Wed Mar 28 15:37:48 GMT 2018

// SimpleDateFormatオブジェクト
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// タイムゾーン
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
// Date -> 文字列
String formattedDate = sdf.format(date.getTime()); // 2018-03-29 00:37:48

String dateStr = "2018-03-03 10:10:00";
// 文字列 -> Date
Date parsedDate = sdf.parse(dateStr); // Sat Mar 03 01:10:00 GMT 2018

文字列とUNIXTIMEの変換では、特に注意を払う必要があります。タイムゾーンの指定は必ず行うようにしましょう。文字列からDateを生成するときも、Dateから文字列を生成するときも、SimpleDateFormatではTimeZone クラスを用いてタイムゾーンを指定できます。

タイムゾーンを指定しないとどうなるのでしょうか。日本時間の2018-03-29T00:30:00(ISO8601形式ですが、あえて時差を除いています)を表すミリ秒精度のUNIXTIME1522251000000は、タイムゾーンGMTとして解釈すると2018-03-28T13:30:00となってしまいます。逆に、日本時間の2018-03-29T00:30:00GMT2018-03-29T00:30:00は文字列としては同じですが、UNIXTIME上では1522251000000(日本)と1522218600000GMT)のように全く異なります。このように、タイムゾーンはいわば時刻変換におけるプロトコルとして機能しているので、変換時には明示的に指定すべきです。

Java7までの日時APIのなかでは比較的良心的なこのクラスですが、そもそも文字列形式の時刻表記とUNIXTIMEを相互に変換する行為自体に落とし穴があるため、バグの温床となりやすいです。Java8(TimeAPI)においてもこの問題に対しては発生しうるので、気をつけたいところです。

TimeZone

Calendar・SimpleDateFormat仕様におけるタイムゾーン指定子として重要な役割を持ちますが、それ以上に特筆する項目は無いと思われます。Java8ではこれに変わるZoneId、そしてZoneOffsetクラスが存在し、timeAPIではタイムゾーン指定子としてこれらを使うことになります。

まとめ

今回はJava7以前における日時に関するAPIについてまとめました。結構個人の思うところが露出してしまっている気もしますが、「Jaav8環境ではTimeAPIを優先して使う」「Java7以前の環境ならば十分注意して使用すべきである」点は概ね正しいと思われます。Java8 time APIについても纏めてみようと思います。

参考文献

Date (Java Platform SE 8)

Calendar (Java Platform SE 7)

java.time (Java Platform SE 8)