🍇

20.1 古典的な日時クラスとAPI(Dateクラス、Calendarクラスなど)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

20.1 古典的な日時クラスとAPI

チャプターの概要

このチャプターでは、未だに現役で利用されている、古典的な日時クラスの特徴とAPIについて学びます。
なおこのチャプターおよび次のチャプターでは、「時間」という言葉には曖昧さがあるため、「日付」や「時刻」といった用語を用います。また「日付」と「時刻」を足し合わせた概念を、「日時」という言い方で統一します。

20.1.1 日時クラスの概要

日時クラスの変遷

Javaでは、旧来より日時を表すクラスとして、java.util.Dateやjava.util.Calendarといったクラスが、Java SEのクラスライブラリとして提供されてきました。ただしこれらのクラスには「APIのラインアップが乏しい」、「国際化(タイムゾーン)に対応していない(Dateクラス)」また「スレッドセーフではない」といった課題がありました。
このような背景から、Java 8において「Date and Time API」が導入され、日時を表すクラスは全面的に刷新されました。java.util.Dateやjava.util.Calendarといった古典的な日時クラスは現在でも使えますが、前述したような課題もあり、また特にDateクラスについては非推奨の扱いとなっているAPIが大半を占めるため、新規のアプリケーション開発では使用するべきではありません。
その一方で現在でも、古典的な日時クラスによって作成されたアプリケーションを見かけることは、ゼロではありません。またベンダーやコミュニティによって提供されるライブラリやフレームワークにも、古典的な日時クラスを前提にしたものが一部に残存しています。
そのため本レッスンでは、このチャプターにおいて、古典的な日時クラスの使用方法について説明をすることにします。

Dateクラスの特徴

java.util.Dateクラスは、ある特定の日時を表すためのクラスです。
Dateクラスでは、UNIX系OSと同じように「西暦1970年1月1日0時0分0秒(GMT)」からの経過時間によって日時を表します。この経過時間は、ミリ秒の単位で表されるため「エポックミリ秒」とも呼ばれ、Javaではlong型の値として扱われます。
Dateクラスのインスタンス生成方法は、表す日時によって以下の2つに分かれます。

  • システムクロックによる現在日時を表すインスタンスを生成する場合
  • 現在日時ではない、特定の日時のインスタンスを生成する場合

まず現在日時を表すDateオブジェクトを生成したい場合は、Dateクラスの引数なしコンストラクタによって生成します。

snippet (pro.kensait.java.basic.lsn_20_1_1.Main)
Date now = new Date();

次に特定の日時のインスタンスを生成する場合ですが、Dateクラスには特定の日時を指定可能なコンストラクタがありますが、これらは非推奨の扱いなので使用すべきではありません。ではどうすれば良いかというと、特定の日時を表すDateオブジェクトは、後述するjava.util.CalendarクラスのgetTime()メソッドによって取得します。
またDateクラスには、日時を操作したり、日付の前後判定をしたりするメソッドなども用意されていますが、これらも多くは非推奨の扱いのため基本的にはCalendarクラスを使用してください。
このような話を聞くと、Dateクラスには存在意義はなくCalendarクラスだけでよいのでは、と思えるかもしれませんが、必ずしもそんなことはありません。
DateクラスとCalendarクラスは、組み合わせて使用するケースが一般的です。この2つのクラスをどのように組み合わせたら良いのかという点は、次のレッスンで後述します。

Calendarクラスの特徴とAPI

java.util.CalendarクラスもDateクラスと同様に、ある特定の日時を表すためのクラスです。
ただしCalendarクラスはDateクラスとは異なり、特定の日付や時間を設定することができます。また日時を操作したり(例えば「現在日時の3日後を求める操作」など)や、前後比較をしたりするためのメソッドも用意されています。
Calendarクラスの主要なAPIを、以下の表に示します。なお厳密にはCalendarクラスには「タイムゾーン」という概念が取り込まれています。ただしタイムゾーンが必要な場合は後述する「Date and Time API」を使用する方が望ましいため、ここではデフォルトのタイムゾーン(すなわち日本時間)を前提にします。

