Date and Time handling is a basic feature in any application we built as Software Engineers, and in its nature, it has some complexity.
In this post, you will see how Java handles date and time, from Java 1.0 to Java 8, focusing on three main APIs:
- java.util.*: Date.
- java.util.*: Calendar.
- java.time.*: LocalDateTime and ZonedDateTime.
We are going to solve some questions regarding those APIs and conclude which is the best API to use for your use cases:
- How can I get the current date and time?
- How can I create a fixed date and time?
- How can I operate over date and time?
- How can I format from date to string and the other way around?
- How can I handle time zones?
- How can I mock date and time for testing?
Let’s get into it.
TRY IT YOURSELF: You can find the source code of this post here.
If you like this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.
java.util.Date
Date is a basic unit of time in Java. This class exists from the Java 1.0 version and we are still using it. However, its design brought some challenges when using it, for instance:
- Handling time zones are not possible.
- Operating over a date object is not possible.
- Date objects are mutable.
Date class has a lot of deprecated methods and constructors, however, as it exists from the first Java version, we still use it as backward compatibility.
Let’s see those challenges in detail solving the following questions.
How can I get the current date and time?
Let’s see how we get the current date using java.util.Date:
@Test public void testNow() { Date now = new Date(); System.out.println(now); assertNotNull(now); }
To get the current date, we create a new instance of Date class. The Date.toString()
method prints the following:
Tue Oct 13 16:05:40 COT 2020
This is the default format of Date. In following sections you will see how to customize this format. Something important to notice, the time zone, COT, this is the operative system time zone.
How can I define a fixed date and time?
Let’s see how we create a fixed date and time using java.util.Date:
@Test public void testFixedDate() { Date now = new Date(2020, 0, 5, 8, 30, 0); System.out.println(now); assertEquals(now.getYear(), 2020); }
We use a different constructor, which receives a year, month, day, hour, minute and second.
NOTE: The month starts on 0, not 1, however, the other parameters start on 1.
You can access the Date information using Date.getXX()
methods, for instance, Date.getYear()
, Date.getMonth()
, etc. However, they are deprecated.
Besides, this constructor is also deprecated, so, we shouldn’t use it as you can see in the output of its Date.toString()
:
Mon Jan 05 08:30:00 COT 3920
Year 3920? seems wrong.
How can I operate over date and time?
Well, there is not much you can do with a Date object. You can compare two dates and check which is after or before each other:
@Test public void testAfterBeforeDate() throws InterruptedException { Date now = new Date(); Thread.sleep(1000); Date nowAfter = new Date(); assertTrue(now.before(nowAfter)); assertTrue(nowAfter.after(now)); }
The missing ways to operate over a Date object is a downside of its API.
How can I format from date to string and the other way around?
Let’s see how to transform from Date to String:
@Test public void testDateToString_rightFormat() { Date now = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); String dateFormat = simpleDateFormat.format(now); System.out.println(dateFormat); assertNotNull(dateFormat); }
SimpleDateFormat class helps to define a date pattern and create a String from a Date. The following is the output of the transformation:
2020-10-13T16:38:23.794-0500
Now, let’s see how to transform from String to Date:
@Test public void testStringToDate() throws ParseException { SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); Date date = simpleDateFormat .parse("2020-01-05T08:30:00.000-0500"); System.out.println(date); assertNotNull(date); }
SimpleDateFormat class helps here also. This is the Date object created:
Sun Jan 05 08:30:00 COT 2020
NOTE: SimpleDateFormat class is not thread safe, so, be careful when you use it in a concurrent environment.
Now, let’s see how to handle time zones.
How can I handle time zones?
Date takes the time zone information from the operative system or the JVM, there is not way to set a different time zone directly using its API:
@Test public void testTimeZone_changeSystemTimeZone() { System.setProperty("user.timezone", "UTC"); Date now = new Date(); System.out.println(now); assertNotNull(now); }
There, we forced the user.timezone property for the JVM to change the timezone to UTC. The Date.toString()
prints the following:
Tue Oct 13 21:48:02 UTC 2020
Handling time zone conversion is a pretty typical use case of a global application and Date cannot help with that.
Besides, it is not possible to easily tests your application using different time zones, and mocking is hard as we see in the following section.
How can I mock date and time for testing?
Well, there are two options: using Powermock to mock the Date instantiation process, or extract the Date creation to a third class and mock that class. For the propose of this post, we will use the second option.
First, we extract the Date creation to a DateUtils class:
public class DateUtils { public Date getNow() { return new Date(); } }
Second, we mock the DateUtils class on the test:
@Mock private DateUtils dateUtils; @Test public void testMockDate() { when(dateUtils.getNow()).thenReturn( new Date(2020, 0, 5, 8, 30, 0)); Date now = dateUtils.getNow(); System.out.println(now); assertEquals(now, new Date(2020, 0, 5, 8, 30, 0)); }
Remember, that Date constructor is deprecated, so, not recommended to use.
java.util.Calendar
Calendar is an abstraction over dates and times, it exists from Java 1.1 version. This class allows more operations than Date class and abstracts different calendars types, besides, handles time zones better. However, its design brought other challenges when using it, for instance:
- Complex API.
- Calendar relies on some operation over Date class.
- Calendar objects are mutable.
Let’s see those challenges in detail solving the following questions.
How can I get the current date and time?
Let’s see how we get the current date using java.util.Calendar:
@Test public void testNow() { Calendar now = Calendar.getInstance(); System.out.println(now); assertNotNull(now); }
To get the current date, we create a new instance of Calendar class through a factory method, in this case, Calendar.getInstance()
. The Calendar.toString()
method prints the following:
java.util.GregorianCalendar[time=1602704657688,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Bogota",offset=-18000000,dstSavings=0,useDaylight=false,transitions=5,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2020,MONTH=9,WEEK_OF_YEAR=42,WEEK_OF_MONTH=3,DAY_OF_MONTH=14,DAY_OF_YEAR=288,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=2,HOUR_OF_DAY=14,MINUTE=44,SECOND=17,MILLISECOND=688,ZONE_OFFSET=-18000000,DST_OFFSET=0]
Calendar doesn’t have a default String format, so, we just see the whole bunch of properties it handles inside. Something good is that we see a ZoneInfo, this class handles the time zone information.
How can I define a fixed date and time?
Let’s see how we created a fixed date and time using java.util.Calendar:
@Test public void testRandomDate() { Calendar now = Calendar.getInstance(); now.set(Calendar.YEAR, 2020); now.set(Calendar.MONTH, 3); now.set(Calendar.DAY_OF_MONTH, 23); now.set(Calendar.HOUR, 8); now.set(Calendar.MINUTE, 30); now.set(Calendar.MILLISECOND, 10); System.out.println(now); assertEquals(now.get(Calendar.YEAR), 2020); }
We need to create a new Calendar instance and set the values you want using the Calendar.set()
method and the Calendar constants.
You can access the Calendar information using Calendar.get(Calendar.X)
methods, for instance, Calendar.get(Calendar.YEAR)
, Calendar.get(Calendar.MONTH)
, etc.
NOTE: Calendar.MONTH properties expect values starting on 0, not 1.
How can I operate over date and time?
Well, Calendar offers some operations like Calendar.add()
, Calendar.after()
and Calendar.before()
:
@Test public void testOperations() { Calendar now = Calendar.getInstance(); now.set(Calendar.YEAR, 2020); now.set(Calendar.MONTH, 3); now.set(Calendar.DAY_OF_MONTH, 23); now.set(Calendar.HOUR, 8); now.set(Calendar.MINUTE, 30); now.set(Calendar.MILLISECOND, 10); now.add(Calendar.MONTH, 2); Calendar expected = Calendar.getInstance(); expected.set(Calendar.YEAR, 2020); expected.set(Calendar.MONTH, 5); expected.set(Calendar.DAY_OF_MONTH, 23); expected.set(Calendar.HOUR, 8); expected.set(Calendar.MINUTE, 30); expected.set(Calendar.MILLISECOND, 10); assertEquals(expected, now); }
If you want to decrease some of the Calendar fields, you should use Calendar.add()
sending a negative number.
How can I format from date to string and the other way around?
Let’s see how to transform from Calendar to String:
@Test public void testDateToString() { Calendar now = Calendar.getInstance(); now.set(Calendar.YEAR, 2020); now.set(Calendar.MONTH, 3); now.set(Calendar.DAY_OF_MONTH, 23); now.set(Calendar.HOUR, 8); now.set(Calendar.MINUTE, 30); now.set(Calendar.SECOND, 10); now.set(Calendar.MILLISECOND, 10); Date nowDate = now.getTime(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); String dateFormat = simpleDateFormat .format(nowDate); System.out.println(dateFormat); assertEquals("2020-04-23T20:30:10.010-0500", dateFormat); }
There is not a specific class to handle this use case of Calendar, instead, we use SimpleDateFormat class. After we create the Calendar, we ask for the Date object using the Calendar.getTime()
method.
Now, let’s see how to transform from String to Calendar:
@Test public void testStringToDate() throws ParseException { SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); Date date = simpleDateFormat .parse("2020-04-23T08:30:10.010-0500"); Calendar now = Calendar.getInstance(); now.setTime(date); System.out.println(date); assertNotNull(now); }
In this case, SimpleDateFormat creates a Date object and we set that into a Calendar object.
How can I handle time zones?
Calendar takes the time zone information from the operative system or the JVM, and you can create a Calendar in a specific time zone, as follows:
@Test public void testTimeZone() { Calendar now = Calendar .getInstance(TimeZone.getTimeZone("UTC")); now.set(Calendar.YEAR, 2020); now.set(Calendar.MONTH, 3); now.set(Calendar.DAY_OF_MONTH, 23); now.set(Calendar.HOUR, 8); now.set(Calendar.MINUTE, 30); now.set(Calendar.SECOND, 10); now.set(Calendar.MILLISECOND, 10); System.out.println(now); assertEquals(TimeZone.getTimeZone("UTC"), now.getTimeZone()); }
There, we set the time zone to UTC. The Calendar.toString()
prints the following:
java.util.GregorianCalendar[time=?,areFieldsSet=false,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2020,MONTH=3,WEEK_OF_YEAR=42,WEEK_OF_MONTH=3,DAY_OF_MONTH=23,DAY_OF_YEAR=288,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=8,HOUR_OF_DAY=20,MINUTE=30,SECOND=10,MILLISECOND=10,ZONE_OFFSET=0,DST_OFFSET=0]
We can see the ZoneInfo object set in UTC.
There is not a easy way to move Calendars from one timezone to another.
How can I mock date and time for testing?
Well, this case is pretty similar with Date class, there are two options: using Powermock to mock the Calendar.getInstance()
method, or extract the Calendar.getInstance()
to a third class and mock that class. For the propose of this post, we will use the second option.
First, we extract the Calendar.getInstance()
creation to a CalendarUtils class:
public class CalendarUtils { public Calendar getNow() { return Calendar.getInstance(); } }
Second, we mock the CalendarUtils class on the test:
@Mock private CalendarUtils calendarUtils; @Test public void testMockDate() { Calendar expected = Calendar.getInstance(); expected.set(Calendar.YEAR, 2020); expected.set(Calendar.MONTH, 3); expected.set(Calendar.DAY_OF_MONTH, 23); expected.set(Calendar.HOUR, 8); expected.set(Calendar.MINUTE, 30); expected.set(Calendar.SECOND, 10); expected.set(Calendar.MILLISECOND, 10); when(calendarUtils.getNow()).thenReturn(expected); Calendar now = calendarUtils.getNow(); System.out.println(now); assertEquals(expected, now); }
After checking how Date and Calendar work, let’s move to the latest API: java.time.*
java.time.LocalDateTime and java.time.ZonedDateTime
In Java 8 a new Date and Time API was created inspired by JodaTime. This new API addresses some of the downsides and challenges Date and Calendar APIs had. Let’s see what this new API offers.
How can I get the current date and time?
Let’s see how we get the current date using java.time.LocalDateTime:
@Test public void testNow() { LocalDateTime now = LocalDateTime.now(); System.out.println(now); assertNotNull(now); }
To get the current date, we create a new instance of LocalDateTime using its factory method LocalDateTime.now()
. This is the LocalDateTime.toString()
of that object:
2020-10-16T15:10:12.606
By default, it prints the date and time in ISO format.
Besides, LocalDateTime is the join between LocalDate and LocalTime, two other objects you can find in the API and use them separably.
You might ask, why the Local prefix? well, that means those dates and times doesn’t have a timezone related. There are other classes to handle timezone as we will see in following sections.
How can I define a fixed date and time?
Let’s see how we created a fixed date and time using java.time.LocalDateTime:
@Test public void testRandomDate() { LocalDateTime now = LocalDateTime .of(2020, 3, 23, 8, 30, 10, 20); System.out.println(now); assertEquals(now.getYear(), 2020); }
We need to create a new LocalDateTime instance using a factory method LocalDateTime.of()
.
You can access the LocalDateTime information using LocalDateTime.getX
methods, for instance, LocalDateTime.getYear()
.
How can I operate over date and time?
Well, LocalDateTime offers some operations like LocalDateTime.plus()
, LocalDateTime.minus()
, LocalDateTime.isAfter()
, LocalDateTime.isBefore()
, and others:
@Test public void testOperations() { LocalDateTime now = LocalDateTime .of(2020, 3, 23, 8, 30, 10, 20); LocalDateTime plus5Days = now.plusDays(5); LocalDateTime expected = LocalDateTime .of(2020, 3, 28, 8, 30, 10, 20); assertEquals(expected, plus5Days); }
In this example, we add 5 days to a defined date. You might notice the return type of the LocalDateTime.plusDays()
operations: a new LocalDateTime object.
Any object in the new API are immutable, that means, you cannot change its state after it was created. Any operation over a object will create a new object.
NOTE: For more information about why immutability is important, check the Object’s Immutability Series.
How can I format from date to string and the other way around?
Let’s see how to transform from LocalDateTime to String:
@Test public void testDateToString() { LocalDateTime now = LocalDateTime .of(2020, 3, 23, 8, 30, 10, 20); DateTimeFormatter formatter = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); String dateFormat = now.format(formatter); System.out.println(dateFormat); assertEquals("2020-03-23T08:30:10.000", dateFormat); }
DateTimeFormatter class handles the transformation to String and vice versa. In this case, we tell the LocalDateTime object to format itself using the DateTimeFormatter object we defined with the date and time pattern.
Now, let’s see how to transform from String to LocalDateTime:
@Test public void testStringToDate() { DateTimeFormatter formatter = DateTimeFormatter .ofPattern( "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS"); LocalDateTime date = LocalDateTime .parse("2020-04-23T08:30:10.000000010", formatter); System.out.println(date); assertEquals(LocalDateTime .of(2020, 4, 23, 8, 30, 10, 10), date); }
In this case, we tell LocalDateTime to parse a String using the DateTimeFormatter defined with the pattern.
NOTE: DateTimeFormatter class is thread safe, meanwhile, SimpleDateFormatter is not.
How can I handle time zones?
The new API has a new class to handle time zones: ZonedDateTime. Let’s see how it works.
@Test public void testTimeZone() { ZonedDateTime now = ZonedDateTime .of(2020, 3, 23, 8, 30, 10, 20, ZoneId.of("UTC")); System.out.println(now); assertEquals(now.getZone(), ZoneId.of("UTC")); }
There, we set the time zone to UTC. The ZonedDateTime.toString()
prints the following:
2020-03-23T08:30:10.000000020Z[UTC]
We can see clearly the timezone.
Now, let’s move between time zones:
@Test public void testTimeZone_moveToPST() { ZonedDateTime utc = ZonedDateTime .of(2020, 3, 23, 8, 30, 10, 20, ZoneId.of("UTC")); ZonedDateTime pst = utc.withZoneSameInstant( ZoneId.of("US/Eastern")); System.out.println(pst); assertEquals(LocalDateTime .of(2020, 3, 23, 4, 30, 10, 20), pst.toLocalDateTime()); }
There, we have a UTC date and time, and we move it to US/Eastern using the ZonedDateTime.withZoneSameInstant()
method. This is how the US/Eastern date looks like:
2020-03-23T04:30:10.000000020-04:00[US/Eastern]
How can I mock date and time for testing?
Well, the new API brings a new class named Clock. A Clock represents the current date and time, in a defined time zone. This abstraction helps to define different clocks, for instance, if you need a lot of precision, you can create an implementation using a NTP server.
The following is an example of how to use Clock and test LocalDateTime objects. First, we have a class that uses LocalDateTime objects:
public class LocalDateTimeUtils { private final Clock clock; public LocalDateTimeUtils(Clock clock) { this.clock = clock; } public LocalDateTime getNow() { return LocalDateTime.now(clock); } }
In this case, we define a Clock object as a member class, and in the getNow method, we create a LocalDateTime using that Clock. The Clock object will be injected on this class.
NOTE: For more information about dependency injection, see the Dependency Inversion Series, besides Unit Testing and Dependency Injection
And the test:
@Test public void testMockDate() { Clock clock = Clock.fixed(ZonedDateTime .of(2020, 3, 23, 8, 30, 10, 20, ZoneId.of("UTC")).toInstant(), ZoneId.of("UTC")); LocalDateTimeUtils localDateTimeUtils = new LocalDateTimeUtils( clock); LocalDateTime now = localDateTimeUtils.getNow(); System.out.println(now); assertEquals(LocalDateTime .of(2020, 3, 23, 8, 30, 10, 20), now); }
To test, we use dependency injection to inject a fixed Clock in the LocalDateTimeUtils class. A fixed clock means that the date and time, and timezone, never change.
Transforming From LocalDateTime to Date
Clearly, the new Java 8 API is more robust than Date and Calendar APIs, however, as there are still a lot of applications using Date and Calendar APIs, the new java.time API defines ways to transform from LocalDateTime to Date, and vice versa.
Let’s start with LocalDateTime to Date:
@Test public void testLocalDateTimeToDate() { LocalDateTime now = LocalDateTime .of(2020, 3, 23, 8, 30, 10, 20); Date date = Date .from(now.atZone(ZoneId.systemDefault()) .toInstant()); System.out.println(date); assertNotNull(date); }
We use the factory method Date.from()
transforming the LocalDateTime to an Instant first.
NOTE: Instant is another class of java.time API.
Now, let’s see the transformation from Date to LocalDateTime.
@Test public void testDateToLocalDateTime() { Date now = new Date(); LocalDateTime date = now.toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime(); System.out.println(date); assertNotNull(date); }
Again, we get from Date a Instant and use it to create a LocalDateTime.
Final Thought
Date and Calendar were between us for a long time, and they are in a lot of current systems, however, the new java.time API is more robust and solves a lot of challenges we had with the previous date and time APIs.
In this post, we just see the surface of the java.time API, there are a lot of more to learn like Instant, Period, Duration classes, I suggest to go deep and study about the whole API.
Finally, don’t be angry if the current system you work on hasn’t migrated yet to the new API, migration is hard and takes time, but, you have now more reasons to push a change in your organization.
If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.