Почему GregorianCalendar меняет день при установке HOUR_OF_DAY на 0 в UTC?

Я наблюдал странное поведение java.util.GregorianCalendar, и мне интересно, почему он так себя ведет.

Я хотел получить время в UTC, что в тот же момент, что и 26.10.2014 01:00 CET, а затем получить UTC полночь за тот же день. Поэтому сначала я установил фактическую дату CET, чем изменил часовой пояс на UTC и, наконец, установил HOUR_OF_DAY равным 0.

Пример:

  • 26.10.2014 01:00 CET совпадает с 25.10.2014 23:00 UTC
  • полночь (25.10.2014 23:00 UTC) должна быть 25.10.2014 00:00 UTC

см. код юнита ниже:

@Test
public void testWeird() {
    GregorianCalendar date = (GregorianCalendar) GregorianCalendar.getInstance(TimeZone.getTimeZone("CET"));
    date.set(2014, 9, 26, 1, 0, 0); //26.10.2014
    System.out.println(date.getTime().toGMTString() + " " + date.getTimeInMillis()); // 25 Oct 2014 23:00:00 GMT 1414278000764 (OK)

    date.setTimeZone(TimeZone.getTimeZone("UTC"));
    //date.get(Calendar.YEAR); // uncomment this line to get different results

    System.out.println(date.getTime().toGMTString() + " " + date.getTimeInMillis()); // 25 Oct 2014 23:00:00 GMT 1414278000764 (OK)
    date.set(Calendar.HOUR_OF_DAY, 0);
    System.out.println(date.getTime().toGMTString() + " " + date.getTimeInMillis()); // 26 Oct 2014 00:00:00 GMT 1414281600764 (NOT OK! why not 25 Oct 2014 00:00:00 GMT 1414195200218 ?)
}

Я ожидал, что установка часа = 0 на 25.10.2014 23:00 GMT даст мне 25.10.2014 00:00 GMT, но она изменилась на 26.10.2014 00:00 GMT.

Однако, если я раскомментирую строку date.get(Calendar.YEAR);, дата, кажется, правильно рассчитана.

То же самое происходит и с jdk.1.7.0_10 и jrockit-jdk1.6.0_37.

Ответ 1

Поскольку GregorianCalender расширяет класс Calendar, он наследует все его функции. Из Java Doc

set(f, value) changes calendar field f to value. In addition, it sets an internal 
member variable to indicate that calendar field f has been changed. Although 
calendar field f is changed immediately, the calendar time value in 
milliseconds is not recomputed until the next call to get(), getTime(), 
getTimeInMillis(),add(), or roll() is made. Thus, multiple calls to set() do not 
trigger multiple, unnecessary computations. As a result of changing a calendar 
field using set(), other calendar fields may also change, depending on the calendar 
field, the calendar field value, and the calendar system. In addition, get(f) will 
not necessarily return value set by the call to the set method after the calendar 
fields have been recomputed.

Пример Java Doc:

Consider a GregorianCalendar originally set to August 31, 1999. Calling 
set(Calendar.MONTH, Calendar.SEPTEMBER) sets the date to September 31, 1999. This 
is a temporary internal representation that resolves to October 1, 1999 if 
getTime()is then called. However, a call to set(Calendar.DAY_OF_MONTH, 30) before 
the call to getTime() sets the date to September 30, 1999, since no recomputation 
occurs after set() itself.

Также класс Календарь имеет следующий побочный эффект: -

        In lenient mode, all of the Calendar fields are normalized.

Это означает, что когда вы вызываете setTimeZone() и set(Calendar.HOUR_OF_DAY, 0), он устанавливает внутреннюю переменную-член, чтобы указать, что установлены поля Календаря. Но Календарное время не пересчитывается в это время. Время календаря пересчитывается только после вызова get(), getTime(), getTimeInMillis(), add() или roll().

Это ошибка в классе календаря JDK-4827490: (cal) Doc: поведение Calendar.setTimeZone недокументировано

Теперь ваш пример изменен, чтобы получить работу, как показано ниже: -

public class DateTimeTest {

    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        GregorianCalendar date = (GregorianCalendar) GregorianCalendar.getInstance(TimeZone
            .getTimeZone("CET"));

        // 26.10.2014 01:00:00
        date.set(2014, 9, 26, 1, 0, 0);

        // 25 Oct 2014 23:00:00 GMT 1414278000764
        System.out.println("CET to UTC    : " + date.getTime().toGMTString() + " "
            + date.getTimeInMillis());

        date.setTimeZone(TimeZone.getTimeZone("UTC"));

//      date.roll(Calendar.HOUR_OF_DAY, true);  //uncomment this line & comment below line & check the different behavior of Calender.
        date.get(Calendar.HOUR_OF_DAY);

        // 25 Oct 2014 23:00:00 GMT 1414278000764
        System.out.println("UTC          : " + date.getTime().toGMTString() + " "
            + date.getTimeInMillis());

        date.set(Calendar.HOUR_OF_DAY, 0);

        // 25 Oct 2014 00:00:00 GMT 1414195200218
        System.out.println("UTC Midnight : " + date.getTime().toGMTString() + " "
            + date.getTimeInMillis());
    }
}

Выход:

CET to UTC   : 25 Oct 2014 23:00:00 GMT 1414278000008
UTC          : 25 Oct 2014 23:00:00 GMT 1414278000008
UTC Midnight : 25 Oct 2014 00:00:00 GMT 1414195200008

Надеюсь, теперь вы получите четкое представление о непредсказуемом поведении класса Calendar.

Ответ 2

Вы напрямую меняете java.util.Date.setTimeZone(TimeZone.getTimeZone("UTC"));?

В часовом поясе GregorianCalendar's находится CET и date's часовой пояс UTC. И вы распечатываете его. Измените экземпляр GregorianCalendar с часовым поясом UTC.

//date.setTimeZone(TimeZone.getTimeZone("UTC")); <- remove it.
 date.set(Calendar.HOUR_OF_DAY, 0);

GregorianCalendar utcCAL = (GregorianCalendar) GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
utcCAL.setTimeInMillis(date.getTimeInMillis());
System.out.println(utcCAL.getTime().toGMTString() + " " + utcCAL.getTimeInMillis());

Выход

25 Oct 2014 22:00:00 GMT 1414274400517

Обновление

Вы также можете использовать функцию java.util.Date.UTC().

//date.setTimeZone(TimeZone.getTimeZone("UTC"));
date.set(Calendar.HOUR_OF_DAY, 0);
Date utcDate = date.getTime();
utcDate.UTC(2014, 9, 1, 26, 1, 0);
System.out.println(utcDate.toGMTString() + " " + date.getTimeInMillis());