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 aDateTime
, but in order to defer to those in SQL land, there's another subclassjava.sql.Date
, which represents a single day (though without a timezone associated with it).- There are no
TimeZone
s associated with aDate
, 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 aDate
object and a set of integer fields such as year, month, day, and hour.Class
GregorianCalendar
is the only subclass ofCalendar
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 Javalong
). 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 aDate
object initialized with the current date / time. The problem is that theDate
API methods are mostly flawed ... and deprecated.Calendar.getInstance()
gives you aCalendar
object initialized with the current date / time, using the defaultLocale
andTimeZone
. Other overloads allow you to use a specificLocale
and/orTimeZone
. 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()
andjava.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
Distinguishing Between Java Threads and Os Threads
Java Refuses to Start - Could Not Reserve Enough Space For Object Heap
Why Maven Uses Jdk 1.6 But My Java -Version Is 1.7
Eclipse/Tomcat: Deploy Doesn't Work Any More (Classnotfoundexception)
Rjava Load Error in Rstudio/R After "Upgrading" to Osx Yosemite
Selenium: How to Stop Geckodriver Process Impacting Pc Memory, Without Calling Driver.Quit()
Is Java "Pass-By-Reference" or "Pass-By-Value"
The Use of Multiple Jframes: Good or Bad Practice
Should I Avoid the Use of Set(Preferred|Maximum|Minimum)Size Methods in Java Swing
Providing White Space in a Swing Gui
What Is Pecs (Producer Extends Consumer Super)
How to Fix Java.Lang.Unsupportedclassversionerror: Unsupported Major.Minor Version
Connect Java to a MySQL Database
How to Round a Number to N Decimal Places in Java