API(メソッド) 説明
static Calendar getInstance() (システムクロックによる)現在日時を表すCalendarオブジェクトを返す。
Date getTime() このCalendarの設定時間を表すDateオブジェクトを返す。
long getTimeInMillis() このCalendarのエポックミリ秒を返す。
void setTime(Date) このCalendarの日時を、指定されたDateに設定する。
void set(int, int) 第一引数に指定されたカレンダーフィールド(定数)を、第二引数の値に設定する。
void set(int, int, int) 第一引数から順に、カレンダーフィールドYEAR(年)、MONTH(月)、およびDAY_OF_MONTH(月の日)の値を設定する。
void set(int, int, int, int, int) 第一引数から順に、カレンダーフィールドYEAR(年)、MONTH(月)、DAY_OF_MONTH(月の日)、HOUR_OF_DAY(時)、およびMINUTE(分)の値を設定します。
void set(int, int, int, int, int, int) 第一引数から順に、カレンダーフィールドYEAR(年)、MONTH(月)、DAY_OF_MONTH(月の日)、HOUR_OF_DAY(時)、MINUTE(分)、およびSECOND(秒)の値を設定します。
int get(int) 指定されたカレンダーフィールド(定数)の値を返す。
void add(int, int) 第一引数に指定された指定されたカレンダーフィールド(定数)に対して、第二引数に指定された値を加算または減算する。
int compareTo(Calendar) このCalendarと指定されたCalendarを比較し、前の日時の場合は-1、後の日時の場合は1、同じ場合は0を返す。

APIの説明に登場する「カレンダーフィールド」ですが、これはカレンダーが内部的に保持する属性を表す定数で、以下のようなものがあります。

  • Calendar.YEAR … 年(西暦)
  • Calendar.MONTH … 月(0始まり)
  • Calendar.DAY_OF_MONTH … 月の日(15日など)
  • Calendar.HOUR_OF_DAY … 時(24時間制)
  • Calendar.MINUTE …分
  • Calendar.SECOND …秒

20.1.2 古典的な日時クラスの使用方法

Calendarクラスの日時設定

ここでは、Calendarクラスに日時を設定する方法を説明します。
まずCalendarオブジェクトは、スタティックなgetInstance()メソッドによって取得します。

snippet_1 (pro.kensait.java.basic.lsn_20_1_2.Main)
Calendar cal = Calendar.getInstance();

この時点でこのCalendarには、現在日時が設定されています。
続いてset()メソッドにより、このCalendarの日時を「2022年11月5日15時20分」に設定します。

snippet_2 (pro.kensait.java.basic.lsn_20_1_2.Main)
cal.set(2022, 10, 5, 15, 20);

第二引数の月は(非常に分かりにくいことに)0始まりなので、注意してください。
この時点でこのCalendarの属性を調べてみましょう。

snippet_3 (pro.kensait.java.basic.lsn_20_1_2.Main)
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1; // 月は0始まりなので1加算
int day = cal.get(Calendar.DAY_OF_MONTH);

このようにget()メソッドにカレンダーフィールドを指定すると、当該の属性を取得することができます。

Calendarクラスによる日付操作

ここでは、CalendarクラスのAPIによって日時を操作する方法を具体的に説明します。
「2022年11月5日15時20分」を表すCalendarに対して、add()メソッドによって月の日を10日前に戻してみましょう。

snippet_4 (pro.kensait.java.basic.lsn_20_1_2.Main)
Calendar cal = Calendar.getInstance();
cal.set(2022, 10, 5, 15, 20);
cal.add(Calendar.DAY_OF_MONTH, -10);

このようにadd()メソッドには、加減算するカレンダーフィールドと加減算の値を指定します。このメソッド呼び出しによって、このCalendarの月日は(2022年11月15日の10日前である)2022年10月26日に設定されます。
続いて同じく「2022年11月5日15時20分」を表すCalendarに対して、add()メソッドとset()メソッドによって「前月初日」を設定してみましょう。

snippet_5 (pro.kensait.java.basic.lsn_20_1_2.Main)
Calendar cal = Calendar.getInstance();
cal.set(2022, 10, 5, 15, 20);
cal.add(Calendar.MONTH, -1); // 前月
cal.set(Calendar.DATE, 1); // 初日

このメソッド呼び出しによって、このCalendarの日付は2022年10月1日に設定されます。

Calendarクラスによる日時の前後比較

CalendarクラスのcompareTo()メソッドを使うと、Calendar同士の前後比較を行うことができます。
以下のコードは、現在日時である2022年11月5日を過去日付である4月10日と比較しています。

snippet_6 (pro.kensait.java.basic.lsn_20_1_2.Main)
Calendar current = Calendar.getInstance(); // 現在=2022年11月5日
Calendar target = Calendar.getInstance();
target.set(2022, 3, 10); // 2022年4月10日
int result = current.compareTo(target);

11月15日の方が後ろのため、変数resultには1が格納されます。
このようなcompareTo()メソッドによる日時の前後比較では、注意しなければならないことがあります。それは比較対象になるのは当該Calendarの年、月、日だけではなく、時、分、秒まで含まれる、ということです。例えば以下は一見すると、変数resultには0が代入されるように見えますが、実際にはそうはなりません。

