What's Wrong With Java Date & Time API

What's wrong with Java Date & Time API?

Ah, the Java Date class. Perhaps one of the best examples of how not to do something in any language, anywhere. Where do I begin?

Reading the JavaDoc might lead one to think that the developers have actually got some good ideas. It goes on about the difference between UTC and GMT at length, despite the fact that the difference between the two is basically leap seconds (which happen pretty rarely).

However, the design decisions really lay to waste any thought of being a well designed API. Here are some of the favourite mistakes:

  • Despite being designed in the last decade of the millennium, it rates years as two digits since 1900. There are literally millions of workarounds doing 1900+ (or 1900-) in the Java world as a result of this banal decision.
  • Months are zero indexed, to cater for the spectacularly unusual case of having an array-of-months and not living with a thirteen element array, the first of which containing a null. As a result, we have 0..11 (and today being month 11 of the year 109). There are a similar number of ++ and -- on the months in order to convert to a string.
  • They're mutable. As a result, any time you want to give a date back (say, as an instance structure) you need to return a clone of that date instead of the date object itself (since otherwise, people can mutate your structure).
  • The Calendar, designed to 'fix' this, actually makes the same mistakes. They're still mutable.
  • Date represents a DateTime, but in order to defer to those in SQL land, there's another subclass java.sql.Date, which represents a single day (though without a timezone associated with it).
  • There are no TimeZones associated with a Date, and so ranges (such as a 'whole day') are often represented as a midnight-midnight (often in some arbitrary timezone)

Finally, it's worth noting that leap seconds generally correct themselves against a good system clock which is updated with ntp within an hour (see links above). The chance of a system being still up and running in the introduction of two leap seconds (every six months minimum, every few years practically) is pretty unlikely, especially considering the fact that you have to redeploy new versions of your code from time to time. Even using a dynamic language which regenerates classes or something like a WAR engine will pollute the class space and run out of permgen eventually.

Why is the Java date API (java.util.Date, .Calendar) such a mess?

Someone put it better than I could ever say it:

  • Class Date represents a specific instant in time, with millisecond
    precision. The design of this class is a very bad joke - a sobering
    example of how even good programmers screw up. Most of the methods in
    Date are now deprecated, replaced by methods in the classes below.
  • Class Calendar is an abstract class for converting between a Date
    object and a set of integer fields such as year, month, day, and hour.

  • Class GregorianCalendar is the only subclass of Calendar in the JDK.
    It does the Date-to-fields conversions for the calendar system in
    common use. Sun licensed this overengineered junk from Taligent - a
    sobering example of how average programmers screw up.

from Java Programmers FAQ, version from 07.X.1998, by Peter van der Linden - this part was removed from later versions though.

As for mutability, a lot of the early JDK classes suffer from it (Point, Rectangle, Dimension, ...). Misdirected optimizations, I've heard some say.

The idea is that you want to be able to reuse objects (o.getPosition().x += 5) rather than creating copies (o.setPosition(o.getPosition().add(5, 0))) as you have to do with immutables. This may even have been a good idea with the early VMs, while it's most likely isn't with modern VMs.

Inconsistency in handling date and time in Java

This is a weirdness caused by Finland time change, see Clock Changes in Helsinki, Finland (Helsingfors) in 1921:

May 1, 1921 - Time Zone Change (HMT → EET)

When local standard time was about to reach
Sunday, May 1, 1921, 12:00:00 midnight clocks were turned forward 0:20:11 hours to
Sunday, May 1, 1921, 12:20:11 am local standard time instead

Those 20 minutes 11 seconds seem to be what you're observing.

As Jim Garrison said in his answer, LocalDateTime is correctly handling that, while Calendar is not.

In reality, it seems that the old TimeZone is getting the offset wrong, while the new ZoneId is getting it right, as can be seen in the following test code:

