Context
java.sql.Timestamp
extends java.util.Date
. The latter is a lie - it does not represent dates at all. It represents time since the epoch without a timezone, and in that sense, the direct equivalent in the java.time
package is in fact java.time.Instant
, and it's best to think of them that way.
Given that the java.util
time types (and therefore, all the java.sql
time types too, given that they all extend j.u.Date
!) are essentially broken due to being confused about their nature (such as Date
being named, well, Date
), you shouldn't use these types. Anywhere. Ever.
Do it right
Fortunately, you don't have to use the broken types. There is no need to mess with java.sql.Timestamp
. Ever. The JDBC5 spec requires that JDBC drivers support the LocalDate
, LocalTime
, LocalDateTime
, and OffsetDateTime
modern java.time
types for, respectively, SQL types DATE
, TIME WITHOUT TIMEZONE
, TIMESTAMP WITHOUT TIMEZONE
and TIMESTAMP WITH TIMEZONE
. Thus, let's say you have a table named person
with a column named birthdate
of type DATE
, you might currently have this kind of code:
try (
var preparedStatement = con.prepareStatement("SELECT birthdate FROM person";
var query = preparedStatement.executeQuery()) {
while (query.next()) {
java.sql.Date d = query.getDate(1);
doStuffWith(d);
}
}
That code is obsolete and dangerous. Instead, replace the offending line (that calls .getDate
) with:
java.time.LocalDate d = query.getObject(1, LocalDate.class):
The javadoc of getObject
states that whether the type you pass 'works' depends on the JDBC driver, but as mentioned, a JDBC5 compatible driver is required to support it.
You can use .setObject()
(when filling values for PreparedStatements, or INSERTing data) too, passing a LocalDate
instance.
This avoids all issues.
A direct answer to your question
You really should skip this section and just do it right. Find all calls to getTimestamp
, getDate
, setTimestamp
, setDate
, etc - and fix them properly.
Because the correct answer to your question is: Nevermind, you need to go one level deeper and stop using this stuff.
However, in the interests of satisfying curiosity:
Timestamp stamp = new Timestamp(123, 7, 1, 10, 0, 0, 0); //this > represents 2023-08-01 10:00:00 (UTC)
stamp.toInstant() //returns 2023-08-01 08:00:00
No, this is all very incorrect. Your understanding of dates and times is wrong.
A Timestamp cannot represent human-reckoning (which is years, months, days, and so on). It represents millis-since-epoch and the notion of referring to some millis-since-epoch values in terms of days, hours, minutes (and a UTC timezone) is a human invention that is subtly incorrect. That's not how it works under the hood. And when you lose sight of that, you get confused.
Instant is the same thing: It represents millis-since-epoch and not days, months, hours, etc. Hence, any interaction with these types in terms of human reckoning, be it that constructor you are invoking where you pass human-reckoning, or that toString()
output of Instant
, is merely for human convenience, is likely to confuse, and is the result of an internal conversion at point of contact - internally nothing is stored as human reckoning with these types!
Knowing that we can figure out why you are getting 'weird' results: The human reckoning constructor of java.sql.Timestamp
'helpfully' assumes you are writing your human reckoning in the platform default timezone (which, evidently, for your system, currently has +02:00 offset relative to UTC, i.e. most of mainland europe in summer for example).
Hence, it dutifully converts your human reckoning to that millis-since-epoch value that represents that exact moment in time when asking someone in Brussels 'hey, can you give me the date and time?' would result in the answer: "Sure mate! It's 2023, august 1st, and it's 10:00 in the morning exactly!" and stores that. It doesn't store timezones, and it doesn't store the notion of '10 o clock'. It just stores that big number: Your object has one field with value 1690876800000
and that is all it has (oversimplifying a bit, there are more fields, but they don't meaningfully affect this conclusion). It doesn't store the timezone and it doesn't store the human reckoning version (2023/08/01/10/00/00).
You then ask it to print itself, which causes it to convert 1690876800000 right back to human reckoning form. As you didn't provide a timezone when asking it to print itself, and it doesn't store one, the behaviour depends on what you're doing:
theInstanceOfJavaSqlTimestamp.toString()
produces the string value "2023-08-01 10:00:00.0", and it does this because its toString also 'helpfully' uses your platform default timezone which is still Europe/Amsterdam
. This might make you think it dutifully stored '10 o clock' - it didn't.
Instead, Instant is 'better' in that it tries to avoid the confusion and avoids creating the impression it is storing timestamp info:
theInstanceOfJavaSqlTimestamp.toInstant().toString()
The toInstant()
call just gives you an instant whose one field with epoch-millis still has the same 1690876800000 value stored inside. However, its toString()
impl chooses to render this value to your eyeballs by converting it on-the-fly to human reckoning just like java.sql.Timestamp
's toString(), but it uses UTC instead of 'platform local timezone'.
That doesn't change this simple fact: These 2 objects represent the exact same instant in time. It's just different takes on 'how should the toString()
method show useful information to a human debugger'.
What I want is: 2023-08-01 12:00:00 I was able to get the result with:
This is fundamentally wrong - Instant and Timestamp simply do not work that way. This question is non-sensical. Your Timestamp represents 1690876800000. That's all it represents. Given that we're talking epoch millis, the only way it is sensible to say: I want it to represent [human reckoning values here], is if you include a full timezone (Europe/Amsterdam
style, not CEST
style, which aren't sufficient to uniquely determine times and dates).
If you don't have a timezone or that isn't what you're trying to represent, then Timestamp
simply cannot be used AT ALL, and instead you need to use LocalDateTime
, which is the only type that can properly represent human reckoning without a timezone!
If you want a nice programmatic interface to construction java.sql.Timestamp
instances that represent what you want, use java.time
and convert to the broken (java.sql.Timestamp
, java.util.Date
, etc) types as late in the process as possible:
Instant nowUtc = Instant.now();
ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.ofInstant(nowUtc, europeZone);
This code can be written a lot more simply:
ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.now(europeZone);
Converting this to a Timestamp
does not make sense because j.s.Timestamp doesn't contain timezone info. If your database column does, then the only proper way to do it is to use .setObject(colIdxOrName, instanceOfOffsetDateTime)
. Having to use OffsetDateTime is painful, but it's a consequence of SQL also being confused and broken in regards to timezones (it thinks offsets are timezones. They aren't, and calling them timezones is very confusing, but, here we are). You can try calling .setObject()
with an instance of ZDT, but if that doesn't work, you'd have to call:
ZoneId europeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime berlinTime = ZonedDateTime.now(europeZone);
preparedStatement.setObject(berlinTime.toOffsetDateTime());
If you must have it in Timestamp form, note that timezones are right out and this code will subtly break on you later. Closest you can get:
Instant now = Instant.now();
// FORCIBLY SET YOUR SYSTEM TO BERLIN TIME!
// STUFF WILL BREAK IF SYSTEM TZ IS EVER CHANGED!
java.sql.Timestamp ts = java.sql.Timestamp.from(now);
j.s.Timestamp applies the platform local timezone whether you want it to or not.