snippet_7 (pro.kensait.java.basic.lsn_20_1_2.Main)
Calendar time1 = Calendar.getInstance();
time1.set(2022, 3, 10); // 2022年4月10日
// 1秒間スリープ
........
Calendar time2 = Calendar.getInstance();
time2.set(2022, 3, 10); // 2022年4月10日
int result = time1.compareTo(time2); 

2つのCalender、変数time1とtime2にはset()メソッドで年、月、日を設定していますが、内部的には属性として時、分、秒を持っています。Calenderの属性には、getInstance()メソッド呼び出し時に現在時刻がセットされています。time1とtime2の間にはインスタンス生成に1秒間の遅れがあるため、この両者を比較するとtime2の方が後ろであると見なされるのです。

DateクラスとCalendarクラスの相互反映

DateクラスとCalendarクラスは、相互にインスタンスの属性を反映し合うことができます。
まずCalendarクラス→Dateクラスです。以下のようにCalendarのgetTime()メソッドによって、Calendarの属性を引き継いだDateを取得します。

snippet_8 (pro.kensait.java.basic.lsn_20_1_2.Main)
Calendar cal = Calendar.getInstance();
Date now = cal.getTime();

次にDateクラス→Calendarクラスです。以下のようにCalendarのsetTime()メソッドにDateを設定することで、Dateの属性がCalendarに反映されます。

snippet_9 (pro.kensait.java.basic.lsn_20_1_2.Main)
Date now = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(now);

ただしDateは、引数のないコンストラクタによって、現在日時を表すインスタンスを生成するケースがほとんどです。また日時の属性を変更することもできない[1]ので、わざわざDateの属性をCalendarに反映させる必然性は基本的に考えられません。

20.1.3 Dateクラスと文字列の相互変換

SimpleDateFormatクラスのAPI

Dateクラスと文字列を相互変換するためには、java.text.SimpleDateFormatクラスを使用します。
SimpleDateFormatクラスは、以下のようなコンストラクタによってインスタンスを生成します。

  • SimpleDateFormat(String)

このとき引数には、"yyyy/M/d h:m:s"のような「日時フォーマット」を指定します。「日時フォーマット」の代表的な記法には、年はyyyy、月はM、月の日はd、時(24時間制)はH、時(12時間制)はh、分はm、秒はs、といったものがあります。また、M、d、H、h、m、sといった記法を、MM、dd、HH、hh、mm、ssといった具合に二文字にすると、10未満のときに先頭に0が追加されます。

続いてAPIです。SimpleDateFormatクラスには数多くのAPIがありますが、中でも代表的なものを以下に示します。

API(メソッド) 説明
String format(Date) 事前に指定された「日時フォーマット」に従って、Dateをフォーマッティングし、文字列として返す。
Date parse(String) 事前に指定された「日時フォーマット」に従って、指定された文字列解析し、Dateを生成して返す。

SimpleDateFormatクラスの主な役割は、これらのAPIによって、Dateから文字列にフォーマッティングしたり、文字列を解析してDateを生成したりすることにあります。
次項からは、これらのAPIの使用方法について具体的に説明していきます。

SimpleDateFormatクラスによる日時→文字列変換(フォーマッティング)

ここでは、SimpleDateFormatクラスによって日時をフォーマッティングするための方法を説明します。以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_20_1_3.Main)
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); //【1】
Date date = new Date();
String dateStr = df.format(date); //【2】

まず「日時フォーマット」を指定して、SimpleDateFormatのインスタンス生成します【1】。
続いてDateから文字列へのフォーマッティングは、SimpleDateFormatオブジェクトのformat()メソッドに対象のDateを指定します【2】。このようにすると、事前に指定された日時フォーマット("yyyy/MM/dd HH:mm:ss")に従って、フォーマッティングされた文字列が生成されます。よって、現在日時が「2022年11月5日 15時3分20秒」であれば、このコードを実行すると、変数dateStrには文字列"2022/11/05 15:03:20"が格納されます。

SimpleDateFormatクラスによる文字列→日時変換(解析)

ここではSimpleDateFormatクラスによって文字列を解析し、Dateオブジェクトを生成するための方法を説明します。以下のコードを見てください。

snippet_2 (pro.kensait.java.basic.lsn_20_1_3.Main)
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); //【1】
try {
    Date date = df.parse("2022/11/05 15:03:20"); //【2】
} catch (ParseException pe) {
    new RuntimeException(pe);
}

「日時フォーマット」を指定して、SimpleDateFormatのインスタンスを生成する【1】点は、前項と同様です。続いて生成したSimpleDateFormatオブジェクトのparse()メソッドに、解析対象の文字列を渡します【2】。このようにすると、事前に指定された日時フォーマット("yyyy/MM/dd HH:mm:ss")に従って文字列が解析され、当該日時を表すDateオブジェクトが生成されます。なおparse()メソッドの呼び出しでは、指定された文字列のフォーマットが不正な場合、チェック例外であるParseExceptionが送出されるため例外ハンドリングが必要です。

