torutkのブログ

ソフトウェア・エンジニアのブログ

年齢を計算する、java.util.Calendarとjava.time.LocalDateとで

年齢を計算する方法を、Java SE 7までの標準APIであるjava.util.Calendarと、Java SE 8の新機能Date and Time APIで実装して比べてみようと思います。

オブジェクトにおける年齢の表現方法

オブジェクト指向プログラミングのチュートリアルやサンプルでみかける人物クラスと年齢属性(Personクラスとageフィールドなど)があります。あまりに単純なサンプルでは年齢を単なるintのフィールドで実装しています。

public class Person {
    private String name;  // 名前
    private int age; // 年齢

    // 名前と年齢を指定してインスタンスを生成
    public Person(String aName, int anAge) {
        name = aName;
        age = anAge;
    }
    // 年齢を取得する
    public int getAge() {
        return age;
    }
}

しかし、年齢をintとして保持しそれを返すというモデリングは実世界とはかけ離れたものです。このPersonインスタンスは、いついかなる時も同じ年齢を持つことになります。実際には、年齢を知りたいある時点(年月日)と誕生日から年齢が決まります。

public class Person {
    private String name;
    private Date birthday;

    public Person(String aName, Date aBirthday) {
        name = aName;
        birthday = aBirthday;
    }

    public int getAge(Date aDate) {
        // birthdayとaDateから年齢を計算し返却する
    }
}

年齢の定義

ここで実装に入る前に年齢の計算の定義をしておきます。

日本の年齢計算を定めた法律によると、年齢が繰り上がるのは誕生日の前日です。正確には誕生日の前日の最後の時刻(午後12時)に繰り上がります。これは時間軸上では誕生日当日の午前0時と一致するのですが、法律上はあくまで前日に繰り上がりとなります。

4月1日生まれの人が学校では早生まれとして前日の3月31日生まれの人と同じ学年になるのは、4月1日生まれの人が満6歳になる日付が法律に基づくと3月31日(午後12時)だからです。

ただし、おそらく社会通念としては年齢は誕生日に繰り上がるというのが自然です。

そこで、日本の法律に関する年齢計算をするときは、前者の定義に基づく設計をしておかないと「バグ」になってしまいます。

また、年齢については時差を考慮しないと思われます。日本で2月1日に生まれた人が、アメリカに住んでいたら1月31日に誕生日を祝うかというと、やっぱり2月1日になるのではと思います。(そもそも時差を考えると生まれた時間まで考慮しないと・・・)

ということで、年齢計算の法律、時差などをどのように扱うかを決めないと年齢を計算することができないという事態になります。

今回は次の定義で年齢計算を実装することにします。

  • 年齢は誕生日当日に繰り上がる
  • どこの時刻帯であっても誕生日は不変とする
  • 年齢の計算における時刻帯は、計算をする時刻帯とする

Java SE 7までの標準APIで実装する

うまくいかないDate型

たぶんふつうに実装するとこういうコードになりますが、いくつか問題が含まれています。

まずはコードを先に示します。

    public int getAge(Date aDate) {
        // 誕生日の年月日を得るためCalendar型のインスタンス取得
        Calendar birthdayCal = Calendar.getInstance();
        birthdayCal.setTime(birthday);
        // 年齢計算日を得るためCalendar型のインスタンス取得
        Calendar theDayCal = Calendar.getInstance();
        theDayCal.setTime(aDate);
        // 計算日の年と誕生日の年の差を算出
        int yearDiff = theDayCal.get(Calendar.YEAR) - birthdayCal(Calendar.YEAR);
        // ただし誕生月・日より年齢計算月日が前であれば年齢は1歳少ない
        if (theDayCal.get(Calendar.MONTH) < birthdayCal.get(Calendar.MONTH)) {
            yearDiff--;
         } else if (theDayCal.get(DAY_OF_MONTH) < birthdayCal.get(Calendar.DAY_OF_MONTH)) {
            yearDiff--;
         }
         return yearDiff;
    }


このコードで問題となるのが、次の部分です。

        Calendar birthdayCal.getInstance();
        birthdayCal.setTime(birthday);
        Calendar theDayCal.getInstance();
        theDayCal.setTime(aDate);

ここで得られるCalendarはプログラムを実行しているロケールのものになるので、もし誕生日を設定したロケールと実行しているロケールが違う場合、誕生日付がずれる可能性があります。

Date型は、UTC時刻で1970-01-01 00:00:00からの経過ミリ秒を保持しています。ここで日本語環境(ロケールja_JP)で実行したプログラムで誕生日(1980-01-01 00:00:00)を設定すると、Date型が保持する値はUTC時刻で1979-12-31 15:00:00(と解釈できる経過ミリ秒)となります。

次に、誕生日を取り出すプログラムが英語(ロケールen_US)であればUTC時刻の1979-12-31 15:00:00は1979-12-31 10:00:00として扱われ、誕生日の年月日は1979年12月31日となります。

このように、Date型で誕生日を保持する方法だと、プログラムを実行するロケールによって日付が変わってしまうという問題があります。
また、Date型を引数にとる場合、元のロケール情報がないため、元のロケール年月日
が不明となります。

そこで、対策としては

  • 誕生日・年齢計算日をCalendar型で受け渡す
  • ロケールを持たない年月日を使用する

となります。

Calendar型を使った場合

Calendar型で扱う場合は次のコードとなります。

public class Person {
    String name;
    Calendar birthday;

    public Person(String aName, Calendar aBirthday) {
        name = aName;
        birthday = aBirthday;
    }

    public int getAge(Calendar aDay) {
        // 計算日の年と誕生日の年の差を算出
        int yearDiff = aDay.get(Calendar.YEAR) - birthday(Calendar.YEAR);
        // ただし誕生月・日より年齢計算月日が前であれば年齢は1歳少ない
        if (aDay.get(Calendar.MONTH) < birthday.get(Calendar.MONTH)) {
            yearDiff--;
         } else if (aDay.get(DAY_OF_MONTH) < birthday.get(Calendar.DAY_OF_MONTH)) {
            yearDiff--;
         }
         return yearDiff;        
    }
}

Calendar型をフィールドに保持する場合の欠点は、Calendar型インスタンスのメモリ使用量が300バイト強と日付を保持するには大きすぎるため、大量の人物データをメモリに展開するとメモリ不足の原因となる可能性があることです*1

Java SE 8のDate and Time APIの場合

Date and Time APIには、java.time.LocalDateクラスがあり、これはタイムゾーンによる時差の考慮をしない日付型です。年齢計算においてはちょうどよい型となります。

public class Person {
    private String name;
    private LocalDate birtyday;
 
    public Person(String aName, LocalDate aBirthday) {
        name = aName;
        birthday = aBirthday;
    }

    public int getAge(LocalDate aDay) {
        return Period.between(birthday, aDay).getYears();
    }
}

Java SE 8のDate and Time APIには、まず時刻帯による調整のない日付(年月日)を表現するLocalDateクラスがあります。また、ある時刻と時刻との間の大きさである時間を表現するPeriodクラスがあります。この両者を使って年齢計算を実現しています。また、LocalDateはメモリ使用量が少ないので大量の人物データをメモリに展開してもそれほどメモリを占めません。

年齢とは、時間軸上での2つの時刻の間の長さを年で表現したものですから、Java Date and Time APIのように、時刻と時間(時刻差)という概念を持つAPIがあると容易に実装することができます。

*1:メモリに100万人分のデータを展開した場合、誕生日だけで300MBに達する。