为什么Java日历中的一月月为0 ?

java.util.Calendar中,January定义为第0个月,而不是第1个月。这有什么具体原因吗?

我看到很多人对此感到困惑……

192275 次浏览

我想说是懒惰。数组从0开始(每个人都知道);一年中的月份是一个数组,这让我相信Sun的一些工程师只是懒得在Java代码中加入这个小细节。

因为程序员痴迷于基于0的索引。好吧,实际情况要比这复杂一些:当您使用基于0的索引来处理底层逻辑时,它更有意义。但总的来说,我还是会坚持我的第一句话。

除了DannySmurf关于懒惰的回答之外,我还要补充一句,这是为了鼓励你使用常量,比如Calendar.JANUARY

这只是Java日期/时间API这一可怕的混乱的一部分。列出它的问题将花费很长时间(而且我确信我不知道一半的问题)。不可否认,处理日期和时间是很棘手的,但不管怎样。

帮你自己一个忙,使用Joda的时间代替,或者可能使用jsr - 310

编辑:至于原因——正如在其他回答中提到的,这很可能是由于旧的C api,或者只是一种从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作为两者的月基数)
  • 可变性——使用不可变类型使得更容易与真正有效的一起工作
  • 类型集不足:DateCalendar作为不同的东西很好, 但是“local”和“zoned”值之间的分离缺失了,就像date/time和date vs time
  • 一样
  • 一个API会导致丑陋的代码和神奇的常量,而不是明确命名的方法
  • 一个很难解释的API——所有关于什么时候重新计算的业务等等
  • 使用无参数构造函数默认为“now”,这导致代码难以测试
  • Date.toString()实现总是使用系统本地时区(在此之前,许多Stack Overflow用户感到困惑)

基于C的语言在某种程度上复制了C。tm结构(在time.h中定义)有一个整数字段tm_mon,(注释)范围为0-11。

基于C的语言从索引0开始数组。因此,这对于以tm_mon作为索引的月份名称数组输出字符串非常方便。

可能是因为C的“struct tm”也有同样的功能。

就我个人而言,我认为Java日历API的奇特之处表明,我需要摆脱以格里高利为中心的思维方式,并尝试在这方面更加不可知论地编程。具体来说,我再次学会了避免对月份之类的常量进行硬编码。

下面哪个选项更可能是正确的?

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


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

这说明了Joda Time让我有点恼火的一件事——它可能会鼓励程序员从硬编码常数的角度思考问题。(不过只有一点点。Joda并不是迫使程序员编程不好。)

在Java 8中,有一个新的日期/时间API JSR 310更加合理。规范负责人与JodaTime的主要作者相同,他们共享许多相似的概念和模式。

它本身并没有定义为0,而是定义为Calendar.January。这是使用整数作为常量而不是枚举的问题。日历。1月== 0。

关于这个问题有很多答案,但无论如何我将给出我的观点。 如前所述,这种奇怪行为背后的原因来自POSIX C time.h,其中月份存储在0-11范围内的int类型中。 要解释为什么,可以这样看;年和日在口语中被认为是数字,但月份有自己的名字。因为一月是第一个月,它将被存储为偏移量0,数组的第一个元素。monthname[JANUARY]将是"January"。一年中的第一个月是第一个月数组元素

另一方面,由于日期数字没有名称,将它们存储在0-30的int型中会令人困惑,并且会添加大量day+1指令用于输出,当然,也容易出现很多错误。

也就是说,这种不一致是令人困惑的,特别是在javascript中(它也继承了这个“特性”),这是一种脚本语言,它应该从语言中抽象出来。

博士TL;:因为月份有名称,而月份的日期没有。

对我来说,没有人比mindpro.com更能解释它:

陷阱

java.util.GregorianCalendar的bug和陷阱比 old java.util.Date类,但它仍然不是野餐 如果夏令时刚开始的时候有程序员的话 如果提案被提出,他们会以疯狂而棘手为由予以否决。与 夏令时,有一个基本的模糊性。在秋天的时候 你把时钟调慢一小时到凌晨两点,就有两个不同的时间 都是当地时间凌晨1:30。你可以告诉他们 只有当你记录下你是打算使用夏时制还是 标准时间的阅读。< / p > 不幸的是,没有办法告诉GregorianCalendar哪个是你的 的意图。你必须用假人告诉它当地时间 以避免歧义。程序员通常关闭他们的 关注这个问题,希望在此期间没有人采取任何行动 小时。< / p > < p >千年虫。这些错误仍然没有从Calendar类中清除。 甚至在JDK (Java Development Kit) 1.3中也有一个2001年的bug。考虑 代码如下:

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 */

该错误在2001/01/01 MST上午7点消失。

GregorianCalendar由一个巨大的int类型的堆控制 魔术常量。这种技术彻底摧毁了 编译时错误检查。例如,获取你使用的月份 GregorianCalendar. get(Calendar.MONTH)); < / p >

GregorianCalendar有raw GregorianCalendar.get(Calendar.ZONE_OFFSET)和夏令时 GregorianCalendar. get( Calendar. DST_OFFSET),但没有办法得到 使用的实际时区偏移量。你必须分别得到这两个

