Java 日历异常行为

Java Calendar Strange behaviour

我在 java 中使用日历对象将输入日期(year/month/date 等从网络获取)转换为纪元时间。我正在重用相同的日历对象。有时我得到的那一年 network 为 0,并且没有对此进行验证。一旦发生这种情况,每当我将日期转换为纪元时,我得到的纪元时间总是负数。这是一个有效的行为

请找到我针对此问题进行单元测试时得到的示例代码和结果。

    Calendar cal= Calendar.getInstance();        
    cal.set(Calendar.YEAR, 0); 
    System.out.println("time 1 : "+cal.getTimeInMillis());  
    System.out.println("Date 1 : "+new Date(cal.getTimeInMillis()));  

    cal.set(Calendar.YEAR, 2020); 
    System.out.println("time 2 : "+cal.getTimeInMillis());
    System.out.println("Date 2 : "+new Date(cal.getTimeInMillis()));  

    cal.set(Calendar.YEAR, 2010); 
    cal.set(Calendar.MONTH, 10);
    System.out.println("time 3 : "+cal.getTimeInMillis());
    System.out.println("Date 3 : "+new Date(cal.getTimeInMillis())); 

此代码的输出是
时间 1:-62151385126938
日期 1:2004 年 7 月星期日 11:51:13 IST 1

时间 2:-125866201126938
日期 2:7 月 4 日星期三 11:51:13 IST 2020

时间 3:-125540041126938
日期 3:2010 年 11 月星期五 11:51:13 IST[​​=12=]

这是 java 日历的预期行为吗?

我的 JDK 版本是 打开JDK 运行时环境 Corretto-8.252.09.1 (build 1.8.0_252-b09)

编辑: 还有其他 option/class 可以解决此问题,但我的目的是找到此行为的根本原因

I am using calendar object

停止这样做。永远不要使用可怕的 CalendarDate classes。仅使用 JSR 310 中定义的现代 java.time classes。

Sometimes the year i am getting from network is 0 and there was no validation for this once. Once this happens the whenever i convert the date to epoch, the epoch time im getting is always negative. Is this a valid behaviour

是的,自 UTC 中 1970 年第一时刻的纪元参考以来的毫秒数负数对于零年的日期是正确的。 0000 年发生在 2000 多年前,因此从 1970-01-01T00:00Z 开始倒计时是一个非常大的毫秒数。

我了解到您看到 Calendar 在当代年份重置后返回奇怪的负数。如果您想了解更多信息,请参阅优秀的 我认为:既然我们有 java.time,就没有必要调查 Calendar。埋葬死者,继续前行

如果您必须与尚未为 java.time 更新的旧代码互操作,您可以在遗留 classes 和现代的。查看添加到旧 classes 的新方法。尽量减少对旧遗留 classes.

的使用
if( myCalendar instanceof GregorianCalendar ) 
{
    ZonedDateTime zdt = ( ( GregorianCalendar ) myCalendar ).toZonedDateTime() ;
}

……和……

Calendar myCalendar = GregorianCalendar.from( zdt ) ;

你说:

I am reusing same calendar object .

停止这样做。重用 class 的对象会导致各种问题。

java.time 中的一个重要设计考虑因素是仅使用不可变对象,以避免这些重用问题。

