SimpleDateFormat 宽容导致意外行为

SimpleDateFormat leniency leads to unexpected behavior

我发现 SimpleDateFormat::parse(String source)'s behavior is (unfortunatelly) defaultly set as lenient: setLenient(true)

By default, parsing is lenient: If the input is not in the form used by this object's format method but can still be parsed as a date, then the parse succeeds.

如果我将宽大度设置为 false,文档中说在严格解析的情况下,输入必须与该对象的格式相匹配。我在没有宽松模式的情况下使用了 SimpleDateFormat 配对,并且错误地在日期中输入了错误(字母 o 而不是数字 0)。 (这里是简短的工作代码:)

// PASSED (year 199)
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.mm.yyyy");
System.out.println(simpleDateFormat.parse("03.12.199o"));
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("03.12.199o"));        //WTF?

令我惊讶的是,这已经过去了,没有 ParseException 被抛出。我会更进一步:

// PASSED (year 1990)
String string = "just a String to mess with SimpleDateFormat";

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.mm.yyyy");
System.out.println(simpleDateFormat.parse("03.12.1990" + string));
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("03.12.1990" + string));

让我们继续:

// FAILED on the 2nd line
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.mm.yyyy");
System.out.println(simpleDateFormat.parse("o3.12.1990"));
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("o3.12.1990"));

最后抛出异常:Unparseable date: "o3.12.1990"。我想知道宽大程度有何不同,为什么我的第一个代码片段的最后一行没有抛出异常?文档说:

With strict parsing, inputs must match this object's format.

我的输入显然严格不匹配格式 - 我希望这种解析非常严格。为什么这(不)发生?

宽容不是整个输入是否匹配,而是格式是否匹配。您的输入仍然可以是 3.12.1990somecrap 并且它会起作用。

实际解析是在 parse(String, ParsePosition) 中完成的,您也可以使用它。基本上 parse(String) 将传递一个设置为从索引 0 开始的 ParsePosition ,当解析完成时,将检查该位置的当前索引。

如果它仍然是 0,则输入的开头与格式不匹配,即使在宽松模式下也不匹配。

但是,对于解析器来说 03.12.199 是一个有效日期,因此它在索引 8 处停止——它不是 0,因此解析成功。如果您想检查是否所有内容都已解析,您必须传递自己的 ParsePosition 并检查索引是否与输入的长度匹配。

如果您使用 setLenient(false),它仍会解析日期,直到满足所需的模式。但是,它将检查输出日期是否为 valid 日期。在您的情况下, 03.12.199 是一个有效日期,因此它不会引发异常。让我们举个例子来理解 setLenient(false)setLenient(true)/default 的不同之处。

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy"); 
System.out.println(simpleDateFormat.parse("31.02.2018"));

以上将给我输出:Sat Mar 03 00:00:00 IST 2018

但是下面的代码抛出 ParseException,因为 31.02.2018 不是 valid/possible 日期:

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy");
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("31.02.2018"));

Why does this (not) happen?

文档中没有很好地解释。

With lenient parsing, the parser may use heuristics to interpret inputs that do not precisely match this object's format. With strict parsing, inputs must match this object's format.

不过,文档确实有点帮助,它提到 DateFormat 使用的 Calendar 对象是宽松的。 Calendar 对象不用于解析本身,而是用于将解析的值解释为日期和时间(我引用 DateFormat 文档,因为 SimpleDateFormat 是 [=13= 的子类]).

  • SimpleDateFormat,无论是否宽松,都将接受 3 位数的年份,例如 199,即使您在 yyyy 中指定格式模式字符串。文档说的是年份:

    For parsing, if the number of pattern letters is more than 2, the year is interpreted literally, regardless of the number of digits. So using the pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.

  • DateFormat,无论是否宽松,都会接受并忽略解析文本后的文本,就像第一个示例中的小写字母 o 一样。它反对文本之前或内部的意外文本,就像在上一个示例中将字母 o 放在前面一样。 DateFormat.parse 的文档说:

    The method may not use the entire text of the given string.

  • 正如我间接所说的那样,在将解析值解释为日期和时间时,宽大程度会有所不同。因此,宽松的 SimpleDateFormat 会将 29.02.2019 解释为 01.03.2019,因为 2019 年 2 月只有 28 天。严格的 SimpleDateFormat 将拒绝这样做并抛出异常。默认的宽松行为会导致非常令人惊讶和完全无法解释的结果。举个简单的例子,按错误的顺序给出日、月和年:1990.03.12 将导致 August 11 year AD 17 AD (2001 years ago).

解决方案

VGR 已经在评论中提到 LocalDate 来自 java.time,现代 Java 日期和时间 API。根据我的经验,java.time 比旧的日期和时间 类 更好用,所以让我们试一试。首先尝试正确的日期字符串:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.mm.yyyy");
    System.out.println(LocalDate.parse("03.12.1990", dateFormatter));

我们得到:

java.time.format.DateTimeParseException: Text '03.12.1990' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {Year=1990, DayOfMonth=3, MinuteOfHour=12},ISO of type java.time.format.Parsed

这是因为我使用了你的dd.mm.yyyy格式模式字符串,其中小写mm表示分钟。当我们足够仔细地阅读错误消息时,它确实指出 DateTimeFormatter 将 12 解释为小时的分钟,这不是我们想要的。虽然 SimpleDateFormat 默许了这一点(即使在严格的情况下),但 java.time 更有助于指出我们的错误。该消息仅间接表示缺少月份值。我们需要使用大写 MM 表示月份。同时,我正在尝试使用错字的日期字符串:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
    System.out.println(LocalDate.parse("03.12.199o", dateFormatter));

我们得到:

java.time.format.DateTimeParseException: Text '03.12.199o' could not be parsed at index 6

索引 6 表示 199。它反对,因为我们指定了 4 位数字并且只提供 3 位。文档说:

The count of letters determines the minimum field width …

它也会反对日期之后未解析的文本。简而言之,在我看来,它为您提供了您所期望的一切。

链接