在java.util.Calendar
中,一月被定义为第 0 个月,而不是第 1 个月。有什么具体原因吗?
我看到很多人对此感到困惑...
它只是 Java 日期/时间 API 的可怕混乱的一部分。列出它的问题需要很长时间(而且我确信我不知道一半的问题)。诚然,处理日期和时间很棘手,但无论如何。
编辑: 至于原因 - 正如其他答案中所述,这很可能是由于旧的 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 个月的日历,这就会失败......但至少指定的大小是你期望的月数。
这不是一个好理由,但这是一个理由......
编辑:作为评论请求一些关于我认为日期/日历有问题的想法:
Date
和Calendar
作为不同的东西很好,但是缺少“本地”与“分区”值的分离,日期/时间与日期与时间也是如此Date.toString()
实现(以前让许多 Stack Overflow 用户感到困惑)因为用几个月做数学要容易得多。
12 月后的 1 个月是 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
所有月份的工作方式都是一样的,不需要解决问题。
基于 C 的语言在某种程度上复制了 C。tm
结构(定义在 中)time.h
有一个整数字段tm_mon
,(注释)范围为 0-11。
基于 C 的语言在索引 0 处开始数组。因此,这对于在月份名称数组中输出字符串很方便,并tm_mon
作为索引。
对此有很多答案,但无论如何我都会就这个问题发表我的看法。如前所述,这种奇怪行为背后的原因来自 POSIX C time.h
,其中月份存储在范围为 0-11 的 int 中。要解释原因,请这样看;年和日在口语中被认为是数字,但月份有自己的名字。因此,由于一月是第一个月,它将被存储为偏移量 0,即第一个数组元素。monthname[JANUARY]
将是"January"
。一年中的第一个月是第一个月数组元素。
另一方面,日期数字,因为它们没有名称,将它们存储在 int 中作为 0-30 会令人困惑,添加很多day+1
输出指令,当然,容易出现很多错误。
话虽如此,这种不一致是令人困惑的,尤其是在 javascript(它也继承了这个“特性”)中,这是一种脚本语言,它应该被抽象得远离语言。
TL;DR:因为月份有名称,而月份中的日期没有。
在 Java 8 中,有一个更健全的新日期/时间 API JSR 310 。规范负责人与 JodaTime 的主要作者相同,他们共享许多相似的概念和模式。
我会说懒惰。数组从 0 开始(每个人都知道);一年中的月份是一个数组,这让我相信 Sun 的一些工程师只是懒得在 Java 代码中加入这个小细节。
可能是因为 C 的“struct tm”也是如此。
因为程序员痴迷于从 0 开始的索引。好的,它比这更复杂一点:当您使用较低级别的逻辑来使用基于 0 的索引时,它更有意义。但总的来说,我仍然会坚持我的第一句话。
就个人而言,我认为 Java 日历 API 的陌生性表明我需要摆脱以公历为中心的思维方式,并尝试在这方面进行更加不可知论的编程。具体来说,我再次学会了避免像几个月这样的硬编码常量。
以下哪项更可能是正确的?
if (date.getMonth() == 3) out.print("March");
if (date.getMonth() == Calendar.MARCH) out.print("March");
这说明了 Joda Time 让我有点恼火的一件事——它可能会鼓励程序员从硬编码常量的角度来思考。(不过只有一点点。Joda 并没有强迫程序员编程不好。)
对我来说,没有人能比mindpro.com更好地解释它:
陷阱
java.util.GregorianCalendar
与班级相比,错误和陷阱要少得多,old java.util.Date
但仍然没有野餐。如果在第一次提出夏令时时有程序员,他们会否决它,认为它疯狂且难以处理。对于夏令时,存在根本的歧义。在秋天,当您将时钟拨回凌晨 2 点时,有两个不同的时间点,都称为当地时间凌晨 1:30。仅当您在读数中记录您是打算采用夏令时还是标准时间时,您才能将它们区分开来。
不幸的是,无法判断
GregorianCalendar
您的意图。您必须诉诸于使用虚拟 UTC TimeZone 告诉它当地时间,以避免歧义。程序员通常对这个问题视而不见,只希望在这个小时内没有人做任何事情。千年虫。这些错误仍然没有超出日历类。即使在 JDK(Java 开发工具包)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 */
该错误在 2001 年 1 月 1 日上午 7 点消失,用于 MST。
GregorianCalendar
由一大堆无类型的 int 魔法常数控制。这种技术完全破坏了编译时错误检查的任何希望。例如获取您使用的月份GregorianCalendar. get(Calendar.MONTH));
GregorianCalendar
有 rawGregorianCalendar.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。然而日期格式。parse 以传统方式运行,一月=1。
java.time.Month
Java 为您提供了另一种使用基于 1 的索引数月的方法。使用java.time.Month
枚举。为十二个月中的每一个月预定义一个对象。他们在 1 月至 12 月分别为 1-12 分配了编号;拨打电话getValue
号码。
使用Month.JULY
(给你 7)而不是Calendar.JULY
(给你 6)。
(import java.time.*;)
您可能会认为,当我们弃用大部分 Date 并添加新的 Calendar 类时,我们会解决 Date 的最大烦恼:1 月是第 0 个月这一事实。我们当然应该这样做,但不幸的是我们没有。我们担心如果 Date 使用从零开始的月份而 Calendar 使用从 1 开始的月份,程序员会感到困惑。一些程序员可能会这样做。但事后看来,Calendar 仍然是从零开始的事实引起了极大的混乱,这可能是 Java 国际 API 中最大的单一错误。
引自Laura Werner的 Java 国际日历,链接在底部。
这可能只是重复其他人所说的话,抛弃旧的和设计不佳的Calendar
类并使用现代 Java 日期和时间 API java.time。从 1 月的 1 到 12 月的 12 月份,月份的编号一直很合理。
如果您Calendar
从尚未升级到 java.time 的遗留 API获取ZonedDateTime
. 根据您的需要,您可以从那里进行进一步的转换。在世界上大多数地方,Calendar
你得到的对象几乎总是子类的一个实例GregorianCalendar
(因为Calendar
类本身是抽象的)。演示:
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
它本身并不完全定义为零,它被定义为 Calendar.January。这是使用整数而不是枚举作为常量的问题。日历.一月 == 0。
因为语言写作比看起来更难,尤其是处理时间比大多数人想象的要困难得多。有关问题的一小部分(实际上,不是 Java),请参阅https://www.youtube.com/watch?v=-5wpm-gesOY上的 YouTube 视频“时间和时区问题 - Computerphile” 。如果你的头因混乱的大笑而掉下来,请不要感到惊讶。
除了 DannySmurf 对懒惰的回答之外,我还要补充一点,这是为了鼓励您使用常量,例如Calendar.JANUARY
.
因为一切都是从 0 开始的。这是 Java 编程的一个基本事实。如果一件事偏离了这一点,那将导致整个混乱。让我们不要争论它们的形成和代码。