maio290
maio290

Reputation: 6732

Timestamp.from not heeding timezone from Instant

When I try to convert a ZonedDateTime to a Timestamp everything is fine until I call Timestamp.from() in the following code:

ZonedDateTime currentTimeUTC = ZonedDateTime.now(ZoneOffset.UTC);
currentTimeUTC = currentTimeUTC.minusSeconds(currentTimeUTC.getSecond());
currentTimeUTC = currentTimeUTC.minusNanos(currentTimeUTC.getNano());
return Timestamp.from(currentTimeUTC.toInstant());

ZonedDateTime.now(ZoneOffset.UTC); -> 2018-04-26T12:31Z
currentTimeUTC.toInstant() -> 2018-04-26T12:31:00Z
Timestamp.from(currentTimeUTC.toInstant()) -> 2018-04-26 14:31:00.0 
// (with Timezone of Europe/Berlin, which is currently +2)

Why is Timestamp.from() not heeding the timezone set in the instant?

Upvotes: 1

Views: 3105

Answers (3)

Basil Bourque
Basil Bourque

Reputation: 338566

tl;dr

  • You are being confused by the unfortunate behavior of Timestamp::toString to apply the JVM’s current default time zone to the objects internal UTC value.
  • ➡ Use Instant, never Timestamp.
  • A String such as 2018-04-26T12:31Z is in standard ISO 8601 format, with the Z being short for Zulu and meaning UTC.

Your entire block of code can be replaced with:

Instant.now()

…such as:

myPreparedStatement.setObject( … , Instant.now() ) ;

Details

The Answer by wowxts is correct. Instant is always in UTC, as is Timestamp, yet Timestamp::toString applies a time zone. This behavior is one of many poor design choices in those troubled legacy classes.

I'll add some other thoughts.

Use Instant for UTC

ZonedDateTime currentTimeUTC = ZonedDateTime.now(ZoneOffset.UTC);

While technically correct, this line is semantically wrong. If you want to represent a moment in UTC, use Instant class. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

Instant instant = Instant.now() ;  // Capture the current moment in UTC.

Avoid legacy Timestamp class

Timestamp.from(currentTimeUTC.toInstant());

While technically correct, using my suggest above, that would be:

Timestamp.from( instant ); // Convert from modern *java.time* class to troublesome legacy date-time class using new method added to the old class.

Nothing is lost going between Instant and Timestamp, as both represent a moment in UTC with a resolution of nanoseconds. However…

No need to be using java.sql.Timestamp at all! That class is part of the troublesome old date-time classes that are now legacy. They were supplanted entirely by the java.time classes defined by JSR 310. Timestamp is replaced by Instant.

JDBC 4.2

As of JDBC 4.2 and later, you can directly exchange java.time objects with your database.

Insert/Update.

myPreparedStatement.setObject( … , instant ) ;

Retrieval.

Instant instant = myResultSet.getObject( … , Instant.class ) ;

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.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

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.

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.

Upvotes: 3

wowxts
wowxts

Reputation: 56

The Instant class doesn't have a timezone, it just has the values of seconds and nanoseconds since unix epoch. A Timestamp also represents that (a count from epoch).

why is the debugger displaying this with a Z behind it?

The problem is in the toString methods:

Instant.toString() converts the seconds and nanoseconds values to the corresponding date/time in UTC - hence the "Z" in the end - and I believe it was made like that for convenience (to make the API more "developer-friendly").

The javadoc for toString says:

A string representation of this instant using ISO-8601 representation.
The format used is the same as DateTimeFormatter.ISO_INSTANT.

And if we take a look at DateTimeFormatter.ISO_INSTANT javadoc:

The ISO instant formatter that formats or parses an instant in UTC, such as '2011-12-03T10:15:30Z'

As debuggers usually uses the toString method to display variables values, that explains why you see the Instant with "Z" in the end, instead of the seconds/nanoseconds values.

On the other hand, Timestamp.toString uses the JVM default timezone to convert the seconds/nanos values to a date/time string.

But the values of both Instant and Timestamp are the same. You can check that by calling the methods Instant.toEpochMilli and Timestamp.getTime, both will return the same value.


Note: instead of calling minusSeconds and minusNanos, you could use the truncatedTo method:

ZonedDateTime currentTimeUTC = ZonedDateTime.now(ZoneOffset.UTC);
currentTimeUTC = currentTimeUTC.truncatedTo(ChronoUnit.MINUTES);

This will set all fields smaller than ChronoUnit.MINUTES (in this case, the seconds and nanoseconds) to zero.

You could also use withSecond(0) and withNano(0), but in this case, I think truncatedTo is better and more straight to the point.


Note2: the java.time API's creator also made a backport for Java 6 and 7, and in the project's github issues you can see a comment about the behaviour of Instant.toString. The relevant part to this question:

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

That reinforces my view that the toString method was designed like this for convenience and ease to use.

Upvotes: 4

Christian Riese
Christian Riese

Reputation: 614

Instant does not hold the Timezone information. It only holds the seconds and nanos. To when you convert your ZonedDateTime into an Instant the information is lost. When converting into Timestamp then the Timestamp will hold the default Timezone, which is, in your case, Europe/Berlin.

Upvotes: 2

Related Questions