Странное поведение с GregorianCalendar

Я просто столкнулся с странным поведением с классом GregorianCalendar, и мне было интересно, действительно ли я делал что-то плохое.

Это добавляется только в том случае, если месяц даты инициализации имеет фактический максимум, превышающий месяц, в который я собираюсь установить календарь.

Вот пример кода:

    // today is 2010/05/31  
    GregorianCalendar cal = new GregorianCalendar();

    cal.set(Calendar.YEAR, 2010);
    cal.set(Calendar.MONTH, 1); // FEBRUARY

    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
    cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
    cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
    cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
    cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

    return cal.getTime(); // => 2010/03/03, wtf

Я знаю, что проблема вызвана тем фактом, что дата инициализации календаря составляет 31 день (может), что беспорядок с месяцем, установленным на февраль (28 дней). Исправить легко (просто установите day_of_month в 1 перед установкой года и месяца), но мне было интересно, действительно ли это было желаемое поведение. Любые мысли?

Ответы

Ответ 1

Он получает фактические максимумы текущей даты/времени. Май имеет 31 день, который составляет 3 больше, чем 28 февраля, и поэтому он переместится на 3 марта.

Вам нужно позвонить Calendar#clear() после получения/создания:

GregorianCalendar cal = new GregorianCalendar();
cal.clear();
// ...

Это приводит к:

Sun Feb 28 23:59:59 GMT-04:00 2010

(что верно в соответствии с моим часовым поясом)

Как сказано в одном из ответов, java.util.Calendar и Date являются эпическими неудачами. При интенсивных операциях даты и времени рассмотрите JodaTime.

Ответ 2

Да, так оно и должно работать. Если вы начинаете с GregorianCalendar с точной датой, и вы изменяете его, делая его несовместимым, вы не должны доверять полученным результатам.

В соответствии с документацией о getActualMaximum(..) он указывает:

Например, если дата этого экземпляра равна 1 февраля 2004 года, фактическое максимальное значение поля DAY_OF_MONTH равно 29, поскольку 2004 год является високосным годом, а если дата этого экземпляра равна 1 февраля 2005 года, то 28.

Поэтому он должен работать, но вы должны кормить его согласованными значениями. 31 февраля 2010 неверно, и применение недействительных значений даты (например, getActualMaximum) не может работать. Как его исправить самостоятельно? Решив, что месяц не так? или что день неправильный?

Кстати, как всегда, всегда используется JodaTime..:)

Ответ 3

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

Если вы не можете использовать JodaTime или JSR-310 в своем проекте, unit test сильно использовать класс Calendar. Как видите, в этом случае код календаря ведет себя по-разному в зависимости от того, в какой день месяца (или в какое время дня) вы запускаете код.

Ответ 4

Может быть, setLenient(boolean lenient) будет сортировать его для вас. Я получаю исключение, когда запускаю код ниже.

Если нет, Джода - лучший ответ.

import java.util.Calendar;

public class CalTest
{
    public static void main(String[] args)
    {
        // today is 2010/05/31
        Calendar cal = Calendar.getInstance();
        cal.setLenient(false);

        cal.set(Calendar.YEAR, 2010);
        cal.set(Calendar.MONTH, 1); // FEBRUARY

        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        System.out.println(cal.getTime());
    }
}

Ответ 5

Я хотел бы внести современный ответ.

    ZonedDateTime endOfFebruary2010 = LocalDate.of(2010, Month.MARCH, 1)
            .atStartOfDay(ZoneId.systemDefault())
            .minusNanos(1);
    System.out.println(endOfFebruary2010);

Запуск в моем часовом поясе:

2010-02-28T23: 59: +59,999999999 + 01: 00 [Европа/Копенгаген]

Распечатка такая же, независимо от времени года и месяца, когда вы ее запускаете. Зависимость от часового пояса может быть неудачной, но ее можно исправить, указав, какой часовой пояс вам нужен, например ZoneId.of("Asia/Oral"). Я использую и рекомендую java.time, современный API дат и времени Java.

Если вам необходим старомодный объект java.util.Date (и только в этом случае), конвертировать:

    Date oldFashionedDate = Date.from(endOfFebruary2010.toInstant());
    System.out.println(oldFashionedDate);

Вс Фев 28 23:59:59 CET 2010

Если вам понадобилось только количество дней в месяц (это было задано по второму вопросу):

    YearMonth ym = YearMonth.of(2011, Month.FEBRUARY);
    int numDays = ym.lengthOfMonth();
    System.out.println(numDays);

28

Насколько я понимаю, ваш реальный вопрос:

... Мне было интересно, действительно ли это было разыскиваемое поведение. Какие-нибудь мысли?

Я твердо верю, что это желание поведения, что конструктор no-arg GregorianCalendar возвращает текущий день и текущее время суток. И что Calender.set() устанавливает только поля, которые вы явно задали, и пытается сохранить остальные поля без изменений. И что 31 февраля 2010 года переполняется в марте без каких-либо признаков ошибки, поскольку в месяц было всего 28 дней. Сочетание этих проектных решений приводит меня к неизбежному выводу: поведение, которое вы наблюдаете, является по дизайну.

Если вы считаете, что это плохой дизайн, мы много согласны с вами. Именно поэтому замена для Calendar и GregorianCalendar появилась с java.time четыре года назад. Вам больше не понадобится использовать Calendar.

Бронирование: наш инструмент по-прежнему зависит от Java 1.7

java.time отлично работает на Java 7. Для этого требуется как минимум Java 6.

  • В Java 8 и более поздних версиях и на более новых устройствах Android (от уровня API 26, как я сказал), современный API встроен.
  • В Java 6 и 7 получите ThreeTen Backport, backport новых классов (ThreeTen для JSR 310, см. Ссылки внизу).
  • На (старше) Android используется версия Android ThreeTen Backport. Его называют ThreeTenABP. И убедитесь, что вы импортируете классы даты и времени из org.threeten.bp с субпакетами.

связи

Ответ 6

Причина должна заключаться в том, что MONTH имеет логическую структуру, подобную перечислению. Вы можете легко заполнить и прочитать Массивы/Коллекции/Списки. Из-за интернационализации он должен быть перечислимым (косвенным доступом). DAY - это просто доступный Integer. Это различие.

Ответ 7

Календарь начинается с текущего дня - 31 мая 2010 года в вашем примере. Когда вы устанавливаете месяц до февраля, дата изменяется до 31 февраля 2010 года, которая нормализовалась до 3 марта 2010 года, поэтому cal.getActualMaximum(Calendar.DAY_OF_MONTH) возвращает 31 марта.

Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, 2010);
c.set(Calendar.MONTH, Calendar.MAY);
c.set(Calendar.DAY_OF_MONTH, 31);
System.out.println(c.getTime());
c.set(Calendar.MONTH, Calendar.FEBRUARY);
System.out.println(c.getTime());

выход:

Mon May 31 20:20:25 GMT+03:00 2010
Wed Mar 03 20:20:25 GMT+03:00 2010

Чтобы исправить код, вы можете добавить cal.clear(); или установить день 1..28 перед началом месяца

Ответ 8

Проблема в том, что DAY_OF_MONTH - 1, день 0 на один день меньше!