GregorianCalendar.set( year, month, day, hour, minute)未设置

DateFormatGregorianCalendar不能正确匹配。你必须

.

. 如果用户没有正确配置他的时区,它将默认

在公历中,月份从1月开始编号=0, 而不是像地球上其他人一样。然而一天从1点开始 就像星期天=1,星期一=2,星期六=7一样。然而, DateFormat。

. parse的行为与January=1的传统方式相同

因为用月份计算要简单得多。

12月之后的一个月就是1月,但是要算出这个,通常你需要用月份的数字来计算

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

我知道!我可以通过使用模量12来快速解决这个问题。

(12 + 1) % 12 = 1

在11月之前的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

所有月份的工作都是一样的,没有必要进行变通。

因为所有东西都是从0开始的。这是Java编程的一个基本事实。如果有一件事偏离了这一点,那么就会导致一系列的混乱。让我们不要争论它们的构成和代码。

java.time.Month

Java为您提供了另一种使用基于1的索引的方法。使用java.time.Month enum。为12个月中的每个月预定义一个对象。他们在1月到12月之间分别有1-12号;调用getValue获取该数字。

使用Month.JULY(给出7) 而不是Calendar.JULY(给你6).

(import java.time.*;)

博士tl;

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

2

细节

乔恩·斯基特的回答是正确的。

现在我们有了这些麻烦的旧遗留日期时间类的现代替代品:java.time类。

java.time.Month

在这些类中有Month 枚举。枚举携带一个或多个预定义对象,这些对象在装入类时自动实例化。在Month上,我们有十几个这样的对象,每个对象都有一个名称:JANUARYFEBRUARYMARCH,等等。每一个都是static final public类常量。您可以在代码中的任何地方使用和传递这些对象。例如:someMethod( Month.AUGUST )

幸运的是,它们有正确的编号,1-12,其中1是一月,12是十二月。

为特定的月份号(1-12)获取Month对象。

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

相反,向Month对象询问其月份号。

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

这个类上还有很多方便的方法,比如知道每个月的天数。这个类甚至可以生成本地化名称 of The month。

您可以获得以各种长度或缩写形式表示的月份的本地化名称。

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

fevrier

另外,你应该在代码基周围传递此枚举的对象,而不仅仅是整数。这样做可以提供类型安全,确保值的有效范围,并使代码更具自文档性。如果不熟悉Java中令人惊讶的强大枚举功能,请参阅甲骨文教程

你可能还会发现YearYearMonth类很有用。


关于java.time

java.time框架内置于Java 8及更高版本中。这些类取代了麻烦的旧遗产日期-时间类,如java.util.Date.Calendar, &java.text.SimpleDateFormat

现在在维护模式中的Joda-Time项目建议迁移到java.time。

要了解更多信息,请参见甲骨文教程。搜索Stack Overflow可以找到很多例子和解释。规格是JSR 310

从哪里获取java。时间类?

因为语言写作比看起来要难得多,特别是处理时间比大多数人想象的要难得多。关于这个问题的一小部分(在现实中,不是Java),请参阅YouTube视频“时间问题”。Timezones - Computerphile" at https://www.youtube.com/watch?v=-5wpm-gesOY。如果你在困惑中笑掉了头,不要感到惊讶。

真正的原因

当我们弃用了Date的大部分并添加了新的 日历类中,我们已经固定了Date最大的烦恼:事实 一月是0月。我们当然应该这么做,但不幸的是 我们没有。我们担心如果Date 使用以零为基数的月份,而Calendar使用以1为基数的月份。还有一些 程序员可能会。但事后来看,事实是 日历仍然以零为基础已经造成了巨大的损失 这可能是Java历史上最大的错误 国际API的。< / p >

引用自Laura Werner的Java国际日历,链接在底部。

更好的选择:java.time

这可能只是重复别人说过的话,把旧的和设计不佳的Calendar类扔出去,使用java。时间,现代Java日期和时间API。从1月的1到12月的12,月份的编号始终是合理的。

如果您从尚未升级到java的遗留API中获得Calendar。时间,首先要做的是转换为现代的ZonedDateTime。根据你的需要,你可以从那里做进一步的转换。在大多数情况下,你得到的Calendar对象实际上总是GregorianCalendar子类的一个实例(因为Calendar类本身是抽象的)。demonstreate:

    Calendar oldfashionedCalendarObject = Calendar.getInstance();
ZonedDateTime zdt
= ((GregorianCalendar) oldfashionedCalendarObject).toZonedDateTime();
    

System.out.println(zdt);
System.out.format("Month is %d or %s%n", zdt.getMonthValue(), zdt.getMonth());

当我刚刚在我的时区运行时输出:

2021-03-17T23:18:47.761+01:00[Europe/Copenhagen]
Month is 3 or MARCH

链接

设置月份为“日历”。MARCH,或者比较,看看它是否== Calendar。比如六月。

Date和日历类可以追溯到Java的早期,当时人们还在摸索,它们被广泛认为设计得不是很好。

如果今天用相同的设计创建Calendar,而不是int类型的Calendar。JUNE等等,他们会用枚举。