am using calendar object in java to convert an input date(year/month/date

您没有向我们展示此代码,因此我无法进一步帮助您。

发布前搜索 Stack Overflow。所有这一切已经被多次提及。

Is this an expected behaviour for java Calendar?

我的答案可能是有争议的,Calendar 的行为常常与预期相反。它的设计和记录就是为了这样做。我理解你的困惑。

没有 0 年。所以第一个期望可能是 Calendar 应该在您尝试将纪元年设置为 0 时抛出异常。使用标准设置它不会。相反,它推断:第 0 年被认为是第 1 年之前的一年,所以那是公元前 1 年。在您的输出中,您可以看到年份是 1(不是 0);但是你看不到的是时代,是公元前,不是公元(CE)。因此,让我们在代码中添加几行打印纪元的代码:

    Calendar cal= Calendar.getInstance();
    System.out.format("Initial:   Era %s year %d%n",
            cal.getDisplayName(Calendar.ERA, Calendar.SHORT_STANDALONE, Locale.ENGLISH),
            cal.get(Calendar.YEAR));
    
    cal.set(Calendar.YEAR, 0);
    System.out.format("Year 0:    Era %s year %d%n",
            cal.getDisplayName(Calendar.ERA, Calendar.SHORT_STANDALONE, Locale.ENGLISH),
            cal.get(Calendar.YEAR));
    System.out.println("time 1 :   "+cal.getTimeInMillis());  
    System.out.println("Date 1 :   "+new Date(cal.getTimeInMillis()));  

当我运行刚才的代码时,输​​出是:

Initial:   Era AD year 2020
Year 0:    Era BC year 1
time 1 :   -62151371618330
Date 1 :   Sun Jul 04 11:06:21 CET 1

接下来,Calendar.YEAR 表示纪元年份,至少对于 GregorianCalendar(这是 Calendar 的具体子 class,您从中获得了一个实例Calendar.getInstance())。所以当你接下来设置 YEAR,也就是纪元年,到 2020 年时,Calendar 停留在基督之前(在普通纪元之前),所以你得到 2020 BC,距今 4000 多年前。并且您的毫秒数“增长”到已经为负值的大约两倍。

    cal.set(Calendar.YEAR, 2020); 
    System.out.format("Year 2020: Era %s year %d%n",
            cal.getDisplayName(Calendar.ERA, Calendar.SHORT_STANDALONE, Locale.ENGLISH),
            cal.get(Calendar.YEAR));
    System.out.println("time 2 :   "+cal.getTimeInMillis());
    System.out.println("Date 2 :   "+new Date(cal.getTimeInMillis()));  
Year 2020: Era BC year 2020
time 2 :   -125866187618330
Date 2 :   Wed Jul 04 11:06:21 CET 2020

恕我直言,这是避免 Calendar class.

的好案例

java.time

是的,如果仅针对其他读者,还有其他选择。

首先,你不应该试图去处理毫无意义的一年。我不知道你的情况和要求,所以不能告诉你当你得到0或负数的年份时该怎么办,但你需要决定,你需要检查并采取适当的行动。正如您所知,您到目前为止所做的是错误的。

其次,很明显你应该使用 java.time,现代 Java 日期和时间 API。

    ZonedDateTime zdt = ZonedDateTime.now(ZoneId.systemDefault());

    zdt = zdt.withYear(0);
    System.out.println(zdt);
    System.out.println(zdt.toEpochSecond());

    zdt = zdt.withYear(2020);
    System.out.println(zdt);
    System.out.println(zdt.toEpochSecond());

    zdt = zdt.withYear(2010);
    System.out.println(zdt);
0000-07-04T11:23:08.573357+00:50:20[Europe/Copenhagen]
-62151197232
2020-07-04T11:23:08.573357+02:00[Europe/Copenhagen]
1593854588
2010-07-04T11:23:08.573357+02:00[Europe/Copenhagen]

您注意到在此输出中,自纪元以来的秒数在负数之后再次变为正数。在java.time中,一年表示临产年,即有符号的年份。 Proleptic 第 0 年是公元前 1 年,-1 表示公元前 2 年,依此类推。所以第 0 年只是任何其他年份,不会产生任何奇怪的行为。

另外 java.time 反对将时代设置为 0,这可能会帮助您更早发现问题:

    zdt = zdt.with(ChronoField.YEAR_OF_ERA, 0);
Exception in thread "main" java.time.DateTimeException: Invalid value for YearOfEra (valid values 1 - 999999999/1000000000): 0
  at java.base/java.time.temporal.ValueRange.checkValidValue(ValueRange.java:311)
  at java.base/java.time.temporal.ChronoField.checkValidValue(ChronoField.java:717)
  at java.base/java.time.LocalDate.with(LocalDate.java:1048)
  at java.base/java.time.LocalDateTime.with(LocalDateTime.java:970)
  at java.base/java.time.ZonedDateTime.with(ZonedDateTime.java:1312)
  at ovv.so.date.special.SetCalendarYear0.main(SetCalendarYear0.java:25)