java.time.Instant.plus(long amountToAdd, TemporalU

2019-08-19 09:07发布

问题:

I trying to add few years to current time. My code looks like:

// ten yeas ago
int backYears = 10;
Instant instant = ChronoUnit.YEARS.addTo(Instant.now(), -backYears);

But I got an exception:

java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Years
at java.time.Instant.plus(Instant.java:862)

When I opened the method Instant.plus I see the following:

@Override
public Instant plus(long amountToAdd, TemporalUnit unit) {
    if (unit instanceof ChronoUnit) {
        switch ((ChronoUnit) unit) {
            case NANOS: return plusNanos(amountToAdd);
            case MICROS: return plus(amountToAdd / 1000_000, (amountToAdd % 1000_000) * 1000);
            case MILLIS: return plusMillis(amountToAdd);
            case SECONDS: return plusSeconds(amountToAdd);
            case MINUTES: return plusSeconds(Math.multiplyExact(amountToAdd, SECONDS_PER_MINUTE));
            case HOURS: return plusSeconds(Math.multiplyExact(amountToAdd, SECONDS_PER_HOUR));
            case HALF_DAYS: return plusSeconds(Math.multiplyExact(amountToAdd, SECONDS_PER_DAY / 2));
            case DAYS: return plusSeconds(Math.multiplyExact(amountToAdd, SECONDS_PER_DAY));
        }
        throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
    }
    return unit.addTo(this, amountToAdd);
}

As you can see MONTHS and YEARS are unsupported. But why? With an old java.util.Calendar I can do that easily:

    Calendar c = Calendar.getInstance();
    c.setTime(date);
    c.add(Calendar.YEAR, amount);
    return c.getTime(); 

The only one reason what I guess is that we don't know how many days in a month and year because of leap day 29 Feb. But to be honest we also have a leap second. Thus I think that this is a bug and all ChronoUnits should be supported. The only one question is: do we need to take in account leap second and leap day. As for my needs it's okay just to assume that month has 30 days and year 365. I don't need to make something like Calendar.roll() but this can satisfy me too.

回答1:

Let’s try something out. I am taking an instant as ZonedDateTime and subtracting 10 years in different time zones.

    OffsetDateTime origin = OffsetDateTime.of(2018, 3, 1, 0, 0, 0, 0, ZoneOffset.UTC);
    Instant originInstant = origin.toInstant();
    Instant tenYearsBackKyiv = origin.atZoneSameInstant(ZoneId.of("Europe/Kiev"))
            .minusYears(10)
            .toInstant();
    long hoursSubtractedKyiv = ChronoUnit.HOURS.between(tenYearsBackKyiv, originInstant);
    System.out.println("Hours subtracted in Київ: " + hoursSubtractedKyiv);
    Instant tenYearsBackSaoPaulo = origin.atZoneSameInstant(ZoneId.of("America/Sao_Paulo"))
            .minusYears(10)
            .toInstant();
    long hoursSubtractedSaoPaulo = ChronoUnit.HOURS.between(tenYearsBackSaoPaulo, originInstant);
    System.out.println("Hours subtracted in São Paulo: " + hoursSubtractedSaoPaulo);

The output is:

Hours subtracted in Київ: 87648
Hours subtracted in São Paulo: 87672

As you can see, 24 hours more (1 day more) is subtracted in São Paulo compared to Київ (Kyiv, Kiev). You may already have figured out that it’s because there we pass from 1 March to 29 February three times in leap years, in Київ only twice.

The old and now outdated Calendar class always had a time zone in it, so knew in which time zone to subtract years (another thing is it was happy to give you a result even in situations where it was unclear which result you wanted). The modern classes ZonedDateTime, OffsetDateTime and LocalDateTime can do the same. So use them. An Instant conceptually doesn’t have a time zone, so refuses to do operations that depend on time zone (I know it’s implemented using UTC, but we should regard this as an irrelevant implementation detail, not as a part of the specification of the interface to the class).

Neither the old nor the modern classes take leap seoncds into account, and you are right, only therefore can an Instant add and subtract days, hours and minutes.