Calendar.getTime 非法参数异常

Calendar.getTime IllegalArgumentException

import java.util.Calendar;

public class WeekYear {
    static String input = "202001";
    //static String format = "YYYYMM";

    public static void main(String[] args) throws ParseException {
        Calendar lCal = Calendar.getInstance();
        System.out.println(lCal.isLenient());
        lCal.setLenient(false);
        lCal.set(Calendar.YEAR, new Integer(input.substring(0, 4)).intValue());
        lCal.set(Calendar.WEEK_OF_YEAR, new Integer(input.substring(4, 6)).intValue());
        //lCal.setMinimalDaysInFirstWeek(5);
        System.out.println(lCal.isLenient());
        System.out.println(lCal.getTime());

        //lCal.set(Calendar.YEAR, new Integer(input.substring(0, 4)).intValue());
        //lCal.set(Calendar.WEEK_OF_YEAR, new Integer(input.substring(4, 6)).intValue());
        //System.out.println(lCal.getTime());
    }
}

当此代码在 2020 年 11 月 22 日执行时,我从 Calendar.getTime() 得到一个 IllegalArgumentException。但是在 2020 年 11 月 27 日执行时它运行良好。

文档说:

The setLenient(boolean leniency) method in Calendar class is used to specify whether the interpretation of the date and time is to be lenient or not. Parameters: The method takes one parameter leniency of the boolean type that refers to the mode of the calendar.

有什么解释吗?我现在即使在本地也无法重现该问题。当地时间设置为 CST

异常堆栈:

