
JavaのDateとは?特定タイミングの日時を表現するクラス「Date」を解説
Javaのjava.util.Dateは、特定タイミングの日時を表現しているクラスです。ここで言う特定タイミングの日時とは、何年何月何日・何時何分何秒・何ミリ秒という特定の一瞬、その日時のことです。
コンピュータは全てを数字で扱いますが、日時も例外ではありません。コンピュータが日時を表す時は、特定日時からの差分、つまり経過時間で表します。この考え方の正しい理解が、Dateを使いこなすカギなのです。
実はDateは、Javaで日時を扱うにはもう古い方法です。Java 12となった今では、Java 8で追加されたDate and Time APIのクラスが標準です。でも、Dateはこれからもあり続けますし、使い方を知ることは大切です。
この記事では、Javaで日時を表すDateについて、初心者向けに説明します。Dateを扱う上で必要な考え方や使い方、実務でDateを使うならぜひ押さえておきたいプログラミング上のノウハウも満載しています。
※この記事のサンプルは、Java 12の環境で動作確認しています
目次
1.Dateとはそもそもナニモノか
では、まずはDateの考え方を学びましょう。Dateの使い方をすぐにでもお伝えしたいのですが、Dateの考え方が理解できていないと、なぜそのような使い方になるのか、いざ使う時に混乱してしまうかもしれません。
ですから、少々退屈かもしれませんが、Dateとはどういうものなのかの説明にしばらくお付き合いください。でも、他のプログラミング言語での日時の扱いでも似た考え方をするので、決して無駄にはなりませんよ!!
1-1.Dateは特定タイミングの日時を表すもの
Dateは、特定タイミングの日時を表すクラスです。この文章を書いているのは、西暦2019年6月6日23時40分28秒くらい(ミリ秒の単位は分かりません…)で、この「瞬間」の日時をミリ秒までで表現するのがDateです。
そして、Dateはミリ秒までの具体的な日時を必ず持っているモノです。人間の感覚での2019年6月6日23時40分28秒「くらい」ではなく、2019年6月6日23時40分28秒123ミリ秒というように、ミリ秒まではっきりと、です。
1-2.【重要】Dateが持つのは1970/01/01 00:00:00(GMT)からの経過ミリ秒
コンピュータで日時を表現する方法には、大きく分けると以下の方法があります。Dateは前者で日時を表現するものです。
- 特定日時からの経過時間(差分)の整数で表す
- 年月日時分秒などのフィールドごとの数値を持つ一つのデータ構造で表す
- 決まった形式の文字列や数字で持っておいて、必要な時に解析して使う
Dateでは、経過時間の単位はミリ秒(以下、ms)です。経過時間の基準日時は、西暦1970年1月1日0時0分0秒(GMT)です。この経過ミリ秒をエポック(epoch)ミリ秒と呼ぶことがあり、秒の場合はUNIX時間とも呼ばれます。
例として、1,000は1970年1月1日0時0分1秒0、1,559,832,028,123は2019年6月6日23時40分28秒123です。ちなみに、1分は60,000ms、1時間は3,600,000ms、1日は86,400,000ms、365日は31,536,000,000msです。
Dateでの経過時間はlongで表します。例のように、大きな桁の整数になるからです。intでは±21億程度の数字を表現できますが、それでも桁があまりに小さすぎ、1970年1月26日5時31分23秒647までしか表せません。
1-2-1.なぜ経過時間で表現するのか?
コンピュータでは基準日時からの経過時間で日時を表現する方法がよく見られます。つまり、1000などの具体的な整数を、日時として内部で使うのです。これは、整数で表現されている方が、処理を高速化できるからです。
経過時間の整数にすれば、日時に関する計算はすべて整数の足し算や引き算になるので、コンピュータがとても高速に処理できます。その代わり、人間視点では直感的には分かりづらいものになってしまいますけれども。
なお、UNIXのいわゆる2038年問題も、基準日時からの経過秒の差分で日時を表現しているから起きるものです。UNIX時間は秒単位ですが、それでもいつかは経過時間の数値型での最大値まで到達することがあるのです。
でも、先ほどお伝えしたとおり、JavaのDateは64bitのlongで差分を表現しています。longの最大値では、292,278,994年くらいまで表現できますので、まあ、人類が生き残っていそうな間は安心して使えそうですね!!
1-3.基準日時より以前はマイナスの経過ミリ秒で表現する
先ほどの例では、基準日時より未来の日時としましたが、もちろん基準日時より前の日時も表せます。基準日時以前は、マイナスの経過時間で表現します。つまり、-86,400,000は1969年12月31日0時0分0秒0となります。
1-4.Dateそのものには年月日やタイムゾーンの概念はない
ここまで説明してきたことからお分かりいただけたかもしれませんが、Dateそのものには年月日やタイムゾーンの考え方はありません。Dateが持っているのは、基準日時からの経過ミリ秒数だけです。
ですので、Dateだけでは、Dateが表現する日時が何年何月何日なのか、何曜日なのか、あるタイムゾーンでは何時になるか、などの処理は行えません。経過ミリ秒からの計算は一応できますが、その計算はかなり面倒です。
そんな日時関連の処理は、Dateとは別のクラスが担当します。例えば、java.util.Calendarや、java.text.SimpleDateFormatなどです。ですから、Javaでの日時処理は、いくつかのクラスを組み合わせて行うものなのです。
1-5.Dateが持つ経過ミリ秒は変更できない
Dateのインスタンスが持つ経過ミリ秒は、後から変更ができません。つまり、一回作ったDateの日時は固定されています。これをDateはイミュータブル(Immutable、変更できない)である、とも言うことがあります。
ですので、特定の日時を表すDateに対して、例えばその1日後を表すDateが必要になったとすれば、新しいDateをその都度作り直すことになります。
これも、Dateが持つ経過ミリ秒へ足し算引き算をすれば計算はできますが、少々分かりづらいですよね。先ほど少し名前の出たクラスCalendarは、そんなよくある日時の計算に使うメソッドを持っていたりもします。
2.Dateの基本的な使い方
ここまで、Dateの前提知識のご説明にお付き合いいただき、ありがとうございました。ここからは、さっそくDateの使い方を紹介していきます。
2-1.Dateのインスタンスの作り方
Dateのインスタンスの作り方にはいくつかの方法があります。ここでは、それぞれを説明していきます。
2-1-1.Dateのデフォルトコンストラクタで現在日時のDateを作る
Dateのデフォルトコンストラクタを使ってインスタンスを作ると、現在日時を持ったDateが得られます。この現在日時は、Javaを動かしたパソコンやサーバに設定されている現在日時です。
Date d = new Date(); System.out.println(d); // → Fri Jun 07 01:28:25 GMT+09:00 2019
2-1-2.Dateの引数ありコンストラクタで指定した日時のDateを作る
Dateのlongを引数に取るコンストラクタを使ってインスタンスを作ると、1970/1/1 00:00:00から指定しただけの経過時間を持つDateを作れます。このlongは、ずっとお話してきた通り、経過時間のミリ秒表現です。
Date d = new Date(1000 * 60 * 60 * 24); // 1日分の経過ミリ秒(86400000ms)を指定してDateを作ると… System.out.println(d); // → Fri Jan 02 09:00:00 GMT+09:00 1970、1日後になった
2-1-3.DateFormat.parseで、日時を表す文字列からDateを作る
Dateは文字列からも作れます。その際には、java.text.DateFormatのサブクラスである、java.text.SimpleDateFormatのメソッドparseを使うのが普通です。parseは、解析するといった意味の言葉です。
DateFormat (Java SE 12 & JDK 12)
https://docs.oracle.com/javase/jp/12/docs/api/java.base/java/text/DateFormat.html
SimpleDateFormat (Java SE 12 & JDK 12)
https://docs.oracle.com/javase/jp/12/docs/api/java.base/java/text/SimpleDateFormat.html
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSSXXX"); Date d = df.parse("2019/06/06 23:40:28.123+00:00"); System.out.println(d); // → Fri Jun 07 08:40:28 GMT+09:00 2019
日時の文字列の書式は、SimpleDateFormatのコンストラクタの引数で指定します。日本でプログラムを作るなら、以下のどれかか、組み合わせればOKでしょう。時分秒やミリ秒が不要なら、その部分を削除します。
書式文字列 | 解釈できる日時の例 |
yyyyMMddHHmmssSSS | 20190102123456123 |
yyyy/MM/dd HH:mm:ss.SSS | 2019/01/02 12:34:56.123 |
yyyy-MM-dd HH:mm:ss.SSS | 2019-01-02 12:34:56.123 |
yyyy/MM/dd HH:mm:ssXXX | 2019/01/02 12:34:56+09:00 |
yyyy-MM-dd’T’HH:mm:ssXXX | 2019-01-02T12:34:56+09:00 |
なお、時分秒やミリ秒の値が未指定なら、その部分の値は0になります。つまり、値“2019/01/01”を、書式“yyyy/MM/dd”でparseして作ったDateは、2019/01/01 00:00:00.000を持つDateになる、ということです。
SimpleDateFormatで指定できる書式の詳細は、後の章でもう少しだけ細かくお伝えします。完全な書式の仕様については、SimpleDateFormatのJavadocを参照してください。
なお、SimpleDateFormatが日時を解析する際のタイムゾーンを意識すべき場合があります。それについては後述しますが、解析結果が予期せぬ日時になる時は、書式の間違いか、タイムゾーンを疑いましょう。。
2-1-4.Calendar.getTimeで、Calendarが持つ日時からDateを作る
java.util.Calendarも、Javaで日時を扱うためのクラスの一つです。Calendar.getTimeで、Calendarが持つ日時をDateとして取り出せます。
Calendar (Java SE 12 & JDK 12)
https://docs.oracle.com/javase/jp/12/docs/api/java.base/java/util/Calendar.html
Calendar c = Calendar.getInstance(); // 現在日時を持つCalendarを生成して、 Date d = c.getTime(); // Calendarが持つ日時をDateに変換すると、 System.out.println(c); // → java.util.GregorianCalendar[time=1559839894431,~(以下、長いので割愛) System.out.println(d); // → Fri Jun 07 01:51:34 GMT+09:00 2019
Calendarのインスタンスに日時を設定した後にgetTimeすれば、必要な日時を持ったDateを自由に作れます。
Calendar c = Calendar.getInstance(); c.set(Calendar.YEAR, 1970); c.set(Calendar.MONTH, 0); // 月は0始まり、1月にするなら0から。あるいは↓のようにする // c.set(Calendar.MONTH, Calendar.JANUARY); c.set(Calendar.DAY_OF_MONTH, 1); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); Date d = c.getTime(); // System.out.println(d); // → Thu Jan 01 00:00:00 GMT+09:00 1970
ただし、後述しますが、SimpleDateFormatと同様に、タイムゾーンについては要注意です。どんなプログラミング言語で日時を扱う時でも、タイムゾーンあるいは時差の考え方は、少々分かりづらいものです。
2-1-5.Date.fromで、Instantが持つ日時からDateを作る
Java 8以降で使える新しい日時の標準APIには、ある特定の瞬間を表すjava.time.Instantというクラスがあります。このInstantからも、Date.fromを使ってDateのインスタンスを作れます。
Instant i = Instant.now(); // 現在の日時のInstantを作って、 Date d = Date.from(i); // そのInstantからDateを作る System.out.println(i); // → 2019-06-06T16:58:32.055471300Z System.out.println(d); // → Fri Jun 07 01:58:32 GMT+09:00 2019
あるいは、Instant.toEpochMilliを使って経過ミリ秒のlongに変換し、そのlongでDateのコンストラクタを呼び出してもOKです。
Instant i = Instant.now(); Date d = new Date(i.toEpochMilli()); System.out.println(i); // → 2019-06-06T16:59:38.521718400Z System.out.println(d); // → Fri Jun 07 01:59:38 GMT+09:00 2019
2-1-6.Dateのdeprecatedなコンストラクタは使わない
DateのJavadocには、ここで紹介したデフォルトコンストラクタ、longを引数に取るコンストラクタの他にも、年月日、時分秒などを指定できるコンストラクタがいくつかオーバーロードされています。
Date d = new Date(2019, 1, 2, 12, 34, 56); System.out.println(d); // → Sun Feb 02 12:34:56 GMT+09:00 3919
これらのコンストラクタは、使うことが推奨されていませんので(deprecated)、使わないことをお勧めします。過去のプログラムとの互換性のためにまだ残されてはいますが、いつ削除されてもおかしくはないものです。
Dateは基準日時からの経過時間を持つだけのものなので、年月日時分秒などの視点での生成・操作が必要な場合は、JavaではCalendarを使います。
なお、Javaは、deprecatedなもの(クラス、メソッドなど)を、過去からの互換性を維持するために残す方針を続けてきました。でも、最近ではそういうレガシーなものが削除される例が出てきています。気を付けましょう。
2-2.経過ミリ秒の取得
Dateのインスタンスから経過時間のミリ秒を取得するには、Date.getTimeを呼び出します。すると、その時間を持ったlongが戻ってきます。基準日時より過去の日時を表すDateなら、マイナスのlongが戻ります。
Date d = new Date(); long time = d.getTime(); System.out.println(time); // → 1559864448025
経過時間のミリ秒を直接取り出して何か意味があるのか?と思われたかもしれませんが、日時の数値を基にして計算したい時などに、結構使ったりするものです。
2-3.Date同士の比較
Date同士の比較は、日時を扱うプログラムでは頻繁に行います。Dateは比較のためのインターフェイスComparableを実装しているので、そのcompareToを使ってもいいですし、専用の比較用メソッドも用意されています。
2-3-1.Dateの大小関係とはどういうものか
日時の大小と、未来と過去を紐付けるときは、少し混乱しがちです。Dateでは、以下のルールとなっています。
- Date1 < Date2 → Date1はDate2よりも過去である
- Date1 == Date2 → Date1とDate2は同じ日時である
- Date1 > Date2 → Date1はDate2よりも未来である
つまり、基準日時からの経過時間が大きい方が未来、小さい方が過去になります。これはDateが実際に持つ値が、経過時間のlongであると知っていると、理解・納得しやすいでしょう。
要注意なのは、Dateが持っているミリ秒単位の日時で比較されることです。比較するDate同士で1ミリ秒違っただけでも結果の大小に反映されますので、おかしな結果になるように見えるなら疑ってもいいポイントです。
なお、実際のところ、Dateのソースコードでも以下のようにlong同士の単純な大小比較をしているだけだったりします。実に簡単なロジックで高速に動くでしょうが、前述した注意をしなければならない理由でもあります。
// Java 12のソースコードより抜粋 public int compareTo(Date anotherDate) { long thisTime = getMillisOf(this); long anotherTime = getMillisOf(anotherDate); return (thisTime<anotherTime ? -1 : (thisTime==anotherTime ? 0 : 1)); }
2-3-2.Dateの大小関係をcompareToで確認する
では、Date.compareToを使って大小判断をしてみます。Comparable.compareToのルールに従って、小さい(-1以下)、等しい(0)、大きい(1以上)のintが戻ってきます。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d1 = df.parse("2019/06/06 12:34:56.789"); // ベースとする日時 Date d2 = df.parse("2019/06/06 12:35:56.789"); // d1より1分未来の日時 Date d3 = df.parse("2019/06/06 12:34:56.789"); // d1と同じ日時 int d1_d2 = d1.compareTo(d2); int d2_d1 = d2.compareTo(d1); int d1_d3 = d1.compareTo(d3); System.out.println(d1_d2); // → -1、d1 は d2 より過去の日時 System.out.println(d2_d1); // → 1、d2 は d1 より未来の日時 System.out.println(d1_d3); // → 0、d1 と d3 の日時は同じ
ここで、==や>、<などで比較してはいけません。==はインスタンスが同じか確認する演算子なので、インスタンスが持つ日時で比較しません。そして、>や<は数値用の比較演算子なので、Dateへは使えません。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d1 = df.parse("2019/06/06 12:34:56.789"); Date d2 = df.parse("2019/06/06 12:34:56.789"); // d1と同じ日時だが、d1とは違うDateのインスタンス System.out.println(d1 == d2); // → false、日時は同じだけれども、違うインスタンスのため System.out.println(d1.compareTo(d2)); // → 0、インスタンスは違うが、同じ日時のため
なお、Dateが持つ日時を、年月日だけなどで大小比較したいこともあります。その場合はDateだけでは簡単にはできませんので、CalendarやSimpleDateFormatを組み合わせるといいでしょう。やり方は後述します。
2-3-3.Dateの大小関係をDate.before/after/equalsで確認する
compareToの結果は数値なので、人間向けにはちょっと不親切です。数字の読み違いでバグを作ってしまうかもしれません。過去、未来、同じという判断をメソッドで表現できれば、もう少し読みやすくなりますよね。
そのためのメソッドが、Date.before/after/equalsです。結果はすべてbooleanで戻ります。ですので、例えばif文で使えば、compareToの結果を数値で判断するよりも、プログラムが直感的になるでしょう。
- before → 自身が引数のDateよりも過去ならtrue、そうでなければfalse
- equals → 自身が引数のDateと同じ日時ならtrue、そうでなければfalse
- after → 自身が引数のDateよりも未来ならtrue、そうでなければfalse
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d1 = df.parse("2019/06/06 12:34:56.789"); Date d2 = df.parse("2019/06/06 12:35:56.789"); // d1の1分未来の日時 Date d3 = df.parse("2019/06/06 12:34:56.789"); // d1と同じ日時 boolean d1_before_d2 = d1.before(d2); boolean d2_before_d1 = d2.before(d1); boolean d1_after_d2 = d1.after(d2); boolean d2_after_d1 = d2.after(d1); boolean d1_equals_d3 = d1.equals(d3); System.out.println(d1_before_d2); // → true、d1 は d2 より過去の日時である System.out.println(d2_before_d1); // → false、d2 は d1 より過去の日時ではない System.out.println(d1_after_d2); // → false、d1 は d2 より未来の日時ではない System.out.println(d2_after_d1); // → true、d2 は d1 より未来の日時である System.out.println(d1_equals_d3); // → true、d1 と d3 の日時は同じである
2-3-4.Dateそのものを、あるいはDateをからめてソートする
DateはComparableを実装していますので、java.util.Collections.sortやjava.util.Arrays.sortなどで、そのままソートできます。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d1 = df.parse("2019/06/06 12:34:56.789"); Date d2 = df.parse("2019/06/06 12:35:56.789"); // d1の1分未来の日時 Date d3 = df.parse("2019/06/06 12:36:56.789"); // d2の1分未来の日時 List<Date> dateList = Arrays.asList(d3, d2, d1); // 未来→過去の順にListを作って、 Collections.sort(dateList); // Listをソートすると dateList.forEach(System.out::println); // Listの内容が過去→未来の順になる Date[] dateArray = { d3, d2, d1 }; // 未来→過去の順に配列を作って、 Arrays.sort(dateArray); // 配列をソートすると Arrays.stream(dateArray).forEach(System.out::println); // 配列の内容が過去→未来の順になる
フィールドとしてDateを持つクラスのソート条件に、Dateのフィールドを加えたいなら、以下のようにcompareToで指定するか、java.util.Comparatorを使えばいいでしょう。
import java.util.Date; class DateCompareSample implements Comparable<DateCompareSample> { int intField; Date dateField; public int compareTo(DateCompareSample obj) { int ret = Integer.compare(intField, obj.intField); if (ret != 0) { return ret; } return dateField.compareTo(obj.dateField); } }
Comparator<DateCompareSample> comparator = new Comparator<DateCompareSample>() { public int compare(DateCompareSample obj1, DateCompareSample obj2) { int ret = Integer.compare(obj1.intField, obj2.intField); if (ret != 0) { return ret; } return obj1.dateField.compareTo(obj2.dateField); } };
3.Dateのいろいろな使い方
ここでは、実際のプログラムでDateを使う場合によく出てくるものを紹介します。Dateはこういう感じで使えますよ、と言うサンプルとしてもご覧ください。
3-1.ログへの日時の追加
ごく身近な使い方として、ログへの日時を追加するのに使います。何かの処理の途中経過を示すログ出力をする時に、ログを出力した日時があることは、デバッグ上とても意味があることです。
といってもやり方はごく簡単で、ログに出したい文字列へDateを結合すればいいだけです。ごく単純にやるなら以下のとおりです。例ではSystem.outを使っていますが、普通はファイルなどの出力ストリームになります。
Date d1 = new Date(); System.out.println(d1 + ": 処理XXXXが始まりました"); // → Sat Jun 08 21:34:53 GMT+09:00 2019: 処理XXXXが始まりました // 何かの処理 for (long i = 0; i < 10000000000L; i++) { ; } Date d2 = new Date(); System.out.println(d2 + ": 処理XXXXが終わりました"); // → Sat Jun 08 21:34:56 GMT+09:00 2019: 処理XXXXが終わりました
ただ、これだとちょっと読みづらいですよね。あと、プログラムを実行すると、1秒以内で処理が終わってしまうこともごく普通なので、その場合は実行状況を把握するログとしては、少々物足りない内容です。
ですから、SimpleDateFormat.formatを組み合わせて、人が読みやすい形式・必要な日時の精度を持つよう、日時を文字列に変換するのが普通です。SimpleDateFormat.formatの使い方は、後でご紹介します。
3-2.処理時間の計測
Dateは、プログラムの処理時間を計測するのにもよく使います。Dateはミリ秒まで持っていますので、普通のプログラムの処理時間計測には十分な精度です。
やり方は簡単で、処理の前後でDateのインスタンスを生成し、経過時間のlong値の引き算をするだけです。
Date start = new Date(); // 処理開始日時 // 何かの長い処理 for (long i = 0; i < 10000000000L; i++) { ; } Date end = new Date(); // 処理終了日時 long diff = end.getTime() - start.getTime(); // 処理終了日時 - 処理開始日時 System.out.println("処理時間は " + diff + "ms です"); // → 処理時間は 5019ms です
もし、マイクロ秒やナノ秒単位の測定をしたいなら、残念ながらミリ秒までしか持たないDateでは実現できません。その場合は、java.time.Instantなどの、Java 8以降で使える新しいクラスを使いましょう。
Instant start = Instant.now(); // 処理開始日時 // 何かの長い処理 for (long i = 0; i < 10000000000L; i++) { ; } Instant end = Instant.now(); // 処理終了日時 // 処理終了日時 - 処理開始日時(ナノ秒単位) long diff = (end.getEpochSecond() * 1000000000 + end.getNano()) - (start.getEpochSecond() * 1000000000 + start.getNano()); System.out.println("処理時間は " + diff + "ns です"); // → 処理時間は 5014588300ns です
3-3.特定期間の加算・減算
繰り返しですが、Dateは基準日時からの経過時間しか持っていません。その機能しかないクラスです。ですから、特定期間…つまり分・時間・日・年などの加減算ができる直接的なメソッドは持っていません。
Dateだけでそのようなことをやるなら、経過時間への加算・減算を行って、その経過時間から新しいDateを作る必要があります。計算をする時は、経過時間はミリ秒だということを意識しましょう。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/01/01 00:00:00.000"); // 2019/1/1のDateを作って、 long time = d.getTime(); // 経過時間を取得して、 // 分・時間・日・月・年の経過時間をミリ秒単位で計算して、 long min = 1000 * 60; long hour = min * 60; long day = hour * 24; long month = day * 31; long year = day * 365; // それぞれ足し合わせると、日時が進んでいるDateを作れる System.out.println(d); // → Tue Jan 01 00:00:00 GMT+09:00 2019 System.out.println(new Date(time + min)); // → Tue Jan 01 00:01:00 GMT+09:00 2019 System.out.println(new Date(time + hour)); // → Tue Jan 01 01:00:00 GMT+09:00 2019 System.out.println(new Date(time + day)); // → Wed Jan 02 00:00:00 GMT+09:00 2019 System.out.println(new Date(time + month)); // → Fri Feb 01 00:00:00 GMT+09:00 2019 System.out.println(new Date(time + year)); // → Wed Jan 01 00:00:00 GMT+09:00 2020
ただし、これは単に機械的に計算した結果でしかありません。例えば何かのDateを翌月1日にする、というような処理はなかなかに大変です。なぜなら、実際には月ごとの日数や閏年を考慮する必要があるからです。
そんなことをやるために、Dateとは別にjava.util.Calendarがあります。Calendarの簡単な使い方と、Dateとの連携のさせ方は、後の章でもご説明します。
3-4.【参考】DateからInstantを取得する
Date.fromではInstantからDateを作れますが、逆にDate.toInstantではDateからInstantを作れます。Dateはミリ秒までですので、当然作ったInstantのマイクロ秒・ナノ秒は0になります。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSSXXX"); Date d = df.parse("2019/01/01 00:00:00.000+00:00"); Instant i = d.toInstant(); System.out.println(i); // → 2019-01-01T00:00:00Z
4.Calendar/SimpleDateFormatとの連携
Dateを使う時は、java.util.Calendarとjava.text.SimpleDateFormatと組み合わせることが大変多いです。これらのクラスを組み合わせてよくやることを、いくつか紹介します。
4-1.【重要】DateをCalendarにする/CalendarをDateにする
まず、CalendarとDateを連携させるために、Date→Calendar、Calendar→Dateの変換の仕方を覚えましょう。といっても、今までの例でも出てきていますね。以下のようにCalendar.getTime/setTimeで行います。
Date→Calendar:
Date d = new Date(); Calendar c = Calendar.getInstanct(); c.setTime(d);
Calendar→Date:
Calendar c = Calendar.getInstanct(); Date d = c.getTime();
なお、Calendarのインスタンスはnewでは作れません。Calendar.getInstanceで現在日時のCalendarを取得し、そのインスタンスにいろいろと設定していく形になります。
4-2.【重要】SimpleDateFormatで、Dateを文字列とし、解析をする
Dateの作り方のところで、SimpleDateFormat.parseを使って、文字列からDateを作りました。その逆、つまりDateから文字列を作るには、SimpleDateFormat.formatを使います。
Date d = new Date(); // YYYY/MM/DD HH:MM.SS.SSS形式のSimpleDateFormatを作って、 SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); // Dateをフォーマットした文字列を作る String dateStr = df.format(d); System.out.println(dateStr); // → 2019/06/08 21:51:25.710、指定した書式の出力になる System.out.println(d); // → Sat Jun 08 21:51:25 GMT+09:00 2019、Dateは同じ日時である Date d2 = df.parse(dateStr); // 同じSimpleDateFormatで文字列をparseすると、 System.out.println(d2); // → Sat Jun 08 21:51:25 GMT+09:00 2019、同じ日時のDateになる
このように、SimpleDateFormatは、文字列の解析とフォーマットの両方に使える大変便利なクラスなのです。
4-2-1.SimpleDateFormatでよく使う書式文字列
SimpleDateFormatでよく使う書式文字列は以下のものです。書式文字列では、特定のアルファベットおよびその数が書式上で意味を持ちます。書式文字列の詳細は、SimpleDateFormatのJavadocで確認してください。
- yyyy → 年(year)の4桁0埋め表現(0000~9999)、例:2019
- MM → 月(Month)の2桁0埋め表現(01~12)、例:01、12
- dd → 日(day)の2桁0埋め表現(00~31)、例:09、31
- HH → 時(Hour)の24時間表記の2桁0埋め表現(00~23)、例: 03、12、23
- mm → 分(minute)の2桁0埋め表現(00~59)、例: 00、09、30、55
- ss → 秒(second)の2桁0埋め表現(00~59)、例: 01、10、45、59
- SSS → ミリ秒の3桁0埋め表現(000~999)、例: 008、012、123
- XXX → 時差の表現、+HH:mmや-HH:mm形式の文字列など、例: +09:00、-09:30
これ以外のアルファベットも意味を持ちますが、書式としては使わず、単に文字列として出力に混ぜ込みたいなら”でくくります。アルファベット以外の文字(/、–、空白など)はくくる必要はなく、そのまま書けます。
なお、解析する文字列が書式文字列どおりであれば解析できるので、欧米などでよく見られるMM/dd/yyyy形式などに対応したいなら、そのとおりに書式文字列を作ればOKです。
4-3.Dateから年月日・時刻・曜日などを抽出する
基準日時からの経過時間しか持っていないDateだけでは、年月日・時分秒・曜日の抽出をするのは難しいです。もちろん、計算すればできないことはありませんが、結構な量の計算が必要です。
この場合は、CalendarあるいはSimpleDateFormatなどを使って、一応は以下のように必要な情報を抜き出すことはできます。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/01/02 12:34:56.789"); Calendar c = Calendar.getInstance(); c.setTime(d); int year = c.get(Calendar.YEAR); int month = c.get(Calendar.MONTH); int day = c.get(Calendar.DAY_OF_MONTH); int hour = c.get(Calendar.HOUR_OF_DAY); int min = c.get(Calendar.MINUTE); int sec = c.get(Calendar.SECOND); int ms = c.get(Calendar.MILLISECOND); int weekday = c.get(Calendar.DAY_OF_WEEK); System.out.println(year); // → 2019 System.out.println(month); // → 0 (Calendarでは月は0始まりのため、1月は0になる) System.out.println(day); // → 2 System.out.println(hour); // → 12 System.out.println(min); // → 34 System.out.println(sec); // → 56 System.out.println(ms); // → 789 System.out.println(weekday); // → 4 (2019/1/2は水曜日、Calendarでは4が戻る(日曜日が1))
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/01/02 12:34:56.789"); SimpleDateFormat df1 = new SimpleDateFormat("yyyy"); SimpleDateFormat df2 = new SimpleDateFormat("MM"); SimpleDateFormat df3 = new SimpleDateFormat("dd"); SimpleDateFormat df4 = new SimpleDateFormat("HH"); SimpleDateFormat df5 = new SimpleDateFormat("mm"); SimpleDateFormat df6 = new SimpleDateFormat("ss"); SimpleDateFormat df7 = new SimpleDateFormat("SSS"); SimpleDateFormat df8 = new SimpleDateFormat("u"); System.out.println(df1.format(d)); // → "2019" System.out.println(df2.format(d)); // → "01" System.out.println(df3.format(d)); // → "02" System.out.println(df4.format(d)); // → "12" System.out.println(df5.format(d)); // → "34" System.out.println(df6.format(d)); // → "56" System.out.println(df7.format(d)); // → "789" System.out.println(df8.format(d)); // → "3" (1の月曜日から始まる)
4-4.Dateからの月末月初を計算する
月末月初の計算とは、例えば 2019/06/10 12:34:56.789という日時から、月末と月初の日付がいつかを調べることです。例では、月末は2019/6/30で、月初は2019/6/1です。
普通の月なら、月末は月を見れば30/31日を判断できますが、2月はうるう年で28/29日が違いますよね。そのプログラミングは少々面倒ですし、実はうるう年の正しい計算方法を知らない人もいて、危なっかしいです。
このような処理は、DateからCalendarを作り、Calendar.getActualMaximum/getActualMinimumを使うことで簡単に行えます。これらは、Calendarが持つフィールドで、あり得る値の最大・最小値を戻すメソッドです
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/06/10 12:34:56.789"); Calendar c = Calendar.getInstance(); c.setTime(d); // フィールドDAY_OF_MONTH(日)が取り得る、最大値と最小値を取得する System.out.println(c.getActualMaximum(Calendar.DAY_OF_MONTH)); // → 30 System.out.println(c.getActualMinimum(Calendar.DAY_OF_MONTH)); // → 1
当然ながら、うるう年を意識した処理もきちんとしてくれますよ。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2000/02/10 12:34:56.789"); Calendar c = Calendar.getInstance(); c.setTime(d); // 2000年はうるう年なので、2月の月末は29日になる System.out.println(c.getActualMaximum(Calendar.DAY_OF_MONTH)); // → 29 System.out.println(c.getActualMinimum(Calendar.DAY_OF_MONTH)); // → 1
4-5.ジャスト0分などに日時を切り捨て・切り上げしたDateを作る
Dateはミリ秒までの日時を持っていますが、ミリ秒は不要だったり、日付はそのままに0時0分0秒にしたいこともあります。その他にも、次の秒や分に進めたい時もあるでしょう。その場合も、Calendarなどでできます。
4-5-1.切り捨て
Dateを一旦Calendarに変換し、切り捨てしたいフィールド以下へ、Calendar.setで0を設定します。
例えば、0時0分ジャストにしたいなら、Calendarの時・分・秒・ミリ秒のフィールドを0にします。分以下、秒以下なら、そのフィールドからにすればOKです。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/06/06 12:34:56.789"); Calendar c = Calendar.getInstance(); c.setTime(d); // 時・分・秒・ミリ秒を0にする c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); Date d2 = c.getTime(); System.out.println(d2); // → Thu Jun 06 00:00:00 GMT+09:00 2019、0時0分0秒になった
あるいは、一旦文字列に変換して解析し直す、という手も使えないことはありません。書式文字列に含まれていないフィールドは0になるからです。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/06/06 12:34:56.789"); // 時分秒を0にするので、年月日だけの文字列に一旦して、 SimpleDateFormat df2 = new SimpleDateFormat("yyyyMMdd"); String dateStr = df2.format(d); // その年月日の文字列を再度parseすれば、 Date d2 = df2.parse(dateStr); System.out.println(d2); // → Thu Jun 06 00:00:00 GMT+09:00 2019、0時0分0秒になった
なお、ミリ秒だけいらないなら、経過時間のミリ秒を1000で割って、また1000を掛けるという、お手軽な計算での手段も一応できます。使う機会はないと思いますが…。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/06/06 12:34:56.789"); long time = (long)(d.getTime() / 1000) * 1000; Date d2 = new Date(time); System.out.println(df.format(d2)); // → 2019/06/06 12:34:56.000
4-5-2.切り上げ
いろいろと方法はありますが、せっかく切り捨てを紹介しましたので、それを活用してみましょう。つまり、切り上げしたいフィールド未満を切り捨てした後に、対象のフィールドをCalendar.addで+1します。
以下の例では、秒を切り上げして、次の分にしています。もし59分だったとすれば、分を+1すれば、時間も当然自動的に+1されます。文字列操作だと、こうは簡単にはいきませんね。
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); Date d = df.parse("2019/06/06 12:34:56.789"); Calendar c = Calendar.getInstance(); c.setTime(d); // 秒の単位で切り上げするので、秒・ミリ秒を0にして、 c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); // その後、分を+1する c.add(Calendar.MINUTE, 1); Date d2 = c.getTime(); System.out.println(d2); // → Thu Jun 06 12:35:00 GMT+09:00 2019、35分になった
なお、本来の切り上げ処理なら、この例だと秒かミリ秒が1以上かをチェックする必要があるでしょう。そのチェック処理の追加は、皆さんへお任せします。
また、Calendar.addでマイナスの数値を指定すれば、その分だけ対象のフィールドが前になります。そして、年月日へも当然使えますので、便利に使ってください。
4-6.【重要】タイムゾーンを意識した処理をする
ここまでずっとお話してきた通り、Dateは基準日時からの経過時間を持っているクラスです。Dateの基準日時はGMT、いわゆる協定世界時のUTC+0で、どこか別の地域のタイムゾーンではありません。
Date自身が持っている日時はGMTです。今までの例で、Dateをprintした時に“GMT+09:00”が一緒に出ていますが、これは私のパソコンの環境が日本時間(UTC+9)で、単にそのタイムゾーンを出力しているだけです。
ですので、画面上にprintする時や文字列を解析してDateを作る時など、タイムゾーンを意識しなければならないことがあります。
4-6-1.Calendarでタイムゾーンを設定する
CalendarをgetInstanceすると、システムのタイムゾーンを持ったCalendarが作られます。前述のとおり、私のパソコンは日本時間なので、Calendarも現地時間はUTCから9時間進んでいるとみなして処理をします。
Calendar c = Calendar.getInstance(); System.out.println(c.getTimeZone().getRawOffset()); // → 32400000、+9時間のミリ秒表現
私の環境でDateをCalendarに渡して日時を取得すると、UTC+0から9時間進んだ日時が得られます。繰り返しですが、DateそのものはGMTなので、GMTから現地時間への解釈・変換をCalendarが行っているのです。
// GMTで1970/1/1 00:00:00のDateを作っても、 Date d = new Date(0); // 1970/1/1 00:00:00.000(GMT) System.out.println(d.getTime()); // → 0 // Calendarからは、Calendarのタイムゾーンに準じた日時が得られる Calendar c = Calendar.getInstance(); c.setTime(d); System.out.println(c.get(Calendar.HOUR_OF_DAY)); // → 9時、0時ではない
ですから、Calendarのタイムゾーンを切り替えれば、同じ日時を表すDateであっても、別のタイムゾーンでの日時を取得できます。
// GMTで1970/1/1 00:00:00のDateを作っても、 Date d = new Date(0); // 1970/1/1 00:00:00.000(GMT) System.out.println(d.getTime()); // → 0 // Calendarのタイムゾーンを切り替えれば、 Calendar c = Calendar.getInstance(); c.setTime(d); c.setTimeZone(TimeZone.getTimeZone("GMT-9")); // CalendarのタイムゾーンをGMT-9に変更 // Calendarから取得できる日時は、全てタイムゾーンに準じたものになっている System.out.println(c.get(Calendar.HOUR_OF_DAY)); // → 15時、つまり0時(24時) - 9時間 System.out.println(c.get(Calendar.DAY_OF_MONTH)); // → 31 System.out.println(c.get(Calendar.MONTH)); // → 11(12月のこと) // でも、Calendarが持っている日時そのものはGMT Date d2 = c.getTime(); System.out.println(d2.getTime()); // → 0、DateそのものはGMT System.out.println(c.getTimeInMillis()); // → 0、Calendarが認識している経過時間もGMT
4-6-2.SimpleDateFormatでタイムゾーンを設定する
SimpleDateFormatもCalendarと同じで、それ自身がタイムゾーンを持っています。こちらも、何もしなければシステムのタイムゾーンです。ですので、出力や解析はシステムのタイムゾーンに準拠したものになるのです。
// GMTで1970/1/1 00:00:00のDateを作っても、 Date d = new Date(0); // 1970/1/1 00:00:00.000(GMT) System.out.println(d.getTime()); // → 0 // SimpleDateFormatの出力はシステムのタイムゾーンを考慮した結果になる SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); System.out.println(df.format(d)); // → 1970/01/01 09:00:00.000、9時間進んでいる
SimpleDateFormatが持つタイムゾーンを変えるには、Calendarと同じようにタイムゾーンを指定します。
Date d = new Date(0); // 1970/1/1 00:00:00.000(GMT) System.out.println(d.getTime()); // → 0 // SimpleDateFormatのタイムゾーンを変更すれば、そのタイムゾーンの出力になる SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); df.setTimeZone(TimeZone.getTimeZone("GMT-9")); // SimpleDateFormatのタイムゾーンをGMT-9に変更 System.out.println(df.format(d)); // → 1970/01/01 00:00:00.000
4-6-3.日時文字列を解析する時は、タイムゾーンを意識しよう
この節で見てきたように、Javaでの日時の根本的な表現方法はGMTです。その日時がプログラムの外からどう見えるかは、システムのタイムゾーンや、CalendarやDateFormatterのタイムゾーンに依存します。
ただ、プログラム外部から取り込んだ情報、特に文字列として取り込んだ日時が、どのタイムゾーンで表現されているものかは意識する必要があります。そうしないと、Dateへ変換した時に時刻がずれるからです。
特に文字列を日時として解釈する時、タイムゾーンを表すものが付いていれば、それに準じて解釈すればいいだけです。タイムゾーンがついていないなら、どのタイムゾーンかをきちんと確認して指定しましょう。
// この文字列は実はGMTなのだけれども、 String dateStr = "1970/01/01 00:00:00"; // タイムゾーンを意識しないで、システムのデフォルト(UTC+9)で解析すると、 SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Date d = df.parse(dateStr); // DateのGMTでは9時間前の日時になり、文字列の日時からずれてしまう System.out.println(d.getTime()); // → -32400000、9時間前
// 文字列にタイムゾーンが付いていて、 String dateStr = "1970/01/01 00:00:00+00:00"; // 解析する書式へもタイムゾーンを指定できていれば、 SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ssXXX"); Date d = df.parse(dateStr); // Dateでの日時もずれない System.out.println(d.getTime()); // → 0、0時0分0秒なので、文字列と合っている
// あるいは、GMTの日時文字列に対して、 String dateStr = "1970/01/01 00:00:00"; // タイムゾーンをプログラム上で明示的に指定できていれば、 SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); df.setTimeZone(TimeZone.getTimeZone("GMT")); Date d = df.parse(dateStr); // Dateでの日時もずれない System.out.println(d.getTime()); // → 0、0時0分0秒なので、文字列と合っている
5.【参考】Date and Time APIの簡単な紹介
Java 8より、Date and Time APIという、日時を取り扱う新しいAPIが追加されました。パッケージとしてjava.timeが新設されています。
java.time (Java SE 12 & JDK 12)
https://docs.oracle.com/javase/jp/12/docs/api/java.base/java/time/package-summary.html
これにより、今までのjava.util.Dateやjava.util.Calendar、java.text.DateFormatは古いAPIということになりました。もちろんこれからも使えますが、機能追加・強化は、新APIの方が優先されていくでしょう。
ここでは、この新しいAPIに接するに当たり、今までのDateとどういう違いがあるのかを簡単にお伝えします。
5-1.Date/Calendarしかないことの問題点
今までのAPIはDateやCalendarがその中心にありました。ですので、Date/Calendarで保持している日時をどう扱うか、というAPIになっています。ですが、いくつかの問題が昔から指摘されていました。
その一つに、Dateは持っている日時の単位が常にミリ秒なので、用途によってはメソッドやメソッドの戻り値の意味があいまいになってしまう、ということがあります。これはCalendarでも同じことです。
例えば、メソッドの引数がDateだとして、そのDateは年月日だけ必要なのか、それともミリ秒まで必要なのかはわかりません。Javadocがあればまだマシですが、なければお手上げなので、ソースを読む必要があります。
Dateでは問題なのでStringやintを使ったとしても、メソッド宣言上はどんなString/intでもコンパイルエラーにはならないので、“YYYY/MM/DD”を想定したメソッドだとしても、メソッド内でのチェックが必要です。
5-2.日時の種類がクラスになって明確に!!
結局、問題は日時の役割をDateとCalendarだけでカバーしようとしたことです。ですので、Date and Time APIでは、用途別のクラスで役割分担をすることになりました。それが従来のものから大きく変わったところです。
また、クラスが多くなったのですが、それぞれのクラスの操作をするメソッド名には統一されたルールがあり、意味が分かりやすくなっています。これは、同じくJava 7にNIO.2で追加されたクラスにも共通した特徴です。
なお、ここで紹介するクラスは全て不変(Immutable)、かつスレッドセーフです。
5-2-1.日時のクラス
日時を直接表すクラスとしては、以下のものが用意されています。
- java.time.Instant:タイムスタンプのようなもの。意味的にはこれが従来のDateに一番近い?
- java.time.LocalTime:時刻だけを持つクラス。年月日の情報は持たない。
- java.time.LocalDate:日付(年月日)だけを持つクラス。時分秒以下の情報はもたない。
- java.time.LocalDateTime:日時を持つクラス。ただし、タイムゾーンや時差の情報は持たない。
- java.time.ZonedDateTime:日時と紐付くタイムゾーンを同時に持つクラス。
- java.time.OffsetDateTime:日時と紐付く時差を同時に持つクラス。
ここでZonedDateTimeとOffsetDateTimeは同じものに見えますが別物です。タイムゾーンと時差は、実は少々違う考え方だからです。ここでは詳細には踏み込みませんので、興味がある方は調べてみてもいいでしょう。
5-2-2.期間のクラス
新しい種類のデータ型として、期間を表現できるクラスが二つできました。
- java.time.Duration:期間をナノ秒単位で表現できるクラス。
- java.time.Period:期間を、何年・何ヶ月・何日など、人間が分かりやすい形で表現したクラス。
5-2-3.年月日・曜日・時差のクラス
年・月・日・曜日・時差なども、それぞれクラスとして扱えるようになりました。これでメソッドの引数や戻り値の意味を、より明確にできるようになりました。
- java.time.Year:年
- java.time.Month:月
- java.time.YearMonth:年月
- java.time.MonthDay:月日
- java.time.DayOfWeek:曜日
- java.time.OffsetTime:時差のオフセット時間(+01:00など)
5-3.日時用のフォーマット・解析用クラスが新しくできた
今までのDateFormat(SimpleDateFormat)に代わり、java.timeにあるクラス用のフォーマット・解析用クラスjava.time.format.DateTimeFormatterが新しく出来ました。従来のものより機能が強化されています。
そして、例えばISO 8601などの各種標準に則った書式用のフォーマッタが、あらかじめいくつか用意されています。ですから、SimpleDateFormatのように、自分で書式文字列を考えなくてもすむことが多いでしょう。
また、DateTimeFormatterはスレッドセーフです。そのため、今までのSimpleDateFormatのように、不具合回避のために無駄にnewする必要もありません。最初に一つ作っておくか、もうあるものを使えばいいのです。
5-4.日時の精度がナノ秒単位になった
地味に機能強化されたポイントは、各種日時の精度の仕様が、今までのミリ秒(1/1000秒)からナノ秒(1/1000000000秒)になったことです。
実際のところ、ナノ秒までの精度があったとしても、普通のプログラムではまず使いません。ですが、プログラミング言語の仕様上できるということと、できないということの間には、天と地ほどの差があります。
それに、そういう時間の精度が必要となるプログラムの領域もあるのです。これにより、Javaを適用できる領域が、また大きく広がることになるでしょう。
6.まとめ
この記事では、Javaで日時を扱うためのクラスjava.util.Dateをご紹介しました。Dateは基準日時からの経過時間のミリ秒を持つだけの単純なクラスですが、このクラスがJavaの日時処理を長らく支えてきました。
そして、Dateだけでは少々使いづらいため、Dateに欠けている機能を補完するためのCalendarやDateFormat(SimpleDateFormat)があります。Javaの日時処理は、これらのクラスを組み合わせて行うのです。
Java 12の今では、公式にはDateの役割は終わりました。Java 8からはjava.timeにあるDate and Time APIが公式のAPIになりましたので、これからはそれらを使ったAPIやプログラムが増えていくでしょう。
しかし、Dateがなくなることはないでしょうし、各種フレームワークやAPIもDateしか扱えないものがまだあります。ですから、Dateの使い方を身に着けることは、今のJavaプログラマにはまだ必要なことなのです。
コメント