Вызов изменений getTime Значение календаря

Я пытаюсь получить воскресение той же недели, что и заданная дата. Во время этого я столкнулся с этой проблемой:

Calendar calendar = Calendar.getInstance(Locale.GERMANY);
calendar.set(2017, 11, 11);
calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
System.out.println(calendar.getTime().toString());

приводит к "Sun Jan 07 11:18:42 CET 2018"

но

Calendar calendar2 = Calendar.getInstance(Locale.GERMANY);
calendar2.set(2017, 11, 11);
calendar2.getTime();
calendar2.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
System.out.println(calendar2.getTime().toString());

дает мне правильную дату "Sun Dec 17 11:18:42 CET 2017"

Может кто-нибудь объяснить, почему первый пример действительно ведет себя таким образом? Это действительно предназначено?

Спасибо

Ответ 1

В принципе API Calendar ужасен, и его следует избегать. Это не задокументировано ужасно ясно, но я думаю, что я вижу, где это происходит, и он ведет себя так, как предполагалось в этой ситуации. Под этим я подразумеваю это после намерения авторов API, а не намерения вас или кого-либо, читающего ваш код...

В документации :

Значения полей календаря можно задать, вызвав методы набора. Любые значения полей, заданные в Календаре, не будут интерпретироваться, пока не потребуется вычислять его значение времени (миллисекунды из эпохи) или значения полей календаря. Вызов get, getTimeInMillis, getTime, add и roll включает такие вычисления.

И затем:

При вычислении даты и времени из полей календаря может быть недостаточно информации для вычисления (например, только год и месяц без дня месяца), или может быть несогласованная информация (например, вторник, 15 июля, 1996 (по-григориански) - 15 июля 1996 года - фактически понедельник). Календарь определяет значения поля календаря для определения даты и времени следующим образом.

Если есть какие-либо конфликты в значениях поля календаря, календарь дает приоритеты для полей календаря, которые были установлены совсем недавно. Ниже приведены комбинации полей календаря по умолчанию. Будет использоваться последняя комбинация, определяемая самым последним установленным одиночным полем.

Для полей даты:

  • ГОД + МЕСЯЦ + DAY_OF_MONTH
  • ГОД + МЕСЯЦ + WEEK_OF_MONTH + DAY_OF_WEEK
  • ГОД + МЕСЯЦ + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK
  • YEAR + DAY_OF_YEAR
  • YEAR + DAY_OF_WEEK + WEEK_OF_YEAR

В первом примере тот факт, что последний набор полей был "днем недели", означает, что он будет использовать вычисление YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK (я думаю). Год и месяц были установлены в декабре 2017 года, но неделя месяца - текущая неделя месяца, которая является неделей 5 января 2018 года... поэтому, когда вы скажете, чтобы установить день недели, чтобы Воскресенье, он нашел воскресенье на "неделе 5" декабря 2017 года. В декабре было всего 4 недели, поэтому он эффективно продвигал его вперед... Думаю. Это все беспорядочно, и вам не стоит об этом думать, в основном.

Во втором примере вызов getTime() "блокирует" год/месяц/день, который вы указали, и вычисляет другие поля. Когда вы устанавливаете день недели, затем корректируете его в пределах существующих вычисленных полей.

В принципе, избегайте этого API, насколько возможно. Используйте java.time, который является более понятным API даты/времени.

Ответ 2

Как сказал Джон Скит, избегайте Calendar. Для вашего дела это действительно ужасно, и его плохо спроектировано в целом. Вместо этого

    WeekFields weekFieldsForLocale = WeekFields.of(Locale.GERMANY);
    // To find out which number Sunday has in the locale,
    // grab any Sunday and get its weekFieldsForLocale.dayOfWeek()
    int dayNumberOfSundayInLocale = LocalDate.now()
            .with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY))
            .get(weekFieldsForLocale.dayOfWeek());
    LocalDate date = LocalDate.of(2017, Month.DECEMBER, 11);
    LocalDate sunday
            = date.with(weekFieldsForLocale.dayOfWeek(), dayNumberOfSundayInLocale);

    System.out.println(sunday);

Отпечатает ожидаемую дату

2017-12-17

Как уже упоминалось, решение состоит в использовании java.time, современного API дат и времени Java. Также, как правило, с ним гораздо лучше работать. Одна приятная функция - класс LocalDate, который я использую. Это дата без времени суток, которая, по-видимому, соответствует вашим требованиям более точно, чем Calendar.

Если вышеизложенное выглядит сложным, его, потому что, как я думаю, вы знаете, "воскресенье той же недели" означает разные вещи в разных местах. В международном стандарте, который придерживается Германия, недели начинаются в понедельник, поэтому воскресенье - последний день недели. В американском стандарте, например, воскресенье в первый день недели. WeekFields.dayOfWeek() показывает дни недели от 1 до 7, поэтому, когда мы хотим установить день на воскресенье, нам сначала нужно выяснить, какое число воскресенье ушло в эту нумерацию (7 в Германии, 1 в США). Поэтому для любого воскресенья получите значение weekFieldsForLocale.dayOfWeek(), а затем используйте это для установки дня недели в воскресенье. Причина, по которой это необходимо, заключается в том, что метод with() является настолько общим и поэтому предназначен для принятия только числовых значений; мы не можем просто передать ему объект DayOfWeek.

Если я подстановлю Locale.US в код, я получаю 2017-12-10, что является правильным воскресеньем для календаря, где воскресенье - первый день недели. Если вы уверены, что только хотите, чтобы ваш код работал в Германии, вы можете, конечно, просто указать код 7 (пожалуйста, сделайте его постоянным с очень пояснительным именем).

Ссылка: Время учебного времени Oracle, объясняющее, как использовать java.time. В сети есть другие ресурсы (просто избегайте устаревшего размещения, предлагающего java.util.Calendar: -)