Странное поведение с 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
на один день меньше!