public static void main(String[] args) {
compare(1800, 1, 1, 0, 0, 0);
compare(1899,12,31, 23,59,59);
compare(1900, 1, 1, 0, 0, 0);
compare(1900,12,30, 23, 0, 0);
compare(1921, 4,30, 0, 0, 0);
compare(1921, 5, 1, 0, 0, 0);
compare(1921, 5, 2, 0, 0, 0);
}
private static void compare(int year, int month, int day, int hour, int minute, int second) {
Calendar calendar = new GregorianCalendar();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone("Europe/Helsinki"));
calendar.set(year, month-1, day, hour, minute, second);
Date date = calendar.getTime();

ZonedDateTime zdt = ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.of("Europe/Helsinki"));

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z XXX");
sdf.setTimeZone(TimeZone.getTimeZone("Europe/Helsinki"));
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss z XXX");

System.out.printf("%04d-%02d-%02d %02d:%02d:%02d %s = %d %s = %d %d%n",
year, month, day, hour, minute, second,
sdf.format(date), date.getTime(),
dtf.format(zdt), zdt.toInstant().toEpochMilli(),
date.getTime() - zdt.toInstant().toEpochMilli());
}

Output

1800-01-01 00:00:00   1800-01-01 00:00:00 EET +02:00 = -5364669600000   1800-01-01 00:00:00 EET +01:39 = -5364668389000   -1211000
1899-12-31 23:59:59 1899-12-31 23:59:59 EET +02:00 = -2208996001000 1899-12-31 23:59:59 EET +01:39 = -2208994790000 -1211000
1900-01-01 00:00:00 1900-01-01 00:00:00 EET +02:00 = -2208996000000 1900-01-01 00:00:00 EET +01:39 = -2208994789000 -1211000
1900-12-30 23:00:00 1900-12-30 23:00:00 EET +01:39 = -2177548789000 1900-12-30 23:00:00 EET +01:39 = -2177548789000 0
1921-04-30 00:00:00 1921-04-30 00:00:00 EET +01:39 = -1536025189000 1921-04-30 00:00:00 EET +01:39 = -1536025189000 0
1921-05-01 00:00:00 1921-05-01 00:20:11 EET +02:00 = -1535938789000 1921-05-01 00:20:11 EET +02:00 = -1535938789000 0
1921-05-02 00:00:00 1921-05-02 00:00:00 EET +02:00 = -1535853600000 1921-05-02 00:00:00 EET +02:00 = -1535853600000 0

Simple date format giving wrong time

In your example, you are using time 1618274313 and you are assuming that it is in milliseconds. However, when I entered the same time on https://www.epochconverter.com/, I got below results:

Please notice the site mentions: Assuming that this timestamp is in seconds.


Now if we use that number multiplied by 1000 (1618274313000) as the input so that the site considers it in milliseconds, we get below results:

Please notice the site now mentions: Assuming that this timestamp is in milliseconds.


Now, when you will use 1618274313000 (correct time in milliseconds) in Java with SimpleDateFormat, you should get your expected result (instead of 23:01:14):

SimpleDateFormat sdf=new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
System.out.println(sdf.format(new Date(1618274313000)));

Good Reason to use java.util.Date in an API

The Date class is part of the API and as such a valid option if it fits your purpose. There are many deprecated methods in it that were replaced by the Calendar class, so make sure you avoid using those methods.

The answer will depend on what is what you need to accomplish. If you just need to sort by the date, a long will be more than enough. A Date value would add some readability to it, but not more functionality. Using Date will not be detrimental either, so the readability factor should be considered.

If the field will be private, you can actually store it as a long and have a getter and a setter that use Date :

private long mDOB;

public Date getDOB () { return new Date(mDOB);}
public void setDOB (Date dob) { mDOB = dob.getTime(); }

Good way for Java Date comparison without time

The date-time API of java.util and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern date-time API.

  • For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7.
  • If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Learn about the modern date-time API from Trail: Date Time.

LocalDate uses JVM's timezone by default

