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);
The way I understand it... Instant is a UTC style time, agnostic of zone always UTC. LocalTime is a time independent of given zone. So you'd expect the following would work given that Instant
implements TemporalAccessor
,
Instant instant = Instant.now();
LocalTime local = LocalTime.from(instant);
but you get "Unable to obtain LocalTime from TemporalAccessor" error. Instead you need to state where "local" is. There is no default - probably a good thing.
Instant instant = Instant.now();
LocalTime local = LocalTime.from(instant.atZone(ZoneId.of("GMT+3")));
System.out.println(String.format("%s => %s", instant, local));
Output
2014-12-07T07:52:43.900Z => 10:52:43.900
instantStart.get(ChronoField.HOUR_OF_DAY)
throws an error because it does not conceptually support it, you can only access HOUR_OF_DAY etc. via a LocalTime instance.
Best Answer
tl;dr
Instant
andLocalDateTime
are two entirely different animals: One represents a moment, the other does not.Instant
represents a moment, a specific point in the timeline.LocalDateTime
represents a date and a time-of-day. But lacking a time zone or offset-from-UTC, this class cannot represent a moment. It represents potential moments along a range of about 26 to 27 hours, the range of all time zones around the globe. ALocalDateTime
value is inherently ambiguous.Incorrect Presumption
Your statement is incorrect: A
LocalDateTime
has no time zone. Having no time zone is the entire point of that class.To quote that class’ doc:
So
Local…
means “not zoned, no offset”.Instant
An
Instant
is a moment on the timeline in UTC, a count of nanoseconds since the epoch of the first moment of 1970 UTC (basically, see class doc for nitty-gritty details). Since most of your business logic, data storage, and data exchange should be in UTC, this is a handy class to be used often.OffsetDateTime
The class
OffsetDateTime
class represents a moment as a date and time with a context of some number of hours-minutes-seconds ahead of, or behind, UTC. The amount of offset, the number of hours-minutes-seconds, is represented by theZoneOffset
class.If the number of hours-minutes-seconds is zero, an
OffsetDateTime
represents a moment in UTC the same as anInstant
.ZoneOffset
The
ZoneOffset
class represents an offset-from-UTC, a number of hours-minutes-seconds ahead of UTC or behind UTC.A
ZoneOffset
is merely a number of hours-minutes-seconds, nothing more. A zone is much more, having a name and a history of changes to offset. So using a zone is always preferable to using a mere offset.ZoneId
A time zone is represented by the
ZoneId
class.A new day dawns earlier in Paris than in Montréal, for example. So we need to move the clock’s hands to better reflect noon (when the Sun is directly overhead) for a given region. The further away eastward/westward from the UTC line in west Europe/Africa the larger the offset.
A time zone is a set of rules for handling adjustments and anomalies as practiced by a local community or region. The most common anomaly is the all-too-popular lunacy known as Daylight Saving Time (DST).
A time zone has the history of past rules, present rules, and rules confirmed for the near future.
These rules change more often than you might expect. Be sure to keep your date-time library's rules, usually a copy of the 'tz' database, up to date. Keeping up-to-date is easier than ever now in Java 8 with Oracle releasing a Timezone Updater Tool.
Specify a proper time zone name in the format of
Continent/Region
, such asAmerica/Montreal
,Africa/Casablanca
, orPacific/Auckland
. Never use the 2-4 letter abbreviation such asEST
orIST
as they are not true time zones, not standardized, and not even unique(!).ZonedDateTime
Think of
ZonedDateTime
conceptually as anInstant
with an assignedZoneId
.To capture the current moment as seen in the wall-clock time used by the people of a particular region (a time zone):
Nearly all of your backend, database, business logic, data persistence, data exchange should all be in UTC. But for presentation to users you need to adjust into a time zone expected by the user. This is the purpose of the
ZonedDateTime
class and the formatter classes used to generate String representations of those date-time values.You can generate text in localized format using
DateTimeFormatter
.LocalDate
,LocalTime
,LocalDateTime
The "local" date time classes,
LocalDateTime
,LocalDate
,LocalTime
, are a different kind of critter. The are not tied to any one locality or time zone. They are not tied to the timeline. They have no real meaning until you apply them to a locality to find a point on the timeline.The word “Local” in these class names may be counter-intuitive to the uninitiated. The word means any locality, or every locality, but not a particular locality.
So for business apps, the "Local" types are not often used as they represent just the general idea of a possible date or time not a specific moment on the timeline. Business apps tend to care about the exact moment an invoice arrived, a product shipped for transport, an employee was hired, or the taxi left the garage. So business app developers use
Instant
andZonedDateTime
classes most commonly.So when would we use
LocalDateTime
? In three situations:Notice that none of these three cases involve a single certain specific point on the timeline, none of these are a moment.
One time-of-day, multiple moments
Sometimes we want to represent a certain time-of-day on a certain date, but want to apply that into multiple localities across time zones.
For example, "Christmas starts at midnight on the 25th of December 2015" is a
LocalDateTime
. Midnight strikes at different moments in Paris than in Montréal, and different again in Seattle and in Auckland.Another example, "Acme Company has a policy that lunchtime starts at 12:30 PM at each of its factories worldwide" is a
LocalTime
. To have real meaning you need to apply it to the timeline to figure the moment of 12:30 at the Stuttgart factory or 12:30 at the Rabat factory or 12:30 at the Sydney factory.Booking appointments
Another situation to use
LocalDateTime
is for booking future events (ex: Dentist appointments). These appointments may be far enough out in the future that you risk politicians redefining the time zone. Politicians often give little forewarning, or even no warning at all. If you mean "3 PM next January 23rd" regardless of how the politicians may play with the clock, then you cannot record a moment – that would see 3 PM turn into 2 PM or 4 PM if that region adopted or dropped Daylight Saving Time, for example.For appointments, store a
LocalDateTime
and aZoneId
, kept separately. Later, when generating a schedule, on-the-fly determine a moment by callingLocalDateTime::atZone( ZoneId )
to generate aZonedDateTime
object.If needed, you can adjust to UTC. Extract an
Instant
from theZonedDateTime
.Unknown zone
Some people might use
LocalDateTime
in a situation where the time zone or offset is unknown.I consider this case inappropriate and unwise. If a zone or offset is intended but undetermined, you have bad data. That would be like storing a price of a product without knowing the intended currency (dollars, pounds, euros, etc.). Not a good idea.
All date-time types
For completeness, here is a table of all the possible date-time types, both modern and legacy in Java, as well as those defined by the SQL standard. This might help to place the
Instant
&LocalDateTime
classes in a larger context.Notice the odd choices made by the Java team in designing JDBC 4.2. They chose to support all the java.time times… except for the two most commonly used classes:
Instant
&ZonedDateTime
.But not to worry. We can easily convert back and forth.
Converting
Instant
.Converting
ZonedDateTime
.About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as
java.util.Date
,Calendar
, &SimpleDateFormat
.To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for
java.sql.*
classes. Hibernate 5 & JPA 2.2 support java.time.Where to obtain the java.time classes?
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as
Interval
,YearWeek
,YearQuarter
, and more.