Most tools/frameworks/databases give programmers poor choices for date types and offer very misleading behaviors. Even experienced programmers get confused and choose wrong types, what's worse - they take the misleading behaviors for granted. The biggest evil is unnecessary timezone conversions.
A timestamp is a moment on the time-line. It is always in UTC, because it doesn't need to be convenient for the end user, and because it needs to be unambiguous. In the Java world a timestamp can be represented as (ignoring milliseconds/nanoseconds precisions for simplicity):
long - the number of milliseconds passed from the Unix epoch (1970-01-01T00:00:00 in UTC). It's always in UTC by definition.java.time.Instant - the modern (java8+) class. In simple words: it's a wrapper around the long (remember, we are ignoring nanoseconds for simplicity).java.util.Date - the legacy (pre-java8) class. Similar to java.time.Instant from the paradigm perspective.java.sql.Timestamp - also similar in paradigm. Based on java.util.Date (extends it) and should be considered a member of the legacy date "framework" (java.util.Date, java.util.Calendar, etc), but unfortunately it's still a thing, because it's still used in JDBC drivers (e.g. MySQL)."1970-01-01T00:00:00Z" - also a very good representation that is both human-readable and unambiguous ("Z" means UTC and clearly states it's not a "local date").
Important! If you deal with a number of seconds/milliseconds/etc. then it must be in UTC, otherwise it would be too confusing.
For example, java.lang.System.currentTimeMillis() in Java and date +%s command in Unix return milliseconds/seconds in UTC. Please drop me a comment if you know a tool or a framework that would deal with non-UTC numbers.
The representations above can be thought of as timezone-agnostic. You may disagree, because they do have a timezone, the UTC timezone, but UTC is a part of the definition, not a part of the data. The practical takeaway is that all of these invocations produce the same results on computers with different timezones:
System.currentTimeMillis()
new Date()
new Timestamp(System.currentTimeMillis())
Instant.now()
Instant.now().toString()
and we can convert between the types without specifying a timezone (or even without implicitly taking computer's timezone).
java.time.LocalDate/java.time.LocalDateTime - the modern (java8+) classes."1970-01-01 00:00:00" - note that there is no "Z" or "UTC".
So, "1970-01-01T00:00:00Z" is a timestamp, "1970-01-01T00:00:00" is a local date, very different notions.
java.sql.Timestamp.toString() method. new java.sql.Timestamp(0L).toString() returns "1970-01-01 03:00:00.0" for the Moscow timezone and here is what actually happens:
toString() contract is violated, because java.sql.Timestamp.toString() represents a different paradigm.
In contrast, the modern equivalent java.time.Instant.ofEpochMilli(0).toString() would return "1970-01-01T00:00:00Z"
irrespective of computer's timezone. Even if we want to switch to the local date paradigm - it will force us to provide an explicit timezone, e.g.
Instant.ofEpochMilli(0).atZone(zoneId).toLocalDateTime() where zoneId can be, for example, ZoneId.systemDefault() or ZoneOffset.UTC.
Consider MySQL's TIMESTAMP behavior: MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval.
java.time.Instant to java.time.LocalDateTime I would have to do LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC), but sometimes the system
timezone is taken implicitly, e.g. LocalDateTime.now() (consider using java.time.LocalDateTime.now(java.time.ZoneId) instead).
LocalDate and LocalDateTime classes. Classes like YearMonth, Year, etc. are similar to those in nature
(you can convert without timezones, e.g.
Year.of(LocalDate.now().getYear()). So Year, for example, should be called LocalYear for consistency.
Instant equivalent to Year, YearMonth, etc. (so I can convert back and forth without any timezones), but java8 dates don't have it.
So I'm left with 2 workarounds. The first is to truncate Instant (truncatedTo method), but there is no check if the non-relevant time fields (e.g. if year then month, day,
hours, etc. all should be zeros) are always zeros. The second is to convert to Year, YearMonth, etc. using UTC which is not ideal, because an effort should be made to keep it
consistent everywhere.