为什么1月在Java日历中为0?


300

在中java.util.Calendar,将1月定义为第0个月,而不是第1个月。是否有任何特定原因?

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


4
因为常量JANUARY,FEBRUARY等存在,这不是实现细节吗?日期类早于适当的Java枚举支持。
gnud

6
更令人讨厌的-为什么会有十一月?
马特b

40
@gnud:不,这不是实现细节。当为您提供“自然”基数(即Jan = 1)的整数,并且需要将其与Calendar API结合使用时,这会很麻烦。
乔恩·斯基特

1
@matt b:适用于有13个月的非格里高利历(月历等)。这就是为什么最好不要以数字来思考,而是让Calendar进行本地化。
erickson

7
13个月的论点毫无道理。如果是这样,为什么不将额外月份设为0或13?
奎因·泰勒,

Answers:


323

它只是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个月的日历后立即失败...但是至少指定的大小是您期望的月数。

这不是一个很好的理由,但这是一个理由...

编辑:作为一种评论,它要求我对Date / Calendar的问题提出一些想法:

  • 令人惊讶的基数(Date中的1900作为Date中的年基,对于过时的构造函数来说是公认的;两者中的0作为月基)
  • 可变性-使用不变类型使得很多简单的什么是真正有效的工作
  • 类型集不足:拥有DateCalendar作为不同的东西很好,但是缺少“本地”值与“分区”值的分隔,日期/时间与日期对时间也是如此
  • 一个使用魔术常数导致丑陋代码的API,而不是使用明确命名的方法
  • 一个很难推理的API-有关何时重新计算事物的所有事务等
  • 使用无参数的构造函数默认为“现在”,这导致难以测试的代码
  • Date.toString()它总是使用系统本地时区(即现在来迷惑许多堆栈溢出用户之前)执行

14
...而弃用所有有用的简单Date方法又是怎么回事?现在,我必须以复杂的方式使用该可怕的Calendar对象来完成过去很简单的事情。
Brian Knoblauch

3
@Brian:我感到你很痛苦。同样,Joda Time更简单:)(不变性因素也使事情变得更加令人愉快。)
Jon Skeet

8
您没有回答问题。
Zeemee '02

2
@ user443854:我在编辑中列出了一些要点-看看是否有帮助。
乔恩·斯凯特

2
如果使用Java 8,则可以放弃Calendar类,并切换到新的光滑的DateTime API。新的API还包括一个不可变的/线程安全的DateTimeFormatter,它是对有问题且昂贵的SimpleDateFormat的重大改进。
ccpizza 2015年

43

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

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

所有月份的工作都是一样的,因此无需解决。


5
这很令人满意。至少这种疯狂有一些价值!
moljac024 2014年

“很多魔术数字”-不,只是出现两次的数字。
user123444555621

