JAVA CalendarクラスのareFieldsSet等の挙動


この記事は、Java経験値の浅い私が学習していく過程で気になったことのメモ的なものです。
Javaな人には常識かとも思いますが、CalendarクラスのareFieldsSetプロパティに言及したネタをあまり見かけなかったので書いておきます。

## キッカケ

日付チェックのロジックを作成しようとして下記のようなテストクラスを作成しました。

```Java:testCode1
    public static void calendarCompare() {
        Calendar calChange = Calendar.getInstance();//カレンダー１
        calChange.set(2015, 3, 25);//日付を変更
        Calendar calNow = Calendar.getInstance();//カレンダー２　現在日時のカレンダー
        if(calNow.compareTo(calChange) > 0){//判定式
            throw new IllegalArgumentException("未来日付は指定できません。");
        }
    }
```

この判定式のところで挙動が想定外だったため（というか単に「<」の向きを勘違い）、カレンダー１にsetメソッドが正しく反映されているのかを疑ってデバッガで調べ始めました。この際、色々挙動についてわかったことがあったので記録しておきます。


#デバッガでCalendarの動作を見てみる

calChange(カレンダー１)のインスタンス取得後のキャプチャです。
<img width="503" alt="60181322-9765-4509-9D86-B02D65BD093C.png" src="https://qiita-image-store.s3.amazonaws.com/0/44777/8954f615-6166-e7a6-471d-f120c693cc30.png">
<img width="503" alt="2F8A33F1-CADC-497D-9172-6231DA0EB571.png" src="https://qiita-image-store.s3.amazonaws.com/0/44777/2141c7a1-bacb-26d1-b656-0f1a3a6274d1.png">

setメソッド実行後のキャプチャです。
- fieldsの値は変わったけど、timeの値は更新されていない。
- areFieldsSetというプロパティがfalseに変わったのが気になる。
<img width="500" alt="73517FCC-A9DE-4795-8930-F56D7B9FA91A.png" src="https://qiita-image-store.s3.amazonaws.com/0/44777/e79d0d0a-bb37-b596-2caf-9be25c996dab.png">
<img width="492" alt="7F567107-1F99-415B-B852-03F2EA37648D.png" src="https://qiita-image-store.s3.amazonaws.com/0/44777/48e08138-c607-3de8-e283-f0053f96e267.png">

##リファレンスで調べてみた
リファレンスでCalendarクラスの関連プロパティ、メソッドについて調べてみました。
https://docs.oracle.com/javase/jp/7/api/java/util/Calendar.html#fields

* **fields** ・・・このカレンダで現在設定されている時刻に対するカレンダフィールド値です。

* **time**・・・このカレンダで現在設定されている時刻です。

* **isTimeSet**・・・time の値が有効である場合は true です。field[] の項目を変更することで、time は無効になります。

* **areFieldsSet**・・・fields[] が現在設定されている時刻と同期をとっている場合は true です。

    （詳細）fields[] が現在設定されている時刻と同期をとっている場合は true です。false の場合は、次にフィールドの値を取得しようとしたときに、time の現在値からのすべてのフィールドが再計算されます。


##動作についての考察
Calendarクラスで扱っている日時の実態はtimeプロパティが持っているミリ秒単位の時間です。この値を更新する際には、カレンダーフィールド＝fields　の値から演算が行われるということですね。

すごい適当ですが、こんな感じ

ユーザ<>fields<>time

で、ユーザ＞fileds は即反映されるけれど、fields＞time は即反映されず、getTime()など読み出しの要求があったタイミングで計算されて反映される。

つまり、timeとfieldsは更新時に非同期になる期間があるということで、その状態を表すフラグがareFieldsSetということですね。


よく見たらsetメソッドの説明にもそう書いてありました。

>set(f, value) では、カレンダフィールド f が value に変更されます。さらに、フィールド f が変更されたことを示すように内部メンバー変数が設定されます。カレンダフィールド f はただちに変更されますが、カレンダの時間値 (ミリ秒) は、get()、getTime()、getTimeInMillis()、add()、または roll() が次に呼び出されるまで再計算されません。このように、set() を複数回呼び出しても、不要な計算が行われることはありません。set() を使用してカレンダフィールドを変更すると、カレンダフィールド、カレンダフィールド値、およびカレンダシステムによってほかのフィールドも変更されることがあります。さらに、get(f) では、カレンダフィールドの再計算後に、setメソッドを呼び出して設定された value が必ず返されるとは限りません。これらの詳細は、具象カレンダクラスによって決定されます。
    
setメソッドは一つの日時を設定するためにフィールド数分複数回の呼び出しが行われる可能性があるため、getメソッドが呼び出されるタイミングで再計算した方が無駄がないということなんでしょう。


##判定式の動作では？

気になるのは冒頭のtestCode1クラスで使っている判定式（compareTo）はどうなるの？　ということです。

compareToはミリ秒同士での比較だから、かならずfieldからtimeの再計算が行われるはずだけど、デバッガで見ていると **areFieldsSet=true** のままなんですね。
どうやって比較してるの？
確かに上のsetメソッドのリファレンスでもcompareToは再計算の対象に入っていませんが。

気になったので、calendar.classの中を覗いてみました。

```Java:Calendar.class(抜粋)
    private int compareTo(long t) {
        long thisTime = getMillisOf(this);
        return (thisTime > t) ? 1 : (thisTime == t) ? 0 : -1;
    }
    private static long getMillisOf(Calendar calendar) {
        if (calendar.isTimeSet) {
            return calendar.time;
        }
        Calendar cal = (Calendar) calendar.clone();
        cal.setLenient(true);
        return cal.getTimeInMillis();
    }
```

どうやら、わざわざ比較のためにcloneを作成して、timeの値を再計算させたら捨ててる様子。
比較は非破壊で行いたいということなんでしょうかね…他の例を知らないからあくまで推測ですが。でもこれって比較をループでやったりすると重そうだなあ。


##終わりに

Javaの日付関連ってやはり把握しづらいですね。
初投稿でお見苦しい点もあると思いますが、何かの参考になれば。