40

java.time has an Instant class which encapsulates a position (or 'moment') on the timeline. While I understand that this is a seconds/nanoseconds value so not directly related to time zones or times offsets, its toString returns a date and time formatted as a UTC date/time, eg 2014-05-13T20:05:08.556Z. Also anInstant.atZone(zone) and anInstant.atOffset(offset) both produce a value that is consistent with treating the Instant as having an implied UTC time-zone/'zero' offset.

I would have expected therefore:

  • ZoneOffset.from(anInstant) to produce a 'zero' ZoneOffset
  • OffsetDateTime.from(anInstant) to produce a date/time with a 'zero' offset
  • ZoneId.from(anInstant) (probably) to produce a UTC ZoneId
  • ZonedDateTime.from(anInstant) (probably) to produce a ZonedDateTime with a UTC ZoneId

The documentation for ZonedDateTime.from, as I read it, appears to endorse this.

In fact ZoneOffset.from(anInstant) fails with DateTimeException, and I suppose for that reason OffsetDateTime.from(anInstant) also fails, as do the other two.

Is this the expected behaviour?

assylias
  • 321,522
  • 82
  • 660
  • 783
user3627702
  • 417
  • 1
  • 5
  • 4

4 Answers4

54

Short answer:

The JSR-310-designers don't want people to do conversions between machine time and human time via static from()-methods in types like ZoneId, ZoneOffset, OffsetDateTime, ZonedDateTime etc. This is explicitly specified if you carefully study the javadoc. Instead use:

OffsetDateTime#toInstant():Instant
ZonedDateTime#toInstant():Instant
Instant#atOffset(ZoneOffset):OffsetDateTime
Instant#atZone(ZoneId):ZonedDateTime

The problem with the static from()-methods is that otherwise people are able to do conversions between an Instant and for example a LocalDateTime without thinking about the timezone.

Long answer:

Whether to consider an Instant as counter or as field tuple, the answer given by JSR-310-team was a strict separation between so-called machine time and human time. Indeed they intend to have a strict separation - see their guidelines. So finally they want Instant to be only interpreted as a machine time counter. So they intentionally have a design where you cannot ask an Instant for fields like year, hour etc.

But indeed, the JSR-310-team is not consistent at all. They have implemented the method Instant.toString() as a field tuple view including year, ..., hour, ... and offset-symbol Z (for UTC-timezone) (footnote: Outside of JSR-310 this is quite common to have a field-based look on such machine times - see for example in Wikipedia or on other sites about TAI and UTC). Once the spec lead S. Colebourne said in a comment on a threeten-github-issue:

"If we were really hard line, the toString of an Instant would simply be the number of seconds from 1970-01-01Z. We chose not to do that, and output a more friendly toString to aid developers, But it doesn't change the basic fact that an Instant is just a count of seconds, and cannot be converted to a year/month/day without a time-zone of some kind."

People can like this design decision or not (like me), but the consequence is that you cannot ask an Instant for year, ..., hour, ... and offset. See also the documentation of supported fields:

NANO_OF_SECOND 
MICRO_OF_SECOND 
MILLI_OF_SECOND 
INSTANT_SECONDS 

Here it is interesting what is missing, above all a zone-related field is missing. As a reason, we often hear the statement that objects like Instant or java.util.Date have no timezone. In my opinion this is a too simplistic view. While it is true that these objects have no timezone state internally (and there is also no need for having such an internal value), those objects MUST be related to UTC timezone because this is the basis of every timezone offset calculation and conversion to local types. So the correct answer would be: An Instant is a machine counter counting the seconds and nanoseconds since UNIX epoch in timezone UTC (per spec). The last part - relationship to UTC zone - is not well specified by JSR-310-team but they cannot deny it. The designers want to abolish the timezone aspect from Instant because it looks human-time-related. However, they can't completely abolish it because that is a fundamental part of any internal offset calculation. So your observation regarding

"Also an Instant.atZone(zone) and an Instant.atOffset(offset) both produce a value that is consistent with treating the Instant as having an implied UTC time-zone/'zero' offset."

is right.

While it might be very intuitive that ZoneOffset.from(anInstant) might produce ZoneOffset.UTC, it throws an exception because its from()-method searches for a non-existent OFFSET_SECONDS-field. The designers of JSR-310 have decided to do that in the specification for the same reason, namely to make people think that an Instant has officially nothing to do with UTC timezone i.e. "has no timezone" (but internally they must accept this basic fact in all internal calculations!).

For the same reason, OffsetDateTime.from(anInstant) and ZoneId.from(anInstant) fail, too.

