为什么Java Calendar中的1月份为0?

时间:2008-12-05 16:23:23

标签: java calendar

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

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

17 个答案:

答案 0 :(得分:310)

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

帮自己一个忙,改为使用Joda Time,或者JSR-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 - 所有关于何时重新计算事物的业务
  • 使用无参数构造函数默认为“now”,这导致难以测试的代码
  • 始终使用系统本地时区的Date.toString()实现(此前许多Stack Overflow用户感到困惑)

答案 1 :(得分:39)

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

12月份是1月份后的1个月,但通常你需要拿出月份数并做数学

12 + 1 = 13 // What month is 13?

我知道!我可以通过使用12的模数来快速解决这个问题。

(12 + 1) % 12 = 1

直到十一月才能正常工作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

所有月份的工作都相同,并且没有必要进行解决。

答案 2 :(得分:34)

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

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

答案 3 :(得分:22)

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

另一方面,由于日期数字没有名称,将它们存储在0到30的int中会让人感到困惑,添加了很多day+1输出指令,当然也很容易很多错误。

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

TL; DR :因为月份有名称,而月份则没有。

答案 4 :(得分:12)

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

答案 5 :(得分:9)

我会说懒惰。数组从0开始(每个人都知道);一年中的几个月是一个阵列,这让我相信Sun的一些工程师只是不愿意把这一点放在Java代码中。

答案 6 :(得分:9)

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

答案 7 :(得分:6)

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

答案 8 :(得分:4)

就个人而言,我把Java日历API的陌生感作为一种迹象,表明我需要将自己与以格列高利为中心的思维模式脱离,并试图在这方面更加非常地编程。具体来说,我再次学会了避免像数月这样的事情的硬编码常量。

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

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

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

这说明了一件让我想起Joda Time的事情 - 它可能会鼓励程序员用硬编码的常量来思考。 (但是,只有一点点。并不是说Joda强迫程序员编程很糟糕。)

答案 9 :(得分:4)

java.util.Month

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

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

(import java.time.*;)

答案 10 :(得分:2)

真正的原因

<块引用>

您可能会认为,当我们弃用 Date 的大部分内容并添加新的 日历类,我们将解决 Date 最大的烦恼:事实 一月是 0 月。我们当然应该有,但不幸的是 我们没有。我们担心如果 Date 使用从零开始的月份,日历使用从一开始的月份。还有一些 程序员可能会。但事后看来,事实是 日历仍然是零基础已经造成了大量的 混乱,这可能是 Java 中最大的单一错误 国际 API。

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

更好的选择:java.time

这可能只是在重复其他人所说的,抛弃旧的和设计糟糕的 Calendar 类,并使用 java.time,现代 Java 日期和时间 API。从 1 月份的 1 到 12 月份的 12,月份的编号一直很合理。

如果您从尚未升级到 java.time 的遗留 API 获得 Calendar,首先要做的是转换为现代 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

链接

答案 11 :(得分:2)

TL;博士

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

2

详细

Answer by Jon Skeet是正确的。

现在,我们可以替换那些麻烦的旧遗留日期时间类:java.time类。

java.time.Month

这些课程中有Month enum。枚举携带一个或多个预定义对象,即在加载类时自动实例化的对象。在Month我们有十几个这样的对象,每个对象都有一个名称:JANUARYFEBRUARYMARCH,依此类推。每个都是static final public类常量。您可以在代码中的任何位置使用并传递这些对象。示例:someMethod( Month.AUGUST )

幸运的是,他们有正确的编号,1-12,其中1是1月,12是12月。

获取特定月份编号(1-12)的Month对象。

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

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

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

此课程中有许多其他方便的方法,例如知道the number of days in each month。该课程甚至可以generate a localized name个月。

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

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

FEVRIER

此外,你应该在你的代码库周围传递这个枚举的对象,而不仅仅是整数。这样做可以提供类型安全性,确保有效的值范围,并使您的代码更加自我记录。如果不熟悉Java中令人惊讶的强大枚举功能,请参阅Oracle Tutorial

您也可能会发现YearYearMonth类很有用。

关于java.time

java.time框架内置于Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.Date.Calendar和&amp; java.text.SimpleDateFormat

现在位于Joda-Timemaintenance mode项目建议迁移到java.time。

要了解详情,请参阅Oracle Tutorial。并搜索Stack Overflow以获取许多示例和解释。规范是JSR 310

从哪里获取java.time类?

ThreeTen-Extra项目使用其他类扩展java.time。该项目是未来可能添加到java.time的试验场。您可以在此处找到一些有用的课程,例如IntervalYearWeekYearQuartermore

答案 12 :(得分:1)

对我来说,没有人比mindpro.com更好地解释:

  

<强>陷阱

     

java.util.GregorianCalendar的错误和陷阱要少得多   old java.util.Date课程,但仍然没有野餐。

     

当夏令时首次出现时,有程序员吗?   提议,他们会否认它是疯狂和难以处理的。同   夏令时,存在着根本的模糊性。在秋天的时候   你在凌晨2点将时钟设置回一小时有两种不同   当地时间凌晨1点30分。你可以告诉他们   只有当你记录你是否打算夏令时或   标准时间与阅读。

     

不幸的是,没有办法告诉GregorianCalendar你是谁   意。你必须诉诸于假人的当地时间   UTC TimeZone可以避免歧义。程序员通常关闭他们的   注意这个问题,希望没有人在这期间做任何事情   小时。

     

千年虫。这些错误仍然不在Calendar类之外。   即使在JDK(Java Development Kit)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有原始的   GregorianCalendar.get(Calendar.ZONE_OFFSET)和夏令时   GregorianCalendar. get( Calendar. DST_OFFSET),但无法获得   使用的实际时区偏移量。你必须分别得到这两个   并将它们加在一起。

     

GregorianCalendar.set( year, month, day, hour, minute)未设置   秒到0。

     

DateFormatGregorianCalendar未正确拼接。你必须   指定日历两次,间接指定为日期。

     

如果用户未正确配置其时区,则默认为   安静到PST或GMT。

     

在GregorianCalendar中,月份从1月开始编号= 0,   而不是像地球上的其他人一样。然而,几天从1开始   星期日= 1,星期一= 2,星期六= 7,星期几也是如此。然而   日期格式。解析用January = 1的传统方式表现。

答案 13 :(得分:0)

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

答案 14 :(得分:0)

因为语言写作比看起来更难,特别是处理时间比大多数人想象的要困难得多。对于问题的一小部分(实际上,不是Java),请参阅YouTube视频&#34;时间问题&amp;时区 - Computerphile&#34;在https://www.youtube.com/watch?v=-5wpm-gesOY。如果你的头脑在混乱中脱落,不要感到惊讶。

答案 15 :(得分:-1)

除了DannySmurf对懒惰的回答之外,我还要补充说,鼓励你使用常量,例如Calendar.JANUARY

答案 16 :(得分:-2)

因为一切都以0开头。这是Java编程的基本事实。如果有一件事情与此有所不同,那么这将导致混乱的全部线索。我们不要争论它们的形成和代码。