327

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

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

4

17 回答 17

344

它只是 Java 日期/时间 API 的可怕混乱的一部分。列出它的问题需要很长时间(而且我确信我不知道一半的问题)。诚然,处理日期和时间很棘手,但无论如何。

帮自己一个忙,改用Joda TimeJSR-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作为不同的东西很好,但是缺少“本地”与“分区”值的分离,日期/时间与日期与时间也是如此
  • 一个 API 会导致带有魔法常量的丑陋代码,而不是明确命名的方法
  • 一个很难推理的 API - 关于何时重新计算事物等的所有业务
  • 使用无参数构造函数默认为“现在”,这导致代码难以测试
  • 始终使用系统本地时区的Date.toString()实现(以前让许多 Stack Overflow 用户感到困惑)
于 2008-12-05T16:29:41.283 回答
53

因为用几个月做数学要容易得多。

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

所有月份的工作方式都是一样的,不需要解决问题。

于 2013-08-01T19:34:20.807 回答
37

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

基于 C 的语言在索引 0 处开始数组。因此,这对于在月份名称数组中输出字符串很方便,并tm_mon作为索引。

于 2008-12-05T16:35:42.207 回答
26

对此有很多答案,但无论如何我都会就这个问题发表我的看法。如前所述,这种奇怪行为背后的原因来自 POSIX C time.h,其中月份存储在范围为 0-11 的 int 中。要解释原因,请这样看;年和日在口语中被认为是数字,但月份有自己的名字。因此,由于一月是第一个月,它将被存储为偏移量 0,即第一个数组元素。monthname[JANUARY]将是"January"。一年中的第一个月是第一个月数组元素。

另一方面,日期数字,因为它们没有名称,将它们存储在 int 中作为 0-30 会令人困惑,添加很多day+1输出指令,当然,容易出现很多错误。

话虽如此,这种不一致是令人困惑的,尤其是在 javascript(它也继承了这个“特性”)中,这是一种脚本语言,它应该被抽象得远离语言。

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

于 2011-08-24T16:56:18.933 回答
12

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

于 2008-12-05T19:25:22.960 回答
10

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

于 2008-12-05T16:25:13.273 回答
9

可能是因为 C 的“struct tm”也是如此。

于 2008-12-05T16:37:16.090 回答
8

因为程序员痴迷于从 0 开始的索引。好的,它比这更复杂一点:当您使用较低级别的逻辑来使用基于 0 的索引时,它更有意义。但总的来说,我仍然会坚持我的第一句话。

于 2008-12-05T16:26:02.523 回答
4

就个人而言,我认为 Java 日历 API 的陌生性表明我需要摆脱以公历为中心的思维方式,并尝试在这方面进行更加不可知论的编程。具体来说,我再次学会了避免像几个月这样的硬编码常量。

以下哪项更可能是正确的?

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

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

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

于 2008-12-05T17:09:57.630 回答
4

对我来说,没有人能比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有 raw 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。然而日期格式。parse 以传统方式运行,一月=1。

于 2012-04-29T14:26:18.783 回答
4

java.time.Month

Java 为您提供了另一种使用基于 1 的索引数月的方法。使用java.time.Month枚举。为十二个月中的每一个月预定义一个对象。他们在 1 月至 12 月分别为 1-12 分配了编号;拨打电话getValue号码。

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

(import java.time.*;)
于 2016-06-28T03:41:34.770 回答
4

真正的原因

您可能会认为,当我们弃用大部分 Date 并添加新的 Calendar 类时,我们会解决 Date 的最大烦恼:1 月是第 0 个月这一事实。我们当然应该这样做,但不幸的是我们没有。我们担心如果 Date 使用从零开始的月份而 Calendar 使用从 1 开始的月份,程序员会感到困惑。一些程序员可能会这样做。但事后看来,Calendar 仍然是从零开始的事实引起了极大的混乱,这可能是 Java 国际 API 中最大的单一错误。

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

更好的选择:java.time

这可能只是重复其他人所说的话,抛弃旧的和设计不佳的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

链接

于 2021-03-17T22:20:44.770 回答
3
于 2016-08-30T05:47:28.000 回答
0

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

于 2008-12-06T14:32:11.770 回答
0

因为语言写作比看起来更难,尤其是处理时间比大多数人想象的要困难得多。有关问题的一小部分(实际上,不是 Java),请参阅https://www.youtube.com/watch?v=-5wpm-gesOY上的 YouTube 视频“时间和时区问题 - Computerphile” 。如果你的头因混乱的大笑而掉下来,请不要感到惊讶。

于 2018-01-09T02:16:30.980 回答
-1

除了 DannySmurf 对懒惰的回答之外,我还要补充一点,这是为了鼓励您使用常量,例如Calendar.JANUARY.

于 2008-12-05T16:27:01.070 回答
-2

因为一切都是从 0 开始的。这是 Java 编程的一个基本事实。如果一件事偏离了这一点,那将导致整个混乱。让我们不要争论它们的形成和代码。

于 2015-11-02T03:10:44.857 回答