About ZonedDateTime.from(anInstant) we read:

"The conversion will first obtain a ZoneId from the temporal object, falling back to a ZoneOffset if necessary. It will then try to obtain an Instant, falling back to a LocalDateTime if necessary. The result will be either the combination of ZoneId or ZoneOffset with Instant or LocalDateTime."

So this conversion will fail again due to the same reasons because neither ZoneId nor ZoneOffset can be obtained from an Instant. The exception message reads as:

"Unable to obtain ZoneId from TemporalAccessor: 1970-01-01T00:00:00Z of type java.time.Instant"

Finally we see that all static from()-methods suffer from being unable to do a conversion between human time and machine time even if this looks intuitive. In some cases a conversion between let's say LocalDate and Instant is questionable. This behaviour is specified, but I predict that your question is not the last question of this kind and many users will continue to be confused.

The real design problem in my opinion is that:

a) There should not be a sharp separation between human time and machine time. Temporal objects like Instant should better behave like both. An analogy in quantum mechanics: You can view an electron both as a particle and a wave.

b) All static from()-methods are too public. Ihat is too easily accessible in my opinion and should better have been removed from public API or use more specific arguments than TemporalAccessor. The weakness of these methods is that people can forget to think about related timezones in such conversions because they start the query with a local type. Consider for example: LocalDate.from(anInstant) (in which timezone???). However, if you directly ask an Instant for its date like instant.getDate(), personally I would consider the date in UTC-timezone as valid answer because here the query starts from an UTC perspective.

c) In conclusion: I absolutely share with the JSR-310-team the good idea to avoid conversions between local types and global types like Instant without specifying a timezone. I just differ when it comes to the API-design to prevent users from doing such a timezone-less conversion. My preferred way would have been to restrict the from()-methods rather than saying that global types should not have any relation to human-time-formats like calendar-date or wall-time or UTC-timezone-offset.

Anyway, this (inconsequent) design of separation between machine time and human time is now set in stone due to preserving backward compatibility, and everyone who wants to use the new java.time-API has to live with it.

Sorry for a long answer, but it is pretty tough to explain the chosen design of JSR-310.

Necreaux
  • 9,451
  • 7
  • 26
  • 43
Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • I agree with pretty well everything you said. I don't care too much that you can't use an`Instant`in any other `.from` method, as there are viable alternatives, but as I said before these things should be documented! Sorry @IdanArye I disagree with you. I've done a programmatic analysis of which`TemporalAccessor`objects are supported as parameter to the`from`methods in the`java.time` classes. The results for`Instant`are:`Instant.from` does not support `DayOfWeek`, `LocalDate`, `LocalDateTime`, `LocalTime`, `Month`, `MonthDay`, `OffsetTime`, `Year`, `YearMonth`, `ZoneOffset` – user3627702 May 16 '14 at 15:35
  • @user3627702 I have always considered it as funny to say that an `Instant` has no concept of date and time, but offering a field-based implementation for `toString()` and even an extra `InstantFormatter`. Well, nobody wants to see logging timestamps as machine counters so the JSR-310-team was here under pressure to partially deviate from their own machine-time-view. Anyway, your idea about an extra documentation line of this behaviour for non-support of `Instant` in from()-methods is very good. – Meno Hochschild May 16 '14 at 21:01
  • 3
    The design is driven to avoid things like LocalDate.from(anInstant) as that would be the source of many bugs (people would complain that such a conversion uses UTC rather than the default time-zone, and developers in the UK would find code works in the winter, but not the summer, so bugs would get into production) – JodaStephen May 23 '14 at 06:21
  • 1
    @JodaStephen Yes, exactly. And what do you think about an extra documentation line in static from()-methods with roughly the content of your comment about the motivation of the choosen design? Any chance to get the documentation updated for reducing confusion? – Meno Hochschild May 24 '14 at 19:21
  • 2
    Getting any change into the JDK is hard, and they don't especially like "explanation" type comments. I may be able to find a way though. – JodaStephen Jul 01 '14 at 15:39
  • At least implementations could fix that misleading message. There is a zone id in the message that says there is no zone id! I lost two hours trying to work out why a perfectly good zoned date-time as shown in the exception text could not be converted to ZonedDateTime. And ZonedDateTime.from(someInstant) should not compile if it can never succeed. – Mark Wood Jun 23 '20 at 16:23
19

The Instant class does not have a time zone. It gets printed like it's in the UTC zone because it has to be printed in some zone(you wouldn't want it to be printed in ticks, would you?), but that's not it's time zone - as demonstrated in the following example:

