Почему январь месяц 0 в календаре Java?

300

В java.util.Calendarянваре январь определяется как месяц 0, а не месяц 1. Есть ли для этого какая-либо конкретная причина?

Я видел, как многие люди запутались в этом ...

Стефан Бонниес
источник
4
Разве это не деталь реализации, поскольку существуют константы ЯНВАРЬ, ФЕВРАЛЬ и т. Д.? Классы даты предшествуют правильной поддержке перечисления java.
gnud
6
Еще больше раздражает - почему тут декабрь?
Мэтт б
40
@gnud: Нет, это не детали реализации. Это причиняет боль, когда вам дают целое число в «естественной» базе (т.е. Jan = 1), и вам нужно использовать его с API календаря.
Джон Скит
1
@matt b: для негригорианских календарей (лунных календарей и т. д.) тринадцать месяцев. Вот почему лучше не думать в терминах чисел, а позволить Календарю сделать это локализацией.
erickson
7
13-месячный аргумент не имеет смысла. Если это так, почему бы не иметь дополнительный месяц 0 или 13?
Куинн Тейлор

Ответы:

323

Это всего лишь часть ужасного беспорядка, который является API даты / времени Java. Список того, что с ним не так, занял бы очень много времени (и я уверен, что я не знаю и половины проблем). По общему признанию, работать с датами и временем сложно, но в любом случае.

Сделайте себе одолжение и используйте вместо этого Joda Time или, возможно, JSR-310 .

РЕДАКТИРОВАТЬ: Что касается причин, почему - как отмечалось в других ответах, это вполне может быть связано со старыми API C, или просто общее чувство, начиная все с 0 ... за исключением того, что дни начинаются с 1, конечно. Я сомневаюсь, что кто-то за пределами первоначальной команды разработчиков мог бы на самом деле изложить причины - но, опять же, я бы призвал читателей не беспокоиться так сильно о том, почему были приняты плохие решения, а взглянуть на всю гамму мерзости java.util.Calendarи найти что-то лучшее.

Один момент , который находится в пользу использования 0 на основе индексов является то , что он делает такие вещи , как «массивы имен» проще:

// I "know" there are 12 months
String[] monthNames = new String[12]; // and populate...
String name = monthNames[calendar.get(Calendar.MONTH)];

Конечно, это не сработает, как только вы получите календарь с 13 месяцами ... но, по крайней мере, указанный размер равен ожидаемому числу месяцев.

Это не хорошая причина, но это причина ...

РЕДАКТИРОВАТЬ: В качестве комментария просит некоторые идеи о том, что я думаю, что неправильно с Дата / Календарь:

  • Удивительные основы (1900 как годовая база в Date, по общему признанию для устаревших конструкторов; 0 как месячная база в обоих)
  • Изменчивость - использование неизменяемых типов значительно упрощает работу с действительно эффективными значениями
  • Недостаточный набор типов: это приятно иметь Dateи Calendarкак разные вещи, но отсутствует разделение «локальных» и «зонированных» значений, как и дата / время против даты против времени
  • API, который приводит к некрасивому коду с магическими константами, вместо четко названных методов
  • API, о котором очень сложно рассуждать - все дело в том, когда что-то пересчитывается и т. Д.
  • Использование конструкторов без параметров по умолчанию «сейчас», что приводит к сложному тестированию кода
  • Date.toString()Реализация , которая всегда использует локальную временную зону системы (это заблуждение многих пользователей переполнению стека до сих пор)
Джон Скит
источник
14
... а что с устаревшими полезными простыми методами Date? Теперь я должен использовать этот ужасный объект Calendar сложными способами, чтобы делать вещи, которые раньше были простыми.
Брайан Кноблаух
3
@ Брайан: Я чувствую твою боль. Опять же, Joda Time проще :) (Фактор неизменности также делает вещи намного приятнее работать.)
Джон Скит
8
Вы не ответили на вопрос.
Zeemee
2
@ user443854: Я перечислил некоторые моменты в редактировании - посмотрите, поможет ли это.
Джон Скит
2
Если вы используете Java 8, вы можете отказаться от класса Calendar и переключиться на новый и изящный API DateTime . Новый API также включает неизменяемый / потокобезопасный DateTimeFormatter, который является большим улучшением по сравнению с проблемным и дорогим SimpleDateFormat.
ccpizza
43

