为什么在Java日历中的1月份0?
在java.util.Calendar
,1月定义为月0,而不是1月。有什么具体的原因呢?
我看到很多人对此感到困惑
这只是Javadate/时间API的可怕混乱的一部分。 列出什么是错误的将需要很长的时间(我相信我不知道一半的问题)。 不可否认,date和时间的工作是棘手的,但无论如何。
帮你一个忙,改用Joda Time ,或者JSR-310 。
编辑:至于为什么 – 正如其他答案中指出的,这可能是由于旧的C API,或只是从0开始一切的一般感觉…除了天以1开始,当然。 我怀疑原始实施团队之外的任何人是否真的可以说明理由 – 但是,我再次敦促读者不要太担心为什么会做出不好的决定,而要关注java.util.Calendar
的整体情况,find更好的东西。
有利于使用基于0的索引的一点是它使“名称数组”更容易:
// I "know" there are 12 months String[] monthNames = new String[12]; // and populate... String name = monthNames[calendar.get(Calendar.MONTH)];
当然,只要您获得13个月的日历,就会失败…但是至less指定的大小是您期望的月数。
这不是一个很好的理由,但这是一个原因
编辑:作为一个评论的请求types的一些想法,我认为是错误的date/日历:
- 令人惊讶的基地(1900年作为date的基数,诚然为过时的build设者; 0作为两个月的基数)
- 可变性 – 使用不可变types使得使用真正有效的值更简单
- 一组不足的types:将
Date
和Calendar
作为不同的事情是很好的,但是“本地”和“分区”值的分离是丢失的,因为date/时间vsdate与时间 - 使用魔术常量导致丑陋代码的API,而不是明确命名的方法
- 一个非常难以推理的API – 所有关于什么时候重新计算的业务
- 使用无参数构造函数默认为“now”,这导致难以testing的代码
- 总是使用系统本地时区的
Date.toString()
实现(现在已经让很多Stack Overflow用户感到困惑)
C语言在一定程度上复制C语言。 tm
结构(在time.h
定义)有一个整数字段tm_mon
,(注释的)范围是0-11。
基于C的语言在索引0处开始数组。因此,这对于在月份名称数组中输出string很方便,其中tm_mon
作为索引。
因为和月份做math要容易得多。
一月之后的十二月就是一月份,但是要正确地计算出来,你需要拿月份数字来做math
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
所有的月份工作都是一样的,解决方法是没有必要的。
这个问题已经有了很多的答案,但是我仍然会就这个问题发表看法。 如前所述,这种奇怪的行为背后的原因来自POSIX C time.h
,其中存储在0到11范围内的int的月份。 为什么要这样解释呢? 几年和几天被认为是口语的数字,但几个月有自己的名字。 因此,因为1月份是第一个月,它将被存储为偏移0,第一个数组元素。 monthname[JANUARY]
"January"
monthname[JANUARY]
将是"January"
。 在这一年的第一个月是第一个月的数组元素。
另一方面,天数由于没有名字,因此将它们存储为0-30的整数将会令人困惑,并会增加大量的day+1
的输出指令,当然,这些错误也容易出现。
这就是说,不一致是令人困惑的,特别是在JavaScript(它也inheritance了这个“特性”),这是一个脚本语言,这应该是抽象的远离语言。
TL; DR :因为月份的名字和月份的日子不一样。
可能是因为C的“struct tm”是一样的。
在Java 8中,有一个更新的date/时间API JSR 310 。 规格领先与JodaTime的主要作者相同,他们有许多相似的概念和模式。
我会说懒惰。 数组从0开始(每个人都知道); 一年中的几个月都是一个数组,这让我相信Sun的一些工程师并没有把这个小小的精灵放进Java代码中。
就我个人而言,我认为Java日历API的奇怪性表明我需要脱离以公历为中心的思维模式,并尝试在这方面更加无意义地进行编程。 具体来说,我再次学会了避免像几个月的事情硬编码常量。
以下哪一项更可能是正确的?
if (date.getMonth() == 3) out.print("March"); if (date.getMonth() == Calendar.MARCH) out.print("March");
这说明了一件让我对Joda时间感到厌烦的事情 – 这可能会鼓励程序员用硬编码的常量来思考。 (但是,只有一点点,就好像乔达迫使程序员编写糟糕的程序一样)。
因为程序员痴迷于基于0的索引。 好吧,这比以下更复杂一点:当您使用低级逻辑来使用基于0的索引时,这会更有意义。 但总的来说,我仍然坚持我的第一句话。
java.util.Month
Java为您提供了另一种使用基于1的索引的方法。 使用java.time.Month
枚举。 一个对象是为十二个月的每一个预定义的。 1月至12月每1-12分配一个号码; 调用getValue
作为数字。
利用Month.JULY
(给你7)而不是Calendar.JULY
(给你6)。
(import java.time.*;)
对我来说,没有人比mindpro.com更好地解释它:
陷阱
java.util.GregorianCalendar
比old java.util.Date
类有更less的bug和陷阱,但它仍然没有野餐。如果程序员在最初提出夏令时的情况下,他们会否认它是疯狂和棘手的。 夏时制有一个基本的模糊性。 在秋季,当你在凌晨2点把你的时钟恢复一个小时时,有两个不同的时刻,两个在当地时间上午1点半。 只有在logging是否打算使用夏令时或标准时间时,才能将其分开。
不幸的是,没有办法告诉你想要的
GregorianCalendar
。 您必须求助于使用虚拟UTC TimeZone告诉当地时间以避免模糊。 程序员通常会关注这个问题,只希望在这个小时内没有人做任何事情。千年虫。 这些错误仍然没有超出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年1月1日上午7点,MST的bug消失。
GregorianCalendar
由一大堆无types的int魔术常量控制。 这种技术完全破坏了编译时错误检查的任何希望。 例如要获取使用GregorianCalendar. get(Calendar.MONTH));
的月份GregorianCalendar. get(Calendar.MONTH));
GregorianCalendar. get(Calendar.MONTH));
GregorianCalendar
具有生GregorianCalendar.get(Calendar.ZONE_OFFSET)
和夏令营GregorianCalendar. get( Calendar. DST_OFFSET)
GregorianCalendar. get( Calendar. DST_OFFSET)
,但没有办法获得实际的时区偏移正在使用。 你必须把这两个分开,并把它们加在一起。
GregorianCalendar.set( year, month, day, hour, minute)
不将秒设置为0。
DateFormat
和GregorianCalendar
不正确网格。 您必须两次指定日历,一次间接指定date。如果用户没有正确configuration他的时区,它将安静地默认为PST或GMT。
在GregorianCalendar中,月份从1月份开始= 0开始编号,而不是像地球上其他人一样编号。 然而,从1开始,星期天= 1,星期一= 2,…星期六= 7,星期几开始。 然而,DateFormat。 以1 = 1的传统方式进行parsing。
除了DannySmurf的懒惰答案之外,我还要补充一点,就是鼓励你使用常量,比如Calendar.JANUARY
。
它不是完全定义为零本身,它被定义为Calendar.January。 这是使用int作为常量而不是枚举的问题。 Calendar.January == 0。
Jon Skeet的答案是正确的。
现在我们已经替代了那些麻烦的旧的遗留date时间类: java.time类。
java.time.Month
在这些类中是Month
枚举 ,定义了十二个对象,每年一月一月到十二月。 幸运的是,他们的理智编号是1-12,其中1月份是1月份,12月份是12月份。
获取特定月份编号(1-12)的Month
对象。
Month month = Month.of( 2 ); // 2 → February.
去另一个方向,问一个Month
对象的月份编号。
int monthNumber = Month.FEBRUARY.getValue(); // February → 2.
这个class上还有很多其他方便的方法,比如知道每个月的天数 。 该类甚至可以生成本地化的月份名称 。
你可以得到不同长度或缩写的月份本地化名称。
String output = Month.FEBRUARY.getDisplayName( TextStyle.FULL , Locale.CANADA_FRENCH );
FEVRIER
另外,你应该在你的代码库中传递这个枚举的对象,而不仅仅是整数 。 这样做可以提供types安全性,确保有效的值范围,并使代码更加自我logging。 如果不熟悉Java中惊人强大的枚举工具,请参阅Oracle教程 。
你也可能会发现有用的Year
和YearMonth
类。
关于java.time
java.time框架内置于Java 8及更高版本中。 这些类取代了麻烦的旧的遗留date时间类,如java.util.Date
, .Calendar
和java.text.SimpleDateFormat
。
Joda-Time项目现在处于维护模式 ,build议迁移到java.time。
要了解更多信息,请参阅Oracle教程 。 并search堆栈溢出了很多例子和解释。 规范是JSR 310 。
从何处获取java.time类?
- Java SE 8和SE 9及更高版本
- 内置。
- 带有捆绑实现的标准Java API的一部分。
- Java 9增加了一些小function和修复。
- Java SE 6和SE 7
- 大部分的java.timefunction都被移植到了ThreeTen-Backport中的 Java 6&7中。
- Android的
- ThreeTenABP项目专门针对Android,采用了ThreeTen-Backport (上文提到)。
- 请参阅如何使用…。
ThreeTen-Extra项目将java.time扩展到其他类。 这个项目是未来可能增加java.time的一个试验场。 你可能会在这里find一些有用的类,比如Interval
, YearWeek
, YearQuarter
等等 。
因为一切都从0开始。这是用Java编程的一个基本事实。 如果有一件事情与此背道而驰,那么这将导致一丝混乱。 我们不要争论他们的形成和代码。