Guide to time and date in Java
java.time
from Java 8. I want to cover the absolute minimum that every junior Java developer should be aware of.When did an event happen?
Philosophy and quantum physics aside, we may treat time as a one-dimensional metric, a real number value. This value keeps growing when time passes by. If one event appeared after another, we assign greater time to that event. Two events happening simultaneously have the same time value. For practical reasons in computer systems we store time in discrete integer, mainly because computer clocks tick discretely. Therefore we can store time as an integer value. By convention we assign time = 0 to January 1st, 1970 but in Java we increment this value every millisecond, not second like in UNIX time. Historically using 32-bit signed integer in UNIX time will cause year 2038 problem. Thus Java stores time in 64-bit integer, which is sufficient even if you increment it thousand times more often. That being said the simplest, yet valid way of storing time in Java is...long
primitive:long timestamp = System.currentTimeMillis();The problem with
long
is that it's so prevalent that using it for storing time undermines the type system. It may be an ID, may be hash value, can be anything. Also long
doesn't have any meaningful methods related to time domain. The very first approach to wrap long
in more meaningful object was java.util.Date
known since Java 1.0:Date now = new Date();
Date
class however has numerous flaws:- It does not represent... date. Seriously, officially date is "[...] the day of the month or year as specified by a number [...]" [1] whereas in Java it represents point in time without any specific calendar (day/month/year).
- Its
toString()
is misleading, displaying calendar date and time in system timezone. Not only it misled thousands of developers to think thatDate
has a timezone attached. Moreover it shows time, but date should only represent day, not hour. - It has 20+ deprecated methods, including
getYear()
,parse(String)
and many constructors. These methods are deprecated for a reason, because they lead you to believeDate
represents, you know, date. java.sql.Date
extendsjava.util.Date
and is actually much more accurate because it indeed represents calendar date (DATE
in SQL). However this narrows the functionality of base classDate
, thus violating Liskov substitution principle. Don't believe me?java.util.Date.toInstant()
works as expected butjava.sql.Date.toInstant()
fails unconditionally withUnsupportedOperationException
...- Worst of them all,
Date
is mutable.
Date
. Simple, huh?Date addOneMinute(Date in) {Looks fine, right? All test cases pass because who on earth would ever validate that input parameters are intact after testing code?
in.setTime(in.getTime() + 1_000 * 60);
return in;
}
Date now = new Date();The output may look as follows:
System.out.println(now);
System.out.println(addOneMinute(now));
System.out.println(now);
Tue Jul 26 22:59:22 CEST 2016Did you notice that
Tue Jul 26 23:00:22 CEST 2016
Tue Jul 26 23:00:22 CEST 2016
now
value was actually changed after adding one minute? When you have a function that takes Date
and returns Date
you would never expect it to modify its parameters! It's like having a function taking x
and y
numbers and retuning sum of them. If you discover that x
was somehow modified during the course of addition, all your assumptions are ruined. By the way that is the reason why java.lang.Integer
is immutable. Or String
. Or BigDecimal
.This is not a contrived example. Imagine a
ScheduledTask
class with a single method:class ScheduledTask {What happens if I say:
Date getNextRunTime();
}
ScheduledTask task = //...Does changing the returned
task.getNextRunTime().setTime(new Date());
Date
have effect on next run time? Or maybe ScheduledTask
returns a copy of its internal state that you are free to modify? Maybe we will leave ScheduledTask
in some inconsistent state? If Date
was immutable no such problem would ever arise.Interestingly, every Java developer will become furious if you confuse Java with JavaScript. But guess what,
Date
in JavaScript has the exact same flaws as java.util.Date
and seems like a bad example of copy-paste. Date
in JavaScript is mutable, has misleading toString()
and no support for time zones whatsoever.A great alternative to
Date
is java.time.Instant
. It does precisely what it claims: stores an instant in time. Instant
does not have date or calendar related methods, its toString()
uses familiar ISO format in UTC time zone (more on that later) and most importantly: it's immutable. If you want to remember when a particular event happened, Instant
is the best you can get in plain Java:Instant now = Instant.now();Notice that
Instant later = now.plusSeconds(60);
Instant
does not have plusMinutes()
, plusHours()
and so on. Minutes, hours and days are concepts related to calendar systems, whereas Instant
is geographically and culturally agnostic.Human readable calendars with ZonedDateTime
Sometimes you do need a human representation of an instant in time. This includes month, day of week, current hour and so on. But here is a major complication: date and time varies across countries and regions. Instant
is simple and universal, but not very useful for human beings, it's just a number. If you have business logic related to calendar, like:- ...must happen during office hours...
- ...up to one day...
- ...two business days...
- ...valid for up to one year...
- ...
java.time.ZonedDateTime
is the best alternative to absolutely awful java.util.Calendar
. As a matter of fact java.util.Date
and Calendar
are so broken by design that they are considered to be deprecated entirely in JDK 9. You can create ZonedDateTime
from Instant
only by providing a time zone. Otherwise default system time zone is used which you have no control over. Converting Instant
to ZonedDateTime
in any way without providing explicit ZoneId
is probably a bug:Instant now = Instant.now();The output is as follows:
System.out.println(now);
ZonedDateTime dateTime = ZonedDateTime.ofInstant(
now,
ZoneId.of("Europe/Warsaw")
);
System.out.println(dateTime);
2016-08-05T07:00:44.057ZNotice that
2016-08-05T09:00:44.057+02:00[Europe/Warsaw]
Instant
(for convenience) displays date formatted in UTC whereas ZonedDateTime
uses supplied ZoneId
(+2 hours during summer, more on that later).Calendar misconceptions
There are many misconceptions and myths related to time and calendars. For example some people believe that the time difference between two locations is always constant. There are at least two reasons for that not being true. First the daylight saving time, aka summer time:LocalDate localDate = LocalDate.of(2016, Month.AUGUST, 5);The output reveals that the difference between Warsaw and Sydney is exactly 8 hours:
LocalTime localTime = LocalTime.of(10, 21);
LocalDateTime local = LocalDateTime.of(localDate, localTime);
ZonedDateTime warsaw = ZonedDateTime.of(local, ZoneId.of("Europe/Warsaw"));
ZonedDateTime sydney = warsaw.withZoneSameInstant(ZoneId.of("Australia/Sydney"));
System.out.println(warsaw);
System.out.println(sydney);
2016-08-05T10:21+02:00[Europe/Warsaw]Or is it? Change August to February and the difference becomes 10 hours:
2016-08-05T18:21+10:00[Australia/Sydney]
2016-02-05T10:21+01:00[Europe/Warsaw]That's because Warsaw does not observe DST in February (it's winter) whereas in Sydney it's summer so they use DST (+1 hour). In August it's vice-versa. To make things even more complex, the time to switch to DST varies and it's always during night of local time so there must be a moment where one country already switched but not the other, for example in October:
2016-02-05T20:21+11:00[Australia/Sydney]
2016-10-05T10:21+02:00[Europe/Warsaw]9 hours of difference. Another reason why time offset differs is political:
2016-10-05T19:21+11:00[Australia/Sydney]
LocalDate localDate = LocalDate.of(2014, Month.FEBRUARY, 5);The time difference between Warsaw and Moscow on February 5th, 2014 was 3 hours:
LocalTime localTime = LocalTime.of(10, 21);
LocalDateTime local = LocalDateTime.of(localDate, localTime);
ZonedDateTime warsaw = ZonedDateTime.of(local, ZoneId.of("Europe/Warsaw"));
ZonedDateTime moscow = warsaw.withZoneSameInstant(ZoneId.of("Europe/Moscow"));
System.out.println(warsaw);
System.out.println(moscow);
2014-02-05T10:21+01:00[Europe/Warsaw]But the difference on the exact same day year later is 2 hours:
2014-02-05T13:21+04:00[Europe/Moscow]
2015-02-05T10:21+01:00[Europe/Warsaw]That's because Russia is changing their DST policy and time zone like crazy.
2015-02-05T12:21+03:00[Europe/Moscow]
Another common misconception about dates is that a day is 24 hours. This is again related to daylight saving time:
LocalDate localDate = LocalDate.of(2017, Month.MARCH, 26);What do you know, the difference between 1 AM on March 26th and 27th, 2017 is... 23 hours (
LocalTime localTime = LocalTime.of(1, 0);
ZonedDateTime warsaw = ZonedDateTime.of(localDate, localTime, ZoneId.of("Europe/Warsaw"));
ZonedDateTime oneDayLater = warsaw.plusDays(1);
Duration duration = Duration.between(warsaw, oneDayLater);
System.out.println(duration);
PT23H
). But if you change the time zone to Australia/Sydney
you'll get familiar 24 hours because nothing special happens that day in Sydney. That special day in Sydney happens to be 2nd of April, 2017:LocalDate localDate = LocalDate.of(2017, Month.APRIL, 2);Which results in one day being equal to... 25 hours. But not in Brisbane (
LocalTime localTime = LocalTime.of(1, 0);
ZonedDateTime warsaw = ZonedDateTime.of(localDate, localTime, ZoneId.of("Australia/Sydney"));
"Australia/Brisbane"
), thousand km north to Sydney, which does not observe DST. Why is all of this important? When you make an agreement with your client that something is suppose to take one day vs. 24 hours this may actually make a huge difference at certain day. You must be precise, otherwise your system will become inconsistent twice a year. And don't get me started on leap second.The lesson to learn here is that every time you enter calendar domain you must think about time zones. There are convenience methods that use default system time zone but in cloud environments you may not have control over that. The same applies to default character encoding, but that's a different story.
Storing and transmitting time
By default you should store and send time either as timestamp (long
value) or as ISO 8601 which is basically what Instant.toString()
does as per the documentation. Prefer long
value as it is more compact, unless you need more readable format in some text encoding like JSON. Also long
is timezone-agnostic so you are not pretending that the timezone you send/store has any meaning. This applies both to transmitting time and storing it in database.There are cases where you may want to send full calendar information, including timezone. For example when you build a chatting application you might want to tell the client what was the local time when the message was sent if your friend lives in a different timezone. Otherwise you know it was sent at 10 AM your time, but what was the time in your friend's location? Another example is flight ticket booking website. You want to tell your clients when flight departs and arrives in local time and it's only the server that knows the exact timezone at departure and destination.
Local time and date
Sometimes you want express date or time without any specific time zone. For example my birthday is://1985-12-25I will celebrate my birthday that day no matter where I am. This means party will start at approximately:
LocalDate.of(1985, Month.DECEMBER, 25)
//20:00Irrespective to time zone. I can even say that my birthday party this year will be precisely at:
LocalTime.of(20, 0, 0)
//2016-12-25T20:00But as long as I don't provide you a location, you don't know what is the time zone I live in, thus what is the actual start time. It's impossible (or very foolish) to convert from
LocalDateTime party = LocalDateTime.of(
LocalDate.of(2016, Month.DECEMBER, 25),
LocalTime.of(20, 0, 0)
);
LocalDateTime
to Instant
or ZonedDateTime
(which both point to a precise moment in time) without giving a time zone. So local times are useful, but they don't really represent any moment in time.Testing
I just scratched the surface of pitfalls and issues one might have with time an date. For example we didn't cover leap years which can become a serious source of bugs. I find property-based testing extremely useful when testing dates:import spock.lang.SpecificationThis test makes sure adding and subtracting one month to any date in 2016 gives back the same date. Pretty straightforward, right? This test fails for a number of days:
import spock.lang.Unroll
import java.time.*
class PlusMinusMonthSpec extends Specification {
static final LocalDate START_DATE =
LocalDate.of(2016, Month.JANUARY, 1)
@Unroll
def '#date +/- 1 month gives back the same date'() {
expect:
date == date.plusMonths(1).minusMonths(1)
where:
date << (0..365).collect {
day -> START_DATE.plusDays(day)
}
}
}
date == date.plusMonths(1).minusMonths(1)Leap years cause all sorts of issues and break the laws of math. Another similar example is adding two months to a date that is not always equal to adding one month two times.
| | | | |
| | | 2016-02-29 2016-01-29
| | 2016-01-30
| false
2016-01-30
date == date.plusMonths(1).minusMonths(1)
| | | | |
| | | 2016-02-29 2016-01-29
| | 2016-01-31
| false
2016-01-31
date == date.plusMonths(1).minusMonths(1)
| | | | |
| | | 2016-04-30 2016-03-30
| | 2016-03-31
| false
2016-03-31
...
Summary
Once again we barely scratched the surface. If there is just one thing I want you to learn from this article: mind the time zone!Tags: java.time