Java 8 java.time: Adding TemporalUnit in Instant v

2019-03-10 23:10发布

问题:

I'm playing around with the new java.time package in Java 8. I have a legacy database that gives me java.util.Date, which I convert to Instant.

What I am trying to do is add a period of time that is based off of another database flag. I could be adding days, weeks, months, or years. I don't want to have to care what I am adding, and I would like to be able to add more options in the future.

My first thought was Instant.plus(), but that gives me an UnsupportedTemporalTypeException for values greater than a day. Instant apparently does not support operations on large units of time. Fine, whatever, LocalDateTime does.

So that gives me this code:

private Date adjustDate(Date myDate, TemporalUnit unit){
    Instant instant = myDate.toInstant();
    LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    dateTime = dateTime.plus(1, unit);
    Instant updatedInstant = dateTime.atZone(ZoneId.systemDefault()).toInstant();
    return new Date(dueInstant.toEpochMilli());
}

Now, this is my first time using the new time API, so I may have missed something here. But it seems clunky to me that I have to go:

Date --> Instant --> LocalDateTime --> do stuff--> Instant --> Date.

Even if I did not have to use the Date part, I would still think it was a bit awkward. So my question is this, am I doing this completely wrong and what is the best way to do this?


Edit: Expanding on the discussion in the comments.

I think I have a better idea now about how LocalDateTime and Instant are playing with java.util.Date and java.sql.Timestamp. Thanks everyone.

Now, a more practical consideration. Let's say a user sends me a date from wherever they are in the world, arbitrary time zone. They send me 2014-04-16T13:00:00 which I can parse into a LocalDateTime. I then convert this directly to a java.sql.Timestamp and persist in my database.

Now, without doing anything else, I pull my java.sql.timestamp from my database, convert to LocalDateTime using timestamp.toLocalDateTime(). All good. Then I return this value to my user using the ISO_DATE_TIME formatting. The result is 2014-04-16T09:00:00.

I assume this difference is because of some type of implicit conversion to/from UTC. I think my default time zone may be getting applied to the value (EDT, UTC-4) which would explain why the number is off by 4 hours.

New question(s). Where is the implicit conversion from local time to UTC happening here? What is the better way to preserve time zones. Should I not be going directly from Local time as a string (2014-04-16T13:00:00) to LocalDateTime? Should I be expecting a time zone from the user input?

回答1:

I will go ahead and post an answer based on my final solution and a sort of summary of the very long comment chain.

To start, the whole conversion chain of:

Date --> Instant --> LocalDateTime --> Do stuff --> Instant --> Date

Is necessary to preserve the time zone information and still do operations on a Date like object that is aware of a Calendar and all of the context therein. Otherwise we run the risk of implicitly converting to the local time zone, and if we try to put it into a human readable date format, the times may have changed because of this.

For example, the toLocalDateTime() method on the java.sql.Timestamp class implicitly converts to the default time zone. This was undesirable for my purposes, but is not necessarily bad behavior. It is important, however, to be aware of it. That is the issue with converting directly from a legacy java date object into a LocalDateTime object. Since legacy objects are generally assumed to be UTC, the conversion uses the local timezone offset.

Now, lets say our program takes the input of 2014-04-16T13:00:00 and save to a database as a java.sql.Timestamp.

//Parse string into local date. LocalDateTime has no timezone component
LocalDateTime time = LocalDateTime.parse("2014-04-16T13:00:00");

//Convert to Instant with no time zone offset
Instant instant = time.atZone(ZoneOffset.ofHours(0)).toInstant();

//Easy conversion from Instant to the java.sql.Timestamp object
Timestamp timestamp = Timestamp.from(instant);

Now we take a timestamp and add some number of days to it:

Timestamp timestamp = ...

//Convert to LocalDateTime. Use no offset for timezone
LocalDateTime time = LocalDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.ofHours(0));

//Add time. In this case, add one day.
time = time.plus(1, ChronoUnit.DAYS);

//Convert back to instant, again, no time zone offset.
Instant output = time.atZone(ZoneOffset.ofHours(0)).toInstant();

Timestamp savedTimestamp = Timestamp.from(output);

Now we just need to output as a human readable String in the format of ISO_LOCAL_DATE_TIME.

Timestamp timestamp = ....
LocalDateTime time = LocalDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.ofHours(0));
String formatted = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(time);