Потому что делать математику с месяцами намного проще.

1 месяц после декабря - январь, но чтобы понять это, вам нужно взять номер месяца и заняться математикой.

12 + 1 = 13 // What month is 13?

Я знаю! Я могу исправить это быстро, используя модуль 12.

(12 + 1) % 12 = 1

Это прекрасно работает в течение 11 месяцев до ноября ...

(11 + 1) % 12 = 0 // What month is 0?

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

((11 - 1 + 1) % 12) + 1 = 12 // Lots of magical numbers!

Теперь давайте подумаем о проблеме с месяцами 0 - 11.

(0 + 1) % 12 = 1 // February
(1 + 1) % 12 = 2 // March
(2 + 1) % 12 = 3 // April
(3 + 1) % 12 = 4 // May
(4 + 1) % 12 = 5 // June
(5 + 1) % 12 = 6 // July
(6 + 1) % 12 = 7 // August
(7 + 1) % 12 = 8 // September
(8 + 1) % 12 = 9 // October
(9 + 1) % 12 = 10 // November
(10 + 1) % 12 = 11 // December
(11 + 1) % 12 = 0 // January

Все месяцы работают одинаково и обходить не нужно.

arucker
источник
5
Это удовлетворительно. По крайней мере, в этом безумии есть какая-то ценность!
moljac024
«Множество магических чисел» - нет, это просто то, что появляется дважды.
user123444555621
Возвращение к месяцу все еще довольно странно, хотя, благодаря неудачному использованию C оператора «остаток», а не «модуля». Я также не уверен, как часто нужно увеличивать месяц, не корректируя год, и если месяцы идут 1-12, не возникает проблем с `while (month> 12) {month- = 12; год ++;}
суперкат
2
Так как нормальные функции, такие как DateTime.AddMonths, слишком сложны для правильной реализации в lib, мы должны выполнить математику, которую вы описали сами ... Mmmmmkay
nsimeonov
8
Я не понимаю эти возражения - ((11 - 1 + 1) % 12) + 1 = 12просто в (11 % 12) + 1течение месяцев 1..12 вам просто нужно добавить 1 после выполнения по модулю. Никакой магии не требуется.
mfitzp
35

Языки на основе Си до некоторой степени копируют Си. tmСтруктура (определенно в time.h) имеет целое поле tm_monс (комментариями) диапазоном 0-11.

Для языков на основе C массивы начинаются с индекса 0. Таким образом, это было удобно для вывода строки в массиве названий месяцев с tm_monиндексом.

stesch
источник
22

На это было много ответов, но я все равно выскажу свое мнение по этому вопросу. Причина этого странного поведения, как указано ранее, исходит от POSIX C, time.hгде месяцы хранятся в целых числах с диапазоном 0-11. Чтобы объяснить почему, посмотрите на это так; годы и дни считаются числами в разговорной речи, но месяцы имеют свои собственные названия. Так как январь является первым месяцем, он будет сохранен как смещение 0, первый элемент массива. monthname[JANUARY]будет "January". Первый месяц в году является первым элементом массива месяца.

Числа дня, с другой стороны, поскольку у них нет имен, хранение их в целых числах от 0 до 30 может привести к путанице, добавить много day+1инструкций для вывода и, конечно, быть склонным к множеству ошибок.

При этом несоответствие сбивает с толку, особенно в javascript (который также унаследовал эту «особенность»), языке сценариев, где его следует абстрагировать далеко от langague.

TL; DR : потому что месяцы имеют названия, а дни месяца - нет.

пиксель битворкс
источник
1
«У месяцев есть имена, а у дней нет». Вы когда-нибудь слышали о «пятнице»? ;) Хорошо, я предполагаю, что вы имели в виду «..дней месяца нет» - возможно, вам стоит заплатить за редактирование вашего (в противном случае хорошего) ответа. :-)
Эндрю Томпсон
0/0/0000 лучше отображать как "00-Jan-0000" или "00-XXX-0000"? ИМХО, много кода было бы чище, если бы было тринадцать месяцев, но месяцу 0 было дано фиктивное имя.
суперкат
1
Это интересный вариант, но 0/0/0000 не является действительной датой. как бы вы сделали 40/40/0000?
Пиксель Битворк
12

В Java 8 есть новый API даты / времени JSR 310, который является более разумным. Спецификация ведет себя так же, как основной автор JodaTime, и у них много общих концепций и шаблонов.

Алекс Миллер
источник
2
Новый API Date Time теперь является частью Java 8
mschenk74
9

Я бы сказал, лень. Массивы начинаются с 0 (это знают все); месяцы года - это массив, и это заставляет меня поверить, что какой-то инженер в Sun просто не удосужился внести эту маленькую тонкость в код Java.

TheSmurf
источник
9
Нет, я бы не стал Более важно оптимизировать эффективность своих клиентов, чем программистов. Поскольку этот клиент проводит здесь время, спрашивая, у него ничего не получилось.
TheSmurf
2
Это совершенно не связано с эффективностью - это не значит, что месяцы хранятся в массиве, а вам нужно 13, чтобы представить 12 месяцев. Дело не в том, чтобы сделать API таким удобным для пользователя, каким он должен был быть в первую очередь. Джош Блох трясет на Дате и Календаре в "Эффективной Яве". Очень немногие API-интерфейсы идеальны, а API-интерфейсы даты / времени в Java играют неудачную роль, будучи обманутыми. Это жизнь, но давайте не будем притворяться, что это как-то связано с эффективностью.
Куинн Тейлор
1
Почему бы тогда не считать дни от 0 до 30? Это просто противоречиво и небрежно.
Juangui Jordán
9

Вероятно, потому что C "struct tm" делает то же самое.

Пол Томблин
источник
8

Потому что программисты помешаны на индексах, основанных на 0. Хорошо, это немного сложнее, чем это: имеет смысл, когда вы работаете с низкоуровневой логикой, чтобы использовать индексирование на основе 0. Но по большому счету я все же буду придерживаться своего первого предложения.

Дина
источник
1
Это еще одна из тех идиомы / привычек , которые идут путь назад к ассемблере или на машинном языке , где все сделано с точки зрения смещения, а не индексов. Обозначение массива стало сокращением для доступа к смежным блокам, начиная со смещения 0.
Кен Джентл
4

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

Что из следующего наиболее вероятно будет правильным?

if (date.getMonth() == 3) out.print("March");

if (date.getMonth() == Calendar.MARCH) out.print("March");

Это иллюстрирует одну вещь, которая немного раздражает меня в Joda Time - это может побудить программистов думать в терминах жестко закодированных констант. (Хотя и немного. Это не так, как будто Joda заставляет программистов плохо программировать.)

Пол Бринкли
источник
1
Но какая схема с большей вероятностью доставит вам головную боль, когда в вашем коде нет константы - у вас есть значение, которое является результатом вызова веб-службы или чего-то еще.
Джон Скит
Конечно, этот вызов веб-службы также должен использовать эту константу. :-) То же самое касается любого внешнего абонента. Как только мы установили, что существует несколько стандартов, становится очевидной необходимость их применения. (Надеюсь, я понял ваш комментарий ...)
Пол Бринкли
3
Да, мы должны применять стандарт, который почти все остальное в мире использует при выражении месяцев - стандарт на основе 1.
Джон Скит
Ключевое слово здесь «почти». Очевидно, что Jan = 1 и т. Д. Кажется естественным в системе дат с чрезвычайно широким использованием, но зачем позволять себе делать исключение, избегая жестко закодированных констант, даже в этом одном случае?
Пол Бринкли
3
Потому что это облегчает жизнь. Это просто так. Я никогда не сталкивался с проблемой «один на один» с системой месячного отсчета. Я видел много таких ошибок с Java API. Игнорировать то, что делают все остальные в мире, просто бессмысленно.
Джон Скит
4

Для меня никто не объясняет это лучше, чем mindpro.com :

Gotchas

java.util.GregorianCalendarимеет гораздо меньше ошибок и ошибок, чем old java.util.Dateкласс, но это все еще не пикник.

Если бы программисты впервые предложили переход на летнее время, они бы наложили вето на это безумие и неразрешимость. С переходом на летнее время возникает фундаментальная неопределенность. Осенью, когда вы переводите часы на один час в 2 часа ночи, появляются два разных момента времени, которые называются 1:30 утра по местному времени. Вы можете различить их только в том случае, если записали, предполагали ли вы переход на летнее или стандартное время с показаниями.

К сожалению, нет способа сказать, GregorianCalendarчто вы намеревались. Вы должны прибегнуть к указанию местного времени с помощью фиктивной UTC TimeZone, чтобы избежать двусмысленности. Программисты обычно закрывают глаза на эту проблему и просто надеются, что никто не сделает ничего в течение этого часа.

Ошибка тысячелетия Ошибки все еще не из классов Календаря. Даже в JDK (Java Development Kit) 1.3 есть ошибка 2001 года. Рассмотрим следующий код:

GregorianCalendar gc = new GregorianCalendar();
gc.setLenient( false );
/* Bug only manifests if lenient set false */
gc.set( 2001, 1, 1, 1, 0, 0 );
int year = gc.get ( Calendar.YEAR );
/* throws exception */

Ошибка исчезает в 7 утра 2001/01/01 для MST.

GregorianCalendarуправляется гигантской кучей нетипизированных int магических констант. Этот метод полностью разрушает любую надежду на проверку ошибок во время компиляции. Например, чтобы получить месяц, который вы используете GregorianCalendar. get(Calendar.MONTH));

GregorianCalendarимеет сырое GregorianCalendar.get(Calendar.ZONE_OFFSET)и летнее время GregorianCalendar. get( Calendar. DST_OFFSET), но не позволяет получить фактическое смещение часового пояса. Вы должны получить эти два отдельно и сложить их вместе.

GregorianCalendar.set( year, month, day, hour, minute) не устанавливает секунды на 0.

DateFormatи GregorianCalendarне мешайте правильно. Вы должны указать Календарь дважды, один раз косвенно как Дата.

Если пользователь неправильно настроил свой часовой пояс, он по умолчанию будет тихо установлен на PST или GMT.

В GregorianCalendar месяцы нумеруются, начиная с января = 0, а не 1, как все остальные на планете. Тем не менее, дни начинаются с 1, как и дни недели с воскресенья = 1, понедельника = 2,… субботы = 7. Все же DateFormat. parse ведет себя традиционным образом с января = 1.

Эдвин Далорсо
источник
4

java.util.Month

Java предоставляет вам другой способ использования индексов на основе 1 в течение нескольких месяцев. Используйте java.time.Monthперечисление. Один объект предопределен для каждого из двенадцати месяцев. У них есть номера, присвоенные каждому 1-12 на январь-декабрь; позвоните getValueпо номеру.

Используйте Month.JULY(Дает вам 7) вместо Calendar.JULY(Дает вам 6).

(import java.time.*;)
Digital_Reality
источник
3

ТЛ; др

Month.FEBRUARY.getValue()  // February → 2.

2

подробности

Ответа на этот вопрос Jon тарелочкам является правильным.

Теперь у нас есть современная замена этим проблемным старым классам даты и времени: классы java.time .

java.time.Month

Среди этих классов есть enum . Перечисление содержит один или несколько предопределенных объектов, объектов, которые автоматически создаются при загрузке класса. На нас есть десяток таких объектов, каждый дал имя: , , , и так далее. Каждый из них является константой класса. Вы можете использовать и передавать эти объекты в любом месте вашего кода. Пример:Month MonthJANUARYFEBRUARYMARCHstatic final publicsomeMethod( Month.AUGUST )

К счастью, у них нормальная нумерация, 1-12, где 1 - январь, а 12 - декабрь.

Получить Monthобъект для определенного номера месяца (1-12).

Month month = Month.of( 2 );  // 2 → February.

Идя в другом направлении, спросите у Monthобъекта номер его месяца.

int monthNumber = Month.FEBRUARY.getValue();  // February → 2.

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

Вы можете получить локализованное название месяца, различной длины или сокращения.

String output = 
    Month.FEBRUARY.getDisplayName( 
        TextStyle.FULL , 
        Locale.CANADA_FRENCH 
    );

Février

Кроме того, вы должны передавать объекты этого перечисления вокруг вашей кодовой базы, а не просто целые числа . Это обеспечивает безопасность типов, обеспечивает допустимый диапазон значений и делает ваш код более самодокументируемым. См. Oracle Tutorial, если вы не знакомы с удивительно мощным средством перечисления в Java.

Вы также можете найти полезные Yearи YearMonthклассы.


О java.time

Java.time каркас встроен в Java 8 и более поздних версий. Эти классы вытеснять неприятные старые устаревшие классы даты и времени , такие как java.util.Date, .Calendar, и java.text.SimpleDateFormat.

Проект Joda-Time , находящийся сейчас в режиме обслуживания , рекомендует перейти на java.time.

Чтобы узнать больше, смотрите Oracle Tutorial . И поиск переполнения стека для многих примеров и объяснений. Спецификация JSR 310 .

Где взять классы java.time?

  • Java SE 8 и SE 9 и позже
    • Встроенный.
    • Часть стандартного Java API со встроенной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и SE 7
    • Большая часть функциональности java.time перенесена на Java 6 и 7 в ThreeTen-Backport .
  • Android

Проект ThreeTen-Extra расширяет java.time дополнительными классами. Этот проект является полигоном для возможных будущих дополнений к java.time. Вы можете найти некоторые полезные классы здесь , такие как Interval, YearWeek, YearQuarter, и более .

Базилик Бурк
источник
0

Оно не определено как ноль само по себе, оно определено как Calendar.January. Это проблема использования int как констант вместо перечислений. Календарь. Январь == 0.

Пол Г.Д.
источник
1
Значения одни и те же. API также могут возвращать 0, это идентично константе. Calendar.JANUARY можно было бы определить как 1 - вот и весь смысл. Перечисление было бы хорошим решением, но истинные перечисления не были добавлены в язык до Java 5, а Date существует с самого начала. К сожалению, вы не можете «исправить» такой фундаментальный API, если его использует сторонний код. Лучшее, что можно сделать, - это предоставить новый API и отказаться от старого, чтобы поощрить людей двигаться дальше. Спасибо, Java 7 ...
Куинн Тейлор
0

Потому что написание языка сложнее, чем кажется, а обработка времени, в частности, намного сложнее, чем думает большинство людей. Небольшую часть проблемы (на самом деле это не Java) смотрите в видео на YouTube «Проблема с часами и часовыми поясами - компьютерный файл» по адресу https://www.youtube.com/watch?v=-5wpm-gesOY . Не удивляйтесь, если ваша голова отвалится от смеха в замешательстве.

Tihamér
источник
-1

В дополнение к ответу DannySmurf о лени, я добавлю, что это побуждает вас использовать константы, такие как Calendar.JANUARY.

Powerlord
источник
5
Это все очень хорошо, когда вы явно пишете код для конкретного месяца, но это неприятно, когда у вас есть месяц в «нормальной» форме из другого источника.
Джон Скит
1
Также неприятно, когда вы пытаетесь напечатать значение этого месяца каким-то конкретным способом - вы всегда добавляете к нему 1.
Брайан Уоршоу
-2

Потому что все начинается с 0. Это основной факт программирования на Java. Если что-то будет отклоняться от этого, то это приведет к путанице. Давайте не будем спорить о формировании их и кодировать их.

Syrrus
источник
2
Нет, большинство вещей в реальном мире начинаются с 1. Смещения начинаются с 0, и месяц года не является смещением, это один из двенадцати, точно так же, как день месяца является одним из 31, 30, 29 или 28. Рассматривать месяц как смещение просто капризно, особенно если в то же время мы не относимся к дню месяца одинаково. В чем причина такой разницы?
SantiBailors
в реальном мире начните с 1, в мире Java начните с 0. НО ... Я думаю, это потому, что: - для вычисления дня недели его нельзя сместить для пары вычислений, не добавляя еще пару шагов к это ... - кроме того, оно показывает полные дни месяца, если это необходимо (без путаницы или необходимости проверять февраль). - Для месяца это вынуждает вас выводить данные в формате даты, который следует использовать в любом случае. Кроме того, поскольку число месяцев в году является регулярным, а количество дней в месяце не имеет смысла, имеет смысл объявить массивы и использовать смещение для лучшего соответствия массиву.
Syrrus