在中java.util.Calendar
,将1月定义为第0个月,而不是第1个月。是否有任何特定原因?
我已经看到很多人对此感到困惑...
在中java.util.Calendar
,将1月定义为第0个月,而不是第1个月。是否有任何特定原因?
我已经看到很多人对此感到困惑...
Answers:
它只是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
和Calendar
作为不同的东西很好,但是缺少“本地”值与“分区”值的分隔,日期/时间与日期对时间也是如此Date.toString()
它总是使用系统本地时区(即现在来迷惑许多堆栈溢出用户之前)执行因为用数月做数学要容易得多。
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
所有月份的工作都是一样的,因此无需解决。
((11 - 1 + 1) % 12) + 1 = 12
仅在1..12 (11 % 12) + 1
个月的时间里,您只需要在做模后加1即可。不需要魔术。
对此有很多答案,但是无论如何,我都会发表自己的看法。如前所述,这种奇怪行为的原因来自POSIX C time.h
,其中月份以0-11的范围存储在一个int中。要解释原因,请这样看:年份和日期被认为是口语中的数字,但是月份则有自己的名字。因此,由于1月是第一个月,它将存储为偏移量0(第一个数组元素)。monthname[JANUARY]
会是"January"
。一年中的第一个月是第一个月数组元素。
另一方面,由于天数没有名称,因此将它们以0-30的形式存储在int中会造成混淆,并增加了很多day+1
用于输出的指令,当然,还容易出现许多错误。
话虽这么说,但这种不一致是令人困惑的,尤其是在javascript(它也继承了这个“功能”)中,这是一种脚本语言,应该在远离语言的地方对其进行抽象。
TL; DR:因为月份有名称,而月份中的几天没有。
在Java 8中,有一个新的日期/时间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 Time的一件事-它可能鼓励程序员从硬编码常量的角度进行思考。(不过,只有一点点。这似乎并不是乔达在强迫程序员编写不好的程序。)
对我而言,没有人能比mindpro.com更好地解释它:
陷阱
java.util.GregorianCalendar
与old 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。
java.util.Month
Java为您提供了几个月使用基于1的索引的另一种方法。使用java.time.Month
枚举。每十二个月预定义一个对象。他们在1月至12月为每个1-12分配了编号;拨打电话getValue
。
使用Month.JULY
(给您7)而不是Calendar.JULY
(给您6)。
(import java.time.*;)
Month.FEBRUARY.getValue() // February → 2.
2
乔恩·斯凯特(Jon Skeet)的回答是正确的。
现在,我们有了那些麻烦的旧旧日期时间类的现代替代品:java.time类。
java.time.Month
在那些类中是枚举。枚举包含一个或多个预定义对象,这些对象在类加载时自动实例化。在我们有十几个这样的对象,每一个名字:,,,等等。每个都是一个类常量。您可以在代码中的任何位置使用和传递这些对象。例:Month
Month
JANUARY
FEBRUARY
MARCH
static final public
someMethod( 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教程。
该java.time框架是建立在Java 8和更高版本。这些类取代麻烦的老传统日期时间类,如java.util.Date
,.Calendar
,和java.text.SimpleDateFormat
。
现在处于维护模式的Joda-Time项目建议迁移到java.time。
要了解更多信息,请参见Oracle教程。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310。
在哪里获取java.time类?
该ThreeTen-额外项目与其他类扩展java.time。该项目为将来可能向java.time添加内容提供了一个试验场。你可能在这里找到一些有用的类,比如Interval
,YearWeek
,YearQuarter
,和更多。
它本身并没有完全定义为零,而是定义为Calendar.January。这是将int用作常量而不是枚举的问题。Calendar.January == 0。
因为语言编写比看起来难,尤其是处理时间比大多数人想象的要难得多。对于问题的一小部分(实际上不是Java),请访问https://www.youtube.com/watch?v=-5wpm-gesOY上的YouTube视频“时间和时区的问题-Computerphile” 。如果您的头因困惑而掉下来,不要感到惊讶。
除了DannySmurf的惰性答案之外,我还要补充一点是,它鼓励您使用常量,例如Calendar.JANUARY
。
因为一切都以0开头。这是Java编程的基本事实。如果有一件事偏离这一点,那将导致一团混乱。我们不要争论它们的形成并与它们一起编码。