但是,由于C不幸地使用了“余数”运算符而不是“模数”,所以回到一个月还是有点棘手。我也不确定在不调整年份的情况下,一个人真正需要颠簸一次的频率,而将月份延长1到12不会对`while(month> 12){month- = 12; 年++;}
超级猫

2
由于类似DateTime.AddMonths之类的理智功能很难在lib中正确实现,因此我们必须自己进行数学描述... Mmmmmkay
nsimeonov

8
我不理解这些赞成意见- ((11 - 1 + 1) % 12) + 1 = 12仅在1..12 (11 % 12) + 1个月的时间里,您只需要做模加1即可。不需要魔术。
mfitzp

35

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

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


22

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

另一方面,由于天数没有名称,因此将它们以0-30的形式存储在int中会造成混淆,并增加了很多day+1用于输出的指令,当然,还容易出现许多错误。

话虽这么说,但这种不一致是令人困惑的,尤其是在javascript(它也继承了这个“功能”)中,这是一种脚本语言,应该在远离语言的地方对其进行抽象。

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


1
“月份有名字,日子没有。” 听说过“星期五”吗?;)好吧,我猜你的意思是“ ..一个月中的几天不”-也许要花钱来编辑您的(否则好)答案。:-)
Andrew Thompson

是将0/0/0000更好地呈现为“ 00-Jan-0000”还是“ 00-XXX-0000”?恕我直言,如果有13个“月”,但是0月被赋予了一个虚拟名称,那么许多代码本来会更干净。
超级猫

1
多数民众赞成在一个有趣的观点,但0/0/0000不是有效的日期。您将如何渲染40/40/0000?
piksel bitworks

12

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


2
新的日期时间API现在是Java 8的一部分
mschenk74,2013年

9

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


9
不,我不会。优化一个客户的效率比一个程序员更重要。由于该客户在这里花时间询问,因此他们失败了。
TheSmurf

2
这与效率完全无关-好像不是几个月存储在数组中,您需要13代表12个月。这不是使API像最初那样易于使用的问题。乔什·布洛赫(Josh Bloch)对“有效Java”中的日期和日历进行了研究。很少有API是完美的,而Java中的日期/时间API不幸地扮演了被愚弄的角色。这就是生活,但是我们不要假装它与效率有关。
奎因·泰勒

1
那为什么不计算0到30天呢?只是不一致和草率。
JuanguiJordán17年


8

因为程序员沉迷于基于0的索引。好吧,这要复杂得多:当您使用低级逻辑使用基于0的索引时,这更有意义。但总的来说,我还是会坚持第一句话。


1
这是另一个那些成语/该走的习惯方式回汇编或机器语言,一切以补偿,而不是指标方面完成。数组符号成为用于访问相邻的块的捷径,起始于偏移0
肯温和

4

就我个人而言,我以Java日历API的陌生性为标志,表明我需要脱离以格里高利主义为中心的思维方式,并尝试在这方面进行更不可知的编程。具体来说,我再次学会了避免诸如月之类的硬编码常量。

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

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

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

这说明了让我有些不喜欢Joda Time的一件事-它可能鼓励程序员从硬编码常量的角度进行思考。(不过,只有一点点。这似乎并不是乔达在强迫程序员编写不好的程序。)


1
但是当您的代码中没有常量时,哪种方案更可能使您头疼-您的值是Web服务调用或其他结果的结果。
乔恩·斯基特

当然,该Web服务调用也应该使用该常数。:-)任何外部呼叫者也一样。一旦我们确定存在多个标准,就明显需要执行一个标准。(我希望我能理解您的评论...)
保罗·布林克利

3
是的,在表达月份时,我们应该执行世界上几乎所有其他标准所使用的标准-从1开始的标准。
乔恩·斯基特

这里的关键词是“几乎”。显然,Jan = 1等在具有广泛用途的日期系统中感觉很自然,但是即使在这种情况下,为什么还要让自己成为避免硬编码常量的例外呢?
Paul Brinkley

3
因为它使生活更轻松。就是这样。我从未遇到过基于1月份的系统的一次性问题。我已经看到Java API有很多此类错误。忽略世界上其他所有人所做的一切都没有道理。
乔恩·斯基特

4

对我而言,没有人能比mindpro.com更好地解释它:

陷阱

java.util.GregorianCalendarold java.util.Date同班同学相比,臭虫和陷阱要少得多, 但仍然不是野餐。

如果最初提出夏令时的时候有程序员,他们会否定它是疯狂而棘手的。使用夏令时,存在根本的歧义。在秋天,当您将时钟设置为凌晨2点一小时时,会有两个不同的时间点,都称为本地时间1:30 AM。仅当您在读数中记录了打算使用夏令时或标准时间时,才能将它们区分开。

不幸的是,没有办法说出GregorianCalendar您的意图。您必须使用虚拟UTC TimeZone告诉它本地时间,以避免产生歧义。程序员通常对这个问题视而不见,只是希望这个小时没有人做任何事情。

千年虫。这些错误仍然不在Calendar类之外。即使在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 */

对于MST,该错误于2001年1月1日上午7点消失。

GregorianCalendar由一大堆未键入的int魔术常数控制。这种技术完全破坏了编译时错误检查的希望。例如获取您使用的月份 GregorianCalendar. get(Calendar.MONTH));

GregorianCalendar具有原始时间 GregorianCalendar.get(Calendar.ZONE_OFFSET)和夏时制 GregorianCalendar. get( Calendar. DST_OFFSET),但是无法获取正在使用的实际时区偏移量。您必须分别获得这两个并将它们添加在一起。

GregorianCalendar.set( year, month, day, hour, minute) 不会将秒设置为0。

DateFormat并且GregorianCalendar不能正确啮合。您必须两次指定日历,一次间接指定为日期。

如果用户未正确配置其时区,它将默认为PST或GMT。

在GregorianCalendar中,月份从1月= 0开始编号,而不是地球上其他所有人的月份编号。然而,天数从1开始,而星期几则从1开始,星期天= 1,星期一= 2,…星期六= 7。但是DateFormat。parse的行为与传统方式相同,即January = 1。


4

java.util.Month

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

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

(import java.time.*;)

3

tl; dr

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

2

细节

乔恩·斯凯特(Jon Skeet)的回答是正确的。

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

java.time.Month

在那些类中是枚举。枚举包含一个或多个预定义对象,这些对象在类加载时自动实例化。在我们有十几个这样的对象,每一个名字:,,,等等。每个都是一个类常量。您可以在代码中的任何位置使用和传递这些对象。例:Month MonthJANUARYFEBRUARYMARCHstatic final publicsomeMethod( Month.AUGUST )

幸运的是,它们的编号是1-12,其中1是1月,12是12月。

获取一个Month特定月份数字(1-12)的对象。

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

朝另一个方向,向Month对象询问其月份号。

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

此类上的许多其他方便的方法,例如知道每个月中的天数。该类甚至可以生成月份的本地化名称

您可以使用各种长度或缩写来获取月份的本地化名称。

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

费维尔

同样,您应该在代码库周围传递该枚举的对象,而不是仅仅传递整数。这样做可以提供类型安全性,确保值的有效范围,并使代码更具自记录性。如果不熟悉Java中强大的枚举功能,请参见Oracle教程

您还会发现有用的YearYearMonth类。


关于java.time

java.time框架是建立在Java 8和更高版本。这些类取代麻烦的老传统日期时间类,如java.util.Date.Calendar,和java.text.SimpleDateFormat

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

要了解更多信息,请参见Oracle教程。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310

在哪里获取java.time类?

  • Java SE 8 SE 9及更高版本
    • 内置的
    • 标准Java API的一部分,具有捆绑的实现。
    • Java 9添加了一些次要功能和修复。
  • Java SE 6 SE 7
    • 很多java.time功能都在ThreeTen- Backport中反向移植到Java 6和7 。
  • 安卓

ThreeTen-额外项目与其他类扩展java.time。该项目为将来可能向java.time添加内容提供了一个试验场。你可能在这里找到一些有用的类,比如IntervalYearWeekYearQuarter,和更多


0

它本身并没有完全定义为零,而是定义为Calendar.January。这是将int用作常量而不是枚举的问题。Calendar.January == 0。


1
值是相同的。API也可能返回0,它与常量相同。Calendar.JANUARY可以被定义为1 -这就是重点。枚举是一个不错的解决方案,但是直到Java 5才将真正的枚举添加到该语言中,并且Date从一开始就存在。不幸的是,但是一旦第三方代码使用了它,您实际上就无法“修复”这样的基本API。最好的办法是提供新的API并淘汰旧的API,以鼓励人们继续前进。谢谢Java 7 ...
Quinn Taylor

0

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


-1

除了DannySmurf的惰性答案之外,我还要补充一点是,它鼓励您使用常量,例如Calendar.JANUARY


5
当您显式编写特定月份的代码时,这一切都很好,但是当您从其他来源获得“正常”格式的月份时,您会感到很痛苦。
乔恩·斯基特

1
当您尝试以某种特定方式打印该月的值时,这也很痛苦-您总是在该值上加1。
Brian Warshaw

-2

因为一切都以0开头。这是Java编程的基本事实。如果有一件事偏离这一点,那将导致一团混乱。我们不要争论它们的形成并与它们一起编码。


2
不,现实世界中的大多数事物都是从1开始的。偏移量是从0开始的,并且一年中的月份不是偏移量,它是十二个之一,就像月中的一天是31或30或29或28.将月份视为偏移量是反复无常的,尤其是如果同时我们不以相同的方式对待月份的日子。造成这种差异的原因是什么?
SantiBailors

在现实世界中,从1开始,在Java世界中,从0开始。但是...我认为是因为:-要计算一周中的某天,在不增加几个步骤的情况下,不能进行两次计算的偏移量它...-另外,如果需要,它会显示月份中的完整日期(不要混淆或需要检查2月。)-对于月份,它会强制您以日期格式输出,无论哪种方式都应使用。另外,由于一年中的月份数是固定的,而一个月中的天数则不合理,因此您是否需要声明数组并使用偏移量以更好地适合数组。
Syrrus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.