Java 格雷戈里安·卡伦达的奇怪行为

Java 格雷戈里安·卡伦达的奇怪行为,java,calendar,Java,Calendar,我刚刚在Gregorianalendar类中遇到了一个奇怪的行为,我想知道我是否真的做了坏事 仅当初始化日期的月份的实际最大值大于我要将日历设置为的月份时,此选项才会追加 下面是示例代码: // today is 2010/05/31 GregorianCalendar cal = new GregorianCalendar(); cal.set(Calendar.YEAR, 2010); cal.set(Calendar.MONTH, 1); // FEB

我刚刚在Gregorianalendar类中遇到了一个奇怪的行为,我想知道我是否真的做了坏事

仅当初始化日期的月份的实际最大值大于我要将日历设置为的月份时,此选项才会追加

下面是示例代码:

    // today is 2010/05/31  
    GregorianCalendar cal = new GregorianCalendar();

    cal.set(Calendar.YEAR, 2010);
    cal.set(Calendar.MONTH, 1); // FEBRUARY

    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
    cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
    cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
    cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
    cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

    return cal.getTime(); // => 2010/03/03, wtf

我知道问题是由于日历初始化日期是31天的月份(5月),这与设置为2月(28天)的月份不一致。修复很简单(只需在设置年份和月份之前将day_of_month设置为1),但我想知道这是否真的是想要的行为。有什么想法吗?

是的,这就是它的工作原理。如果您从一个具有精确日期的
GregoriaCalendar
开始,并通过使其不一致来修改它,那么您不应该相信您获得的结果

根据有关
getActualMaximum(..)
的文档,它指出:

例如,如果此实例的日期为2004年2月1日,则DAY_of_MONTH字段的实际最大值为29,因为2004年是闰年,如果此实例的日期为2005年2月1日,则为28

所以它应该是有效的,但你必须用一致的值来喂养它2010年2月31日不正确,应用依赖于日期值的内容(如
getActualMaximum
)无法工作。它应该如何自行修复?决定那个月是错误的?还是那天错了

顺便说一句,正如每个人总是说的那样,使用…)

也许
setLenient(布尔lenient)
会帮你解决这个问题。当我运行下面的代码时,我得到一个异常

如果不是,乔达是一个更好的答案

import java.util.Calendar;

public class CalTest
{
    public static void main(String[] args)
    {
        // today is 2010/05/31
        Calendar cal = Calendar.getInstance();
        cal.setLenient(false);

        cal.set(Calendar.YEAR, 2010);
        cal.set(Calendar.MONTH, 1); // FEBRUARY

        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        System.out.println(cal.getTime());
    }
}

我相信这不是我们想要的行为。我同样确信,当他们制作这个类的时候,没有人真正考虑过这个用例。事实上,Calendar在内部状态以及如何管理所有set方法中的所有潜在转换方面有一个非常大的问题


如果您不能在项目中使用JodaTime或JSR-310,那么在使用Calendar类时需要进行大量的单元测试。正如您在本例中所看到的,日历代码的行为因您运行代码的月份(或时间)而异。

它获取当前日期/时间的实际最大值。5月有31天,比2月28日多3天,因此将转移到3月3日

您需要在获取/创建后调用:

GregorianCalendar cal = new GregorianCalendar();
cal.clear();
// ...
这导致:

Sun Feb 28 23:59:59 GMT-04:00 2010
(根据我的时区,这是正确的)


正如其中一个答案中所说,
java.util.Calendar
Date
都是史诗般的失败。考虑做密集的日期/时间操作时,

原因应该是,那个月份有一个枚举式的逻辑结构。您可以轻松地填充和读取数组/集合/列表。由于国际化,它必须是可枚举的(间接访问)。DAY只是一个可直接访问的整数。这就是区别。

在您的示例中,日历从当前日期开始—2010年5月31日。将月份设置为2月时,日期将更改为2010年2月31日,标准化为2010年3月3日,因此cal.getActualMaximum(Calendar.DAY_OF_month)返回3月31日

Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, 2010);
c.set(Calendar.MONTH, Calendar.MAY);
c.set(Calendar.DAY_OF_MONTH, 31);
System.out.println(c.getTime());
c.set(Calendar.MONTH, Calendar.FEBRUARY);
System.out.println(c.getTime());
输出:

Mon May 31 20:20:25 GMT+03:00 2010
Wed Mar 03 20:20:25 GMT+03:00 2010

要修复代码,可以添加cal.clear();或者在设置月份之前设置第1天..28天

问题是
每月的第1天
是以1为基础的,第
0天
是少一天

我想提供一个现代的答案

    ZonedDateTime endOfFebruary2010 = LocalDate.of(2010, Month.MARCH, 1)
            .atStartOfDay(ZoneId.systemDefault())
            .minusNanos(1);
    System.out.println(endOfFebruary2010);
在我的时区运行此打印:

2010-02-28223:59:59.99999999+01:00[欧洲/哥本哈根]

无论您运行打印输出的时间是什么,打印输出都是相同的。对时区的依赖可能是不幸的,但可以通过指定所需的时区来修复,例如
ZoneId.of(“Asia/Oral”)
。我正在使用并推荐
java.time
,这是一种现代的java日期和时间API

如果您不可避免地需要一个老式的
java.util.Date
对象(仅在本例中),请转换:

    Date oldFashionedDate = Date.from(endOfFebruary2010.toInstant());
    System.out.println(oldFashionedDate);
太阳2010年2月28日23:59:59 CET

如果您在某个月只需要计算天数(这是在中询问的):

二十八

据我所知,你真正的问题是:

…我想知道这是否真的是通缉犯的行为。有什么想法吗 ?

我坚信no-arg
gregorianalendar
构造函数返回当前日期和当前时间是需要的行为。而
Calender.set()
只设置显式设置的字段,并尝试保持其他字段不变。2010年2月31日的洪水泛滥到了3月份,没有任何错误的迹象,因为这个月只有28天。这些设计决策的结合让我得出了一个不可避免的结论:你观察到的行为是设计造成的

如果你认为这是一个糟糕的设计,我们很多人都同意你。这也是为什么
日历
格雷戈里安日历
在四年前被
java.time
取代的原因。您将不再需要使用
日历

保留:我们的工具仍然依赖于Java1.7
java.time
在Java7上运行良好。它至少需要Java6

  • 在Java8和更高版本以及更新的Android设备上(我听说是API级别26),现代API是内置的
  • 在Java6和Java7中,获取三个后端口,即新类的后端口(三个用于JSR310;请参阅底部的链接)
  • 在(较旧的)Android上使用Android版本的ThreeTen Backport。它叫ThreeTenABP。并确保从带有子包的
    org.threeten.bp
    导入日期和时间类
链接
  • 解释如何使用
    java.time
  • ,其中首先描述了
    java.time
  • ,java.time的后端口到Java6和Java7(JSR-310为三十)
  • ,Android版的ThreeT
        YearMonth ym = YearMonth.of(2011, Month.FEBRUARY);
        int numDays = ym.lengthOfMonth();
        System.out.println(numDays);