java.time 比较不一致

java.time Comparison Is Not Consistent

我在我的代码中使用 LocalData 对象,我注意到使用 compareTo方法不一致
我使用 compareTo 方法来获得差异(以天为单位) 在两个日期之间。
但是,它似乎只适用于同一月份的日期。

我附上一个代码片段来演示这个问题:

import java.text.MessageFormat;
import java.time.LocalDate;

public class timeComparison {
    public static void main(String[] args) {
        LocalDate valentinesDay = LocalDate.of(2020, 2, 14);
        LocalDate darwinDay = LocalDate.of(2020, 2, 12);
        LocalDate leapDay = LocalDate.of(2020, 2, 29);
        LocalDate superTuesday = LocalDate.of(2020, 3, 3);

        String valentinesVsDarwin = MessageFormat.format(
                "Valentine day is {0} days after Darwin day",
                valentinesDay.compareTo(darwinDay)
        );
        System.out.println(valentinesVsDarwin);

        String darwinVsLeap = MessageFormat.format(
                "Leap day is {0} days after Darwin day",
                leapDay.compareTo(darwinDay)
        );
        System.out.println(darwinVsLeap);

        String superTuesdayVsLeap = MessageFormat.format(
                "Super Tuesday is {0} days after leap day",
                superTuesday.compareTo(leapDay)
        );
        System.out.println(superTuesdayVsLeap);
    }
}

我得到的输出是:

Valentine day is 2 days after Darwin day
Leap day is 17 days after Darwin day
Super Tuesday is 1 days after leap day

我期待得到 Super Tuesday is 3 days after leap day

我想知道是什么原因导致的问题 以及如何获得两个不同日期之间的差异。

TL;DR

compareTo 方法不适用于此用途。
它是为了指示顺序,而不是显示时间差异。

解开谜团

为了更好地理解这一点,可以查看LocalDate的源代码。
在任何 IDE 上,您可以选择 "Go To => Implementation" 来查看源代码。

public int compareTo(ChronoLocalDate other) {
    return other instanceof LocalDate ? this.compareTo0((LocalDate)other) : super.compareTo(other);
}

int compareTo0(LocalDate otherDate) {
    int cmp = this.year - otherDate.year;
    if (cmp == 0) {
        cmp = this.month - otherDate.month;
        if (cmp == 0) {
            cmp = this.day - otherDate.day;
        }
    }

    return cmp;
}

从上面的源码可以了解到compareTo方法 return日期之间的天数差异,而是 第一个参数中有差异的日期之间的差异 (可能是几年,可能是几个月,也可能是几天)。

compareTo 方法的目的是什么

为了理解上面的代码及其用途,
人们需要稍微了解一下 Java 中的比较是如何工作的。
Java 的 LocalDate class 实现 ChronoLocalDate, 这是一个可比较的对象。
这意味着该对象能够将自身与另一个对象进行比较。
为此,class 本身必须实现 java.lang.Comparable 比较其实例的界面。
Java's API documentation 中所述:

This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's compareTo method is referred to as its natural comparison method.

此外,compareTo方法的详细方法如下:

Compares this object with the specified object for order.
Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

意思是,为了进行比较,我们需要 return 一个符合上述规则的整数。 因此,数字本身没有任何意义, 而不是指示对象的 "order"。 这就是为什么如上所示的方法不计算天数差异,而是寻求对象自然顺序的直接指示:首先是年,然后是月,最后是天。
当您比较具有不同装载的日期时 - 该方法显示了这一点。
如果您要比较中的日期,这同样适用 2020 年和 2018 年的日期 - 结果是 2 / -2。

你应该如何衡量数据差异

我们发现 compareTo 不能用作日期差异的测量工具。
您可以根据需要使用 PeriodChronoUnit
来自 Java 的 Period and Duration tutorial

ChronoUnit

The ChronoUnit enum, discussed in the The Temporal Package, defines the units used to measure time.
The ChronoUnit.between method is useful when you want to measure an amount of time in a single unit of time only, such as days or seconds.
The between method works with all temporal-based objects, but it returns the amount in a single unit only.

Period

To define an amount of time with date-based values (years, months, days), use the Period class.
The Period class provides various get methods, such as getMonths, getDays, and getYears, so that you can extract the amount of time from the period.
The total period of time is represented by all three units together: months, days, and years.
To present the amount of time measured in a single unit of time, such as days, you can use the ChronoUnit.between method.

下面是演示上述方法用法的代码片段:

jshell> import java.time.LocalDate;

jshell> LocalDate darwinDay = LocalDate.of(2020, 2, 12);
darwinDay ==> 2020-02-12

jshell> LocalDate leapDay = LocalDate.of(2020, 2, 29);
leapDay ==> 2020-02-29

jshell> leapDay.compareTo(darwinDay);
 ==> 17

jshell> import java.time.Period;

jshell> Period.between(darwinDay, leapDay).getDays();
 ==> 17

jshell> import java.time.temporal.ChronoUnit;

jshell> ChronoUnit.DAYS.between(darwinDay, leapDay);
 ==> 17

jshell> LocalDate now = LocalDate.of(2020, 3, 28);
now ==> 2020-03-28

jshell> LocalDate yearAgo = LocalDate.of(2019, 3, 28);
yearAgo ==> 2019-03-28

jshell> yearAgo.compareTo(now);
 ==> -1

jshell> Period.between(yearAgo, now).getDays();
 ==> 0

jshell> ChronoUnit.DAYS.between(yearAgo, now);
 ==> 366

I was expecting to get Super Tuesday is 3 days after leap day.

这是一个错误的期望。查看 Java 如何比较 LocalDate:

的两个实例
int compareTo0(LocalDate otherDate) {
    int cmp = (year - otherDate.year);
    if (cmp == 0) {
        cmp = (month - otherDate.month);
        if (cmp == 0) {
            cmp = (day - otherDate.day);
        }
    }
    return cmp;
}

由于年份相同(2020 年),它给出了月份之间的差异,即 3 - 2 = 1

您应该使用 java.time.temporal.ChronoUnit::between 来计算 LocalDate 的两个实例之间的天数。

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class Main {
    public static void main(String[] args) {
        System.out.println(ChronoUnit.DAYS.between(LocalDate.of(2020, 2, 29), LocalDate.of(2020, 4, 3)));
    }
}

输出:

34