Instant instant=Instant.now();
System.out.println(instant);//prints 2014-05-14T06:18:48.649Z
System.out.println(instant.atZone(ZoneId.of("UTC")));//prints 2014-05-14T06:18:48.649Z[UTC]

The signatures of ZoneOffset.from and OffsetDateTime.from accept any temporal object, but they fail for some types. java.time does not seem to have an interface for temporals that have a timezone or offset. Such an interface could have declare getOffset and getZone. Since we don't have such interface, these methods are declared separately in multiple places.

If you had a ZonedDateTime you could call it's getOffset. If you had an OffsetDateTime you could also call it's offset - but these are two different getOffset methods, as the two classes don't get that method from a common interface. That means that if you have a Temporal object that could be either you would have to test if it's an instanceof both of them to get the offset.

OffsetDateTime.from simplifies the process by doing that for you - but since it also can't rely on a common interface, it has to accept any Temporal object and throw an exception for those that don't have an offset.

Idan Arye
  • 12,402
  • 5
  • 49
  • 68
  • 1
    Thanks for that. The bottom line is that `Instant` can't be used as a valid parameter to any of the `from` methods. It would be good if single line to that effect were added to the API documentation for each data type. – user3627702 May 14 '14 at 11:00
  • It's not just `Instance` - `from` will fail for any object that does not have all the required information. For example - `ZonedDateTime.from(Year.now())` also fails. No point in listing all the classes that don't work in each `from` method's documentation - instead they simply specify that it throws `DateTimeException` if the conversion is not possible. – Idan Arye May 14 '14 at 11:42
  • One correction: The symbol Z **IS** UTC following ISO-8601-paper. To say that "Z" and "Z[UTC]" have different meaning is very questionable. Personally I consider latter case as just redundant. – Meno Hochschild May 14 '14 at 14:06
  • @MenoHochschild In this specific case they do have different meanings. The `Z` states that the time is printed according to the UTC timezone, while the `[UTC]` states that the `Temporal` object is set to the UTC timezone. In the first case, the `Instant` object is not really in the UTC timezone - it only gets printed that way because that's the default. – Idan Arye May 14 '14 at 14:49
  • I disagree with your statement that "an `Instant` is not really in the UTC timezone". It is also not a question of an arbitrary default setting. This class MUST use UTC timezone as reference for any conversions and calculations - equal what the JSR-310-spec officially says. We should also have the domain specific view as superior view in mind. – Meno Hochschild May 14 '14 at 15:40
  • 1
    Of course `Instant` is represented internally as UTC, but the idea of this class is to abstract away the timezone. As far as the `from` methods care an `Instant` doesn't have a timezone they can use. – Idan Arye May 14 '14 at 16:19
  • The Z[UTC] is shown because ZoneId.of("UTC") is not the same as ZoneOffset.UTC. It was originally, but caused issues in testing, so was changed to be different. You can normalize ZoneId.of("UTC") to ZoneOffset.UTC using normalized() http://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#normalized-- – JodaStephen May 23 '14 at 06:16
  • Note also that the TemporalAccessor interface does allow access to the offset and zone. You simply call query(TemporalQuery) using a constant from TemporalQueries http://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalQueries.html . Or for simplicity, use ZoneOffset.from(temporal) – JodaStephen May 23 '14 at 06:17
  • @JodaStephen different? Normalize? Caused issues? Interesting statements, but I would like to learn the how and why? – YoYo Nov 24 '15 at 16:33
  • Maybe it is more safe to use this way? It worked for me: toInstant().atZone(ZoneId.systemDefault()) – João Neves Filho Sep 16 '19 at 00:25
13

Just an example w.r.t conversions, i believe some folks will get below exception

(java.time.DateTimeException: Unable to obtain LocalDateTime from TemporalAccessor: 2014-10-24T18:22:09.800Z of type java.time.Instant)

if they try -

LocalDateTime localDateTime = LocalDateTime.from(new Date().toInstant());

to resolve the issue, please pass in zone -

LocalDateTime localDateTime = LocalDateTime.from(new Date().toInstant().atZone(ZoneId.of("UTC")));

rohtakdev
  • 956
  • 1
  • 13
  • 16
0

The key to to get a LocalDateTime from an Instant is to provide the system's TimeZone:

LocalDateTime.ofInstant(myDate.toInstant(), ZoneId.systemDefault())

As a side note, beware of multi-zone cloud servers as the timezone will surely change.

ATorras
  • 4,073
  • 2
  • 32
  • 39