Whenever timezone is involved, make sure to specify the same while creating an instance of LocalDate. A LocalDate uses JVM's timezone by default and you should never compare a LocalDate from one timezone to that of another without converting both of them in the same timezone (the recommended one is UTC). Same is the case with LocalDateTime. Instead of using LocalDate, you should do all processing with objects which have both date and time (e.g. LocalDateTime) and if required you can derive the LocalDate from them.

Also, the java.util.Date object simply represents the number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT (or UTC). When you print an object of java.util.Date, its toString method returns the date-time in the JVM's timezone, calculated from this milliseconds value.

Therefore, if you are deriving expiryDate from a java.util.Date object, it is essentially date-time in UTC.

You can convert now-in-PST and expiryDate into java.time.Instant and compare them. A java.time.Instant is an instantaneous point on the UTC time-line.

Demo using the modern date-time API:

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;

public class Main {
public static void main(String[] args) {
LocalDateTime nowInPST = LocalDateTime.now(ZoneId.of("America/Los_Angeles"));
System.out.println(nowInPST);

// Convert it to date in UTC
Instant nowInPSTConvertedToInstant = nowInPST.atZone(ZoneId.of("America/Los_Angeles"))
.withZoneSameInstant(ZoneId.of("Etc/UTC"))
.toInstant();

// Some java.util.Date
Calendar calendar = Calendar.getInstance();
calendar.set(2020, 0, 10, 10, 10, 10);
Date date = calendar.getTime();
Instant expiry = date.toInstant();

System.out.println(nowInPSTConvertedToInstant.isBefore(expiry));
}
}

Output:

2021-01-17T10:58:38.490041
false

Note: Check the following notice at the Home Page of Joda-Time

Joda-Time is the de facto standard date and time library for Java
prior to Java SE 8. Users are now asked to migrate to java.time
(JSR-310).

Simplify your expression

The following statement

boolean notExpired = expiryDate.isEqual(now) || expiryDate.isAfter(now);

can be simplified as

boolean notExpired = !expiryDate.isBefore(now);

How to get the current date/time in Java

It depends on what form of date / time you want:

  • If you want the date / time as a single numeric value, then System.currentTimeMillis() gives you that, expressed as the number of milliseconds after the UNIX epoch (as a Java long). This value is a delta from a UTC time-point, and is independent of the local time-zone1.

  • If you want the date / time in a form that allows you to access the components (year, month, etc) numerically, you could use one of the following:

    • new Date() gives you a Date object initialized with the current date / time. The problem is that the Date API methods are mostly flawed ... and deprecated.

    • Calendar.getInstance() gives you a Calendar object initialized with the current date / time, using the default Locale and TimeZone. Other overloads allow you to use a specific Locale and/or TimeZone. Calendar works ... but the APIs are still cumbersome.

    • new org.joda.time.DateTime() gives you a Joda-time object initialized with the current date / time, using the default time zone and chronology. There are lots of other Joda alternatives ... too many to describe here. (But note that some people report that Joda time has performance issues.; e.g. https://stackoverflow.com/questions/6280829.)

    • in Java 8, calling java.time.LocalDateTime.now() and java.time.ZonedDateTime.now() will give you representations2 for the current date / time.

Prior to Java 8, most people who know about these things recommended Joda-time as having (by far) the best Java APIs for doing things involving time point and duration calculations.

With Java 8 and later, the standard java.time package is recommended. Joda time is now considered "obsolete", and the Joda maintainers are recommending that people migrate.3.


1 - System.currentTimeMillis() gives the "system" time. While it is normal practice for the system clock to be set to (nominal) UTC, there will be a difference (a delta) between the local UTC clock and true UTC. The size of the delta depends on how well (and how often) the system's clock is synced with UTC.

2 - Note that LocalDateTime doesn't include a time zone. As the javadoc says: "It cannot represent an instant on the time-line without additional information such as an offset or time-zone."

3 - Note: your Java 8 code won't break if you don't migrate, but the Joda codebase may eventually stop getting bug fixes and other patches. As of 2020-02, an official "end of life" for Joda has not been announced, and the Joda APIs have not been marked as Deprecated.



Related Topics



Leave a reply



Submit