20.1.4 DateクラスとCalendarクラスによるクラス設計

DateクラスとCalendarクラスによるクラス設計

このレッスンでは、開発者が作成するクラスにおけるDateクラスおよびCalendarクラスの典型的な利用方法を、「人物」を表すPersonクラスを題材に説明します。
Personクラスは以下のように、name(名前)、age(年齢)、birthDay(生年月日)という3つのフィールドがあり、コンストラクタとアクセサメソッドも定義されているものとします。

pro.kensait.java.basic.lsn_20_1_4.Person
public class Person {
    // フィールド
    private String name; // 名前
    private int age; // 年齢
    private Date birthDay; // 生年月日
    // コンストラクタ
    ........
    // アクセサメソッド
    ........
}

生年月日を表すbirthDayフィールドは、Date型として定義しました。特定の日付を表す、という意味ではCalendar型も候補になるように思えるかもしれませんが、Calendarクラスはスレッドセーフではないためフィールドの型として定義するのは望ましくありません。その点Date型は、基本的に一度値を設定したら更新することはできず、新しいDataオブジェクトで上書きすることしかできないため、フィールドの型にして問題ありません。
さてこのようなPersonクラスから、Aliceのインスタンスを生成します。

snippet_1 (pro.kensait.java.basic.lsn_20_1_4.Main)
Calendar cal = Calendar.getInstance();
cal.set(1997, 3, 10); //【1】1997年4月10日
Date birthday = cal.getTime(); //【2】
Person alice = new Person("Alice", 25, birthday); //【3】

このコードでは、まずAliceの生年月日である「1997年4月10日」をCalendarクラスによって設定しています【1】。そしてCalenderからgetTime()メソッドによってDataに変換し【2】、それをPersonクラスのコンストラクタに渡してAliceのインスタンスを生成します【3】。
このように、あるクラスが属性として日時を持つ場合は、フィールドにはDate型を定義し、Calenderクラスによって任意の日付を設定した上で、getTime()メソッドでDateに変換する、というのが、両クラスの典型的な組み合わせパターンです。

さて、次にこのPersonクラスに機能を追加してみましょう。具体的には、生年月日を文字列として取得したり、文字列として設定するためのメソッド(以下のコード)を追加する、というものです。

snippet (pro.kensait.java.basic.lsn_20_1_4.Person)
public String getBirthDayStr() { //【1】生年月日を文字列で取得
    SimpleDateFormat df = new SimpleDateFormat("yyyy年M月d日");
    return df.format(birthDay);
}
public void setBirthDayStr(String birthDayStr) { //【2】生年月日を文字列で設定
    SimpleDateFormat df = new SimpleDateFormat("yyyy年M月d日");
    try {
        this.birthDay = df.parse(birthDayStr);
    } catch (ParseException pe) {
        new RuntimeException(pe);
    }
}

まずgetBirthDayStr()メソッド【1】では、SimpleDateFormatクラスによって、Date型のbirthDayフィールドから、指定された日時フォーマットに従って文字列に変換して返します。またsetBirthDayStr()メソッド【2】では、同じくSimpleDateFormatクラスを利用し、文字列を解析してDateを生成した上でbirthDayフィールドに設定します。
Personクラスにこのようなメソッドを追加し、以下のように呼び出します。

snippet_2 (pro.kensait.java.basic.lsn_20_1_4.Main)
String birthdayStr = alice.getBirthDayStr();

すると生年月日である「1997年4月10日」が、フォーマッティングされた文字列として返されます。このように、あるクラスが属性としてDate型を持つ場合、それを文字列として取得したり設定したりするためのメソッドを合わせて用意しておくという設計は、比較的よく見られるでしょう。

なおこのコードでは、SimpleDateFormatオブジェクトを生成するための記述がgetBirthDayStr()メソッドとsetBirthDayStr()メソッドとで同じなので、PersonクラスのフィールドとしてSimpleDateFormatクラスを定義してはどうかと考えるかもしれません。ただしSimpleDateFormatクラスはスレッドセーフではないため、フィールドとして定義すると予期せぬ不具合が発生する可能性があります。このクラスはメソッドのスコープ内で宣言して使うのが原則です。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. 古典的な日時クラスの課題とDate and Time API導入の経緯について。
  2. DateクラスやCalendarクラスの概要やAPIについて。
  3. Dateクラスと文字列を相互変換するAPIについて。
  4. DateクラスとCalendarクラスによるクラス設計について。
脚注
  1. 厳密には「できない」わけではなく、Dateクラスには日時を操作するためのAPIがあるが、非推奨の扱いなので使用すべきではない。 ↩︎

Discussion