Exception in thread "main" java.lang.IllegalArgumentException: year: 2020 -> 2019
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2829)
at java.util.Calendar.updateTime(Calendar.java:3393)
at java.util.Calendar.getTimeInMillis(Calendar.java:1782)
at java.util.Calendar.getTime(Calendar.java:1755)
at WildDog.main(WildDog.java:13)
`````````

tl;博士

从不使用 Calendar,现在是旧版,被 java.time class 取代,例如 ZonedDateTime.

使用特制的 class、YearWeek from the ThreeTen-Extra project, to track standard ISO 8601 weeks

自定义格式化程序

定义一个 DateTimeFormatter 对象来匹配您的非标准输入字符串。

org.threeten.extra.YearWeek
.parse(
    "202001" ,
    new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .appendValue( IsoFields.WEEK_BASED_YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
    .appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2)
    .toFormatter()
)
.toString()

2020-W01

标准格式化程序

或者操纵您的输入字符串以符合 ISO 8601 standard format,在 week-based-year 和 week 之间插入一个 -Wjava.time classes 和 ThreeTen-Extra classes 都使用 ISO 8601 格式当 parsing/generating 个字符串时默认。

String input = "202001";
String inputModified = input.substring( 0 , 4 ) + "-W" + input.substring( 4 );
YearWeek yearWeek = YearWeek.parse( inputModified ) ;

yearWeek.toString(): 2020-W01

避免遗留日期时间 classes

不要浪费时间去理解 Calendar。这个可怕的 class 多年前被 JSR 310 中定义的现代 java.time classes 所取代。

周的定义

您必须指定您对一周的定义。您是说第 1 周包含一年的第一天吗?或者第 1 周包含一周中的某一天?或者第 1 周是第一个完全由新年日期组成的日历周?或者也许是行业特定的周定义?其他定义?

关于 Calendar 的一个令人困惑的事情是它对一周的定义改变了 Locale。这是避免遗留问题的众多原因之一 class。

基于周的年份

根据您对周的定义,一周中的年份可能不是该周某些日期的日历年。基于周的年份可能与日历年重叠。

标准周和基于周的年份

例如,standard ISO 8601 week 将一周定义为:

  • 星期一开始,并且
  • 第 1 周包含日历年的第一个星期四。

所以每个基于周的年份有 52 或 53 个整周。当然,这意味着之前 and/or 之后日历年的某些日期可能会出现在我们基于周的年份的 first/last 周中。

org.threeten.extra.YearWeek

一个问题是您试图用 class 来表示年-周,该 class 表示时刻,在时区上下文中带有一天中的时间的日期。

相反,使用专门构建的 class。您可以在 ThreeTen-Extra library, YearWeek 中找到一个。该库扩展了 java.time class 内置于 Java 8 及更高版本中的功能。

有了 class 我认为我们可以定义一个 DateTimeFormatter 来使用格式模式 YYYYww 解析您的输入,其中 YYYY 表示 4 位数字year of week-based-year,ww 表示两位数的周数。像这样:

// FAIL
String input = "202001" ; 
DateTimeFormatter f = DateTimeFormatter.ofPattern( "YYYYww" ) ;
YearWeek yearWeek = YearWeek.parse( input , f ) ;

但是使用那个格式化程序会抛出一个 DateTimeParseException,原因让我无法理解。

Exception in thread "main" java.time.format.DateTimeParseException: Text '202001' could not be parsed: Unable to obtain YearWeek from TemporalAccessor: {WeekOfWeekBasedYear[WeekFields[SUNDAY,1]]=1, WeekBasedYear[WeekFields[SUNDAY,1]]=2020},ISO of type java.time.format.Parsed

Caused by: java.time.DateTimeException: Unable to obtain YearWeek from TemporalAccessor: {WeekOfWeekBasedYear[WeekFields[SUNDAY,1]]=1, WeekBasedYear[WeekFields[SUNDAY,1]]=2020},ISO of type java.time.format.Parsed

Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: WeekBasedYear

或者,我们可以使用 DateTimeFormatterBuilder 从零件构建 DateTimeFormatter。通过仔细阅读 the OpenJDK source code for Java 13 for DateTimeFormatter.ISO_WEEK_DATE,我能够拼凑出这个似乎有效的格式化程序。

DateTimeFormatter f =  
        new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .appendValue( IsoFields.WEEK_BASED_YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
        .appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2)
        .toFormatter()
;

使用那个:

String input = "202001" ; 
YearWeek yearWeek = YearWeek.parse( input , f ) ;

ISO 8601

让您的数据发布者了解 ISO 8601 以文本形式表示日期时间值的标准定义格式。

要生成表示我们 YearWeek 值的标准格式字符串,请调用 toString.

String output = yearWeek.toString() ;

2020-W01

并解析标准字符串。

YearWeek yearWeek = YearWeek.parse( "2020-W01" ) ;

Basil Bourque 已经提供了一个很好的答案。这是一个不需要外部依赖项的(前提是您使用的是 Java 8 或更高版本)。

java.time

    WeekFields wf = WeekFields.of(Locale.US);
    DateTimeFormatter yearWeekFormatter = new DateTimeFormatterBuilder()
            .appendValue(wf.weekBasedYear(), 4)
            .appendValue(wf.weekOfWeekBasedYear(), 2)
            .parseDefaulting(ChronoField.DAY_OF_WEEK, DayOfWeek.SUNDAY.getValue())
            .toFormatter();

    String input = "202001";
    LocalDate date = LocalDate.parse(input, yearWeekFormatter);
    System.out.println(date);

输出为:

2019-12-29

假设星期日是一周的第一天,第 1 周是包含 1 月 1 日的那一周,这是正确的:2020 年的第 1 周从 2019 年 12 月 29 日星期日开始。如果您想要定义的周其他方式,只需使用不同的 WeekFields 对象。

我建议您不要使用 Calendar class。 class 总是设计得很糟糕,现在已经过时了。相反,我使用 java.time,现代 Java 日期和时间 API。

有什么解释吗?

感谢 user85421 如何重现。您首先要创建一个代表当前日期的 Calendar 对象(实际上是 GregorianCalendar 的一个实例),在您的示例中,2020 年 11 月 22 日是星期日(显然已经将计算机时钟提前了将近一年)。然后,您将其年份设置为 2020 年(不变)并将其周数设置为 1。但是,正如我们在上面看到的,这会将日期更改为 2019 年 12 月 29 日,从而与您设置为 2020 年的年份发生冲突. 因此 GregorianCalendar 决定你在问不可能的事情并抛出异常。我得到的堆栈跟踪是:

java.lang.IllegalArgumentException: YEAR: 2020 -> 2019
    at java.base/java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2826)
    at java.base/java.util.Calendar.updateTime(Calendar.java:3395)
    at java.base/java.util.Calendar.getTimeInMillis(Calendar.java:1782)
    at java.base/java.util.Calendar.getTime(Calendar.java:1755)
    at ovv.misc.Test.main(Test.java:17)

在你的第二个例子中,你是 运行 你在 2020 年 11 月 27 日,一个星期五的程序。这次改成2020年1月3日星期五,所以还是2020年内,所以没有冲突,也就没有例外。

该解释假定您的默认语言环境是一年中的第 1 周定义为包含 1 月 1 日的那一周。在将我的计算机时间设置为 11 月之后,我运行 在我自己的语言环境中有您的代码2020 年 22 日,我的时区为 America/Chicago。未见异常(输出包括 Sun Jan 05 13:54:27 CST 2020)。我的语言环境遵循国际标准 ISO。星期一是一周的第一天,第 1 周是新年中至少有 4 天的第一周。所以 2020 年的第 1 周是从 2019 年 12 月 30 日星期一到 1 月 5 日星期日。我想在星期一或星期二我也可以在这个语言环境中重现你的问题,我没试过。

PS 如何解析整数

提示,要将字符串解析为 int,只需使用 Integer.parseInt(yourString)。无需创建新的 Integer 对象。

Link

Oracle tutorial: Date Time 解释如何使用 java.time.