为什么在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:将DateCalendar作为不同的事情是很好的,但是“本地”和“分区”值的分离是丢失的,因为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.GregorianCalendarold 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。

DateFormatGregorianCalendar不正确网格。 您必须两次指定日历,一次间接指定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教程 。

你也可能会发现有用的YearYearMonth类。


关于java.time

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

Joda-Time项目现在处于维护模式 ,build议迁移到java.time。

要了解更多信息,请参阅Oracle教程 。 并search堆栈溢出了很多例子和解释。 规范是JSR 310 。

从何处获取java.time类?

  • Java SE 8SE 9及更高版本
    • 内置。
    • 带有捆绑实现的标准Java API的一部分。
    • Java 9增加了一些小function和修复。
  • Java SE 6SE 7
    • 大部分的java.timefunction都被移植到了ThreeTen-Backport中的 Java 6&7中。
  • Android的
    • ThreeTenABP项目专门针对Android,采用了ThreeTen-Backport (上文提到)。
    • 请参阅如何使用…。

ThreeTen-Extra项目将java.time扩展到其他类。 这个项目是未来可能增加java.time的一个试验场。 你可能会在这里find一些有用的类,比如IntervalYearWeekYearQuarter 等等 。

因为一切都从0开始。这是用Java编程的一个基本事实。 如果有一件事情与此背道而驰,那么这将导致一丝混乱。 我们不要争论他们的形成和代码。