A Brief History of Time in Java
JDK 1.0 introduced the Date API as part of java.util.date
. The date API was always weird.
For example, let's look at this particular Date API:-
Date date = new Date(2014 /* year */, 1 /* month */, 1 /* day */ );
By looking at the API, you might think since the Date
constructor takes year
, month
and day
that this call represents 2014-Jan-1
. But this represents 3914-Feb-1
! The rules for this
constructor are as follows:
- The integer years parameter is an offset from 1900. So, if you wanted 2014, you need to give 2014-1900, which is 114.
- The month is 0 indexed. So, months are represented using 0-11.
- The day parameter works as expected.
Everyone recognized this API was bad. In fact, it was deprecated and replaced just the next year, as part of JDK 1.1,
with the Calendar
API!
The Calendar class fixed many of these issues. It gave constants to all the Month's and Days of the week. It handled
timezones and locales. So, to create a new Calendar
object with 2014-Jan-1
, you can now do
Calendar cal = Calendar.getInstance();
cal.set(2014, Calendar.JANUARY, 1);
But it still had many issues:
-
The name implied that you get a Calendar, but you get an instant in Date and Time. This may be a minor nit for some, but the name just does not make sense.
-
Even though constants where introduced, the months are still 0 indexed, and it is extremely easy to give the wrong index when constructing the Calendar instance.
-
Common calculations like the difference between days cannot be easily done. You must convert the values into milliseconds calculate the difference and then convert the values back into days. And this has its own pitfalls as the user has to use the DST aware time zones. A complete function will look something like this:
// FROM https://stackoverflow.com/a/31800947/4468788
import java.util.concurrent.TimeUnit;
/**
* Compute the number of calendar days between two Calendar objects.
* The desired value is the number of days of the month between the
* two Calendars, not the number of milliseconds' worth of days.
* @param startCal The earlier calendar
* @param endCal The later calendar
* @return the number of calendar days of the month between startCal and endCal
*/
public static long calendarDaysBetween(Calendar startCal, Calendar endCal) {
// Create copies so we don't update the original calendars.
Calendar start = Calendar.getInstance();
start.setTimeZone(startCal.getTimeZone());
start.setTimeInMillis(startCal.getTimeInMillis());
Calendar end = Calendar.getInstance();
end.setTimeZone(endCal.getTimeZone());
end.setTimeInMillis(endCal.getTimeInMillis());
// Set the copies to be at midnight, but keep the day information.
start.set(Calendar.HOUR_OF_DAY, 0);
start.set(Calendar.MINUTE, 0);
start.set(Calendar.SECOND, 0);
start.set(Calendar.MILLISECOND, 0);
end.set(Calendar.HOUR_OF_DAY, 0);
end.set(Calendar.MINUTE, 0);
end.set(Calendar.SECOND, 0);
end.set(Calendar.MILLISECOND, 0);
// At this point, each calendar is set to midnight on
// their respective days. Now use TimeUnit.MILLISECONDS to
// compute the number of full days between the two of them.
return TimeUnit.MILLISECONDS.toDays(
Math.abs(end.getTimeInMillis() - start.getTimeInMillis()));
}
-
The calendar class is mutable, which means these classes are not thread safe.
-
java.text.DateFormat
was introduced for parsing date strings, but it is not thread safe. For example:
// FROM https://java-8-tips.readthedocs.io/en/stable/datetime.html
SimpleDateFormat sdf = new SimpleDateFormat("ddMMyyyy");
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
es.submit(() -> {
try {
System.out.println(sdf.parse("15081947"));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
es.shutdown();
Output:
-------
Fri Aug 15 00:00:00 IST 1947
Mon Aug 11 00:00:00 IST 1947
Fri Aug 15 00:00:00 IST 1947
Fri Aug 15 00:00:00 IST 1947
So 7 years after the release of JDK 1.1, in Java 8 new java.time
package was introduced to
replace Date
and Calendar
.
Date is Dead, Long Live DateTime!
The ThreeTen project was created in 2014 to introduce a new Date and Time API
in Java. This new project was championed by the author of Joda-Time
, a popular DateTime library that was used
prior to Java 8. This project introduced the java.time
package into Java 8.
This provided:-
java.time
- The main package to work with dates, times and instants.java.time.chrono
- API to work with other calendar systems like the Japanese calendar.java.time.format
- API to print and parse date and times.java.time.temporal
- Gives access to date and time fileds (like Hours, Days etc) and date time adjustors (firstDayOfMonth()
).java.time.zone
- Support for time-zones and their rules.
As a developer, you would normally interact with the java.time
package.
Clock
The Clock
class provides access to the current instant.
You can picture it as a normal wall clock, whose clock hands are always ticking. All the other classes in the package provide
an instant of this date. You can imagine LocalDateTime
as you are clicking a picture of that clock. The value of the time at that
instant is stored in the class.
This class is particularly useful for testing. You can create a clock this is fixed to a particular date (think of it as a broken clock).
With this clock, you can always get the same Date using the DateTime
classes.
You can find the example in the tips section.
Instant
The Instant
class is an instantaneous point on the time-line.
You can imagine it as the number of epoch seconds that have passed when the picture of the clock was taken.
LocalDate / LocalTime / LocalDateTime
The LocalDate
/
LocalTime
/
LocalDateTime
classes provide access to date time
without time zone information.
This class is useful for storing a description of date, rather than the date itself. For example, you could do something like this:
final LocalDateTime firstDayOf2014 = LocalDateTime.of(2014, Month.JANUARY, 1, 0, 0);
The above describes a date that is the first day of 2014. But to know whether the time right now is greater than that date, we would need time zone information. Another example is birthdays. They always occur on the same date, but we would only want to show a "Happy Birthday!" message if that is the date in the user's time zone.
ZonedDateTime
The ZonedDateTime
class provides the date and time
with a time zone. It is aware of things like Daylight Saving Time (DST) and other anomalies in time zone rules like that. So,
if you want to calculate tomorrow's date time in US Central Time, you should use this class.
OffsetTime / OffsetDateTime
The OffsetTime
and
OffsetDateTime
classes provide the instant of time,
as well as the offset from UTC. This class is useful when communicating over the network or to a database. You can (and should)
convert this to ZonedDateTime
if you are doing any time zone sensitive calculations.
Period / Duration
The Period
class represents a date-based amount of time.
If you want to add '1 year, 5 months and 3 days' to a date, you should make use of the Period
API.
The Duration
class represents quantity of time in seconds
and nanoseconds. You can use this to add '500 seconds' to a time for example.
Tips and Tricks
Usage with JPA / Hibernate
When working with JPA / Hibernate, prefer the Local
based classes if you need date time without time zone
and Offset
based classes if you need date time with time zone.
You can also use ZonedDateTime
and Duration
/ Instant
based classes, but this works only with Hibernate and can be
dangerous. For a full explanation go here.
Testing
Be incredibly careful when using the .now()
method as this can affect testability. For example
public class MyAwesomeDateUtilities {
private MyAwesomeDateUtilities() {
}
public static LocalDateTime get10DaysBeforeNow() {
return LocalDateTime.now().minusDays(10);
}
}
To write a test for this, you would have to do something like
public class MyAwesomeDateUtilitiesTest {
@Test
public void testGetLocalDateTimeFrom10DaysBefore() {
LocalDateTime nowMinus10 = LocalDateTime.now().minusDays(10);
Assert.assertTrue(nowMinus10.equals(MyAwesomeDateUtilities.get10DaysBeforeNow()));
}
}
This is a awfully bad test case. Why? The test code and the actual implementation is the same! If something were wrong with the implementation in the first place it would never have been caught.
Instead, you should convert your class to something along the lines of this:
public class MyAwesomeDateUtilities {
private Clock clock;
public MyAwesomeDateUtilities(Clock clock) {
this.clock = clock;
}
public LocalDateTime get10DaysBeforeNow() {
return LocalDateTime.now(clock).minusDays(10);
}
}
Now you can test like so
public class MyAwesomeDateUtilitiesTest {
@Test
public void testGetLocalDateTimeFrom10DaysBefore() {
final Clock clock = Clock.fixed(Instant.parse("2014-12-22T10:15:30.00Z"), ZoneId.of("UTC"));
final MyAwesomeDateUtilities util = new MyAwesomeDateUtilities(clock);
LocalDateTime nowMinus10 = LocalDateTime.parse("2014-12-12T10:15:30.00Z");
Assert.assertTrue(nowMinus10.equals(MyAwesomeDateUtilities.get10DaysBeforeNow()));
}
}
Much better!
Conclusion
The issue that I see is that unfortunately java.util.Date
is what shows up in autocomplete when you start typing out Date
in
Java IDE's. This leads to Date
being used even though it should not be. The key takeaway from this post is avoid Date
and
Calendar
APIs religiously. They lead to a class of bugs that are not obvious at first glance.