So far in this book we have focussed on functional aspects of Java 8 and looked at how to design better API's using Optional and default and static methods in Interfaces. In this chapter, we will learn about another new API that will change the way we work with dates -- Date Time API. Almost all Java developers will agree that date and time support prior to Java 8 is far from ideal and most of the time we had to use third party libraries like Joda-Time in our applications. The new Date Time API is heavily influenced by Joda-Time API and if you have used it then you will feel home.
Before we learn about new Date Time API let's understand why existing Date API sucks. Look at the code shown below and try to answer what it will print.
import java.util.Date;
public class DateSucks {
public static void main(String[] args) {
Date date = new Date(12, 12, 12);
System.out.println(date);
}
}Can you answer what above code prints? Most Java developers will expect the
program to print 0012-12-12 but the above code prints Sun Jan 12 00:00:00 IST 1913. My first reaction when I learnt that program prints Sun Jan 12 00:00:00 IST 1913 was WTF???
The code shown above has following issues:
-
What each 12 means? Is it month, year, date or date, month, year or any other combination.
-
Date API month index starts at 0. So, December is actually 11.
-
Date API rolls over i.e. 12 will become January.
-
Year starts with 1900. And because month also roll over so year becomes `1900
- 12 + 1 == 1913`. Go figure!!
-
Who asked for time? I just asked for date but program prints time as well.
-
Why is there time zone? Who asked for it? The time zone is JVM's default time zone, IST, Indian Standard Time in this example.
Date API is close to 20 years old introduced with JDK 1.0. One of the original authors of Date API is none other than James Gosling himself -- Father of Java Programming Language.
There are many other design issues with Date API like mutability, separate class
hierarchy for SQL, etc. In JDK1.1 effort was made to provide a better API i.e.
Calendar but it was also plagued with similar issues of mutability and index
starting at 0.
Java 8 Date Time API was developed as part of JSR-310 and reside insides
java.time package. The API applies domain-driven design principles with
domain classes like LocalDate, LocalTime applicable to solve problems related to
their specific domains of date and time. This makes API intent clear and easy to
understand. The other design principle applied is the immutability. All the
core classes in the java.time are immutable hence avoiding thread-safety
issues.
The three classes that you will encounter most in the new API are LocalDate,
LocalTime, and LocalDateTime. Their description is like their name suggests:
-
LocalDate: It represents a date with no time or timezone.
-
LocalTime: It represents time with no date or timezone
-
LocalDateTime: It is the combination of LocalDate and LocalTime i.e. date with time without time zone.
We will use JUnit to drive our examples. We will first write a JUnit case that will explain what we are trying to do and then we will write code to make the test pass. Examples will be based on great Indian president -- A.P.J Abdul Kalam.
import org.junit.Test;
import java.time.LocalDate;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
public class DateTimeExamplesTest {
private AbdulKalam kalam = new AbdulKalam();
@Test
public void kalamWasBornOn15October1931() throws Exception {
LocalDate dateOfBirth = kalam.dateOfBirth();
assertThat(dateOfBirth.toString(), equalTo("1931-10-15"));
}
}LocalDate has a static factory method of that takes year, month, and date
and gives you a LocalDate. To make this test pass, we will write dateOfBirth
method in AbdulKalam class using of method as shown below.
import java.time.LocalDate;
import java.time.Month;
public class AbdulKalam {
public LocalDate dateOfBirth() {
return LocalDate.of(1931, Month.OCTOBER, 15);
}
}There is an overloaded of method that takes month as integer instead of
Month enum. I recommend using Month enum as it is more readable and clear.
There are two other static factory methods to create LocalDate instances --
ofYearDay and ofEpochDay.
The ofYearDay creates LocalDate instance from the year and day of year for
example March 31st 2015 is the 90th day in 2015 so we can create LocalDate using
LocalDate.ofYearDay(2015, 90).
LocalDate january_21st = LocalDate.ofYearDay(2015, 21);
System.out.println(january_21st); // 2015-01-21
LocalDate march_31st = LocalDate.ofYearDay(2015, 90);
System.out.println(march_31st); // 2015-03-31The ofEpochDay creates LocalDate instance using the epoch day count. The
starting value of epoch is 1970-01-01. So, LocalDate.ofEpochDay(1) will give
1970-01-02.
LocalDate instance provide many accessor methods to access different fields like year, month, dayOfWeek, etc.
@Test
public void kalamWasBornOn15October1931() throws Exception {
LocalDate dateOfBirth = kalam.dateOfBirth();
assertThat(dateOfBirth.getMonth(), is(equalTo(Month.OCTOBER)));
assertThat(dateOfBirth.getYear(), is(equalTo(1931)));
assertThat(dateOfBirth.getDayOfMonth(), is(equalTo(15)));
assertThat(dateOfBirth.getDayOfYear(), is(equalTo(288)));
}You can create current date from the system clock using now static factory
method.
LocalDate.now()@Test
public void kalamWasBornAt0115() throws Exception {
LocalTime timeOfBirth = kalam.timeOfBirth();
assertThat(timeOfBirth.toString(), is(equalTo("01:15")));
}LocalTime class is used to work with time. Just like LocalDate, it also
provides static factory methods for creating its instances. We will use the of
static factory method giving it hour and minute and it will return LocalTime as
shown below.
public LocalTime timeOfBirth() {
return LocalTime.of(1, 15);
}There are other overloaded variants of of method that can take second and
nanosecond.
LocalTime is represented to nanosecond precision.
You can print the current time of the system clock using now method as shown
below.
LocalTime.now()You can also create instances of LocalTime from seconds of day or nanosecond
of day using ofSecondOfDay and ofNanoOfDay static factory methods.
Similar to LocalDate LocalTime also provide accessor for its field as shown
below.
@Test
public void kalamWasBornAt0115() throws Exception {
LocalTime timeOfBirth = kalam.timeOfBirth();
assertThat(timeOfBirth.getHour(), is(equalTo(1)));
assertThat(timeOfBirth.getMinute(), is(equalTo(15)));
assertThat(timeOfBirth.getSecond(), is(equalTo(0)));
}When you want to represent both date and time together then you can use
LocalDateTime. LocalDateTime also provides many static factory methods to
create its instances. We can use of factory method that takes a LocalDate
and LocalTime and gives LocalDateTime instance as shown below.
public LocalDateTime dateOfBirthAndTime() {
return LocalDateTime.of(dateOfBirth(), timeOfBirth());
}There are many overloaded variants of of method which as arguments take year,
month, day, hour, min, secondOfDay, nanosecondOfDay.
To create current date and time using system clock you can use now factory
method.
LocalDateTime.now()Now that we know how to create instances of LocalDate, LocalTime, and
LocalDateTime let's learn how we can manipulate them.
LocalDate, LocalTime, and LocalDateTime are immutable so each time you perform a manipulation operation you get a new instance.
@Test
public void kalam50thBirthDayWasOnThursday() throws Exception {
DayOfWeek dayOfWeek = kalam.dayOfBirthAtAge(50);
assertThat(dayOfWeek, is(equalTo(DayOfWeek.THURSDAY)));
}We can use dateOfBirth method that we wrote earlier with plusYears on
LocalDate instance to achieve this as shown below.
public DayOfWeek dayOfBirthAtAge(final int age) {
return dateOfBirth().plusYears(age).getDayOfWeek();
}There are similar plus* variants for adding days, months, weeks to the value.
Similar to plus methods there are minus methods that allow you minus year,
days, months from a LocalDate instance.
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);Just like LocalDate LocalTime and LocalDateTime also provide similar
plus*andminus*methods.
For this use-case, we will create an infinite stream of LocalDate starting
from the Kalam's date of birth using the Stream.iterate method.
Stream.iterate method takes a starting value and a function that allows you to
work on the initial seed value and return another value. We just incremented the
year by 1 and return next year birthdate. Then we transformed LocalDate to
DayOfWeek to get the desired output value. Finally, we limited our result set
to the provided limit and collected Stream result into a List.
public List<DayOfWeek> allBirthDateDayOfWeeks(int limit) {
return Stream.iterate(dateOfBirth(), db -> db.plusYears(1))
.map(LocalDate::getDayOfWeek)
.limit(limit)
.collect(toList());
}Duration and Period classes represents quantity or amount of time.
Duration represents quantity or amount of time in seconds, nano-seconds, or days like 10 seconds.
Period represents amount or quantity of time in years, months, and days.
@Test
public void kalamLived30601Days() throws Exception {
long daysLived = kalam.numberOfDaysLived();
assertThat(daysLived, is(equalTo(30601L)));
}To calculate number of days kalam lived we can use Duration class. Duration
has a factory method that takes two LocalTime, or LocalDateTime or
Instant and gives a duration. The duration can then be converted to days,
hours, seconds, etc.
public Duration kalamLifeDuration() {
LocalDateTime deathDateAndTime = LocalDateTime.of(LocalDate.of(2015, Month.JULY, 27), LocalTime.of(19, 0));
return Duration.between(dateOfBirthAndTime(), deathDateAndTime);
}
public long numberOfDaysLived() {
return kalamLifeDuration().toDays();
}@Test
public void kalamLifePeriod() throws Exception {
Period kalamLifePeriod = kalam.kalamLifePeriod();
assertThat(kalamLifePeriod.getYears(), is(equalTo(83)));
assertThat(kalamLifePeriod.getMonths(), is(equalTo(9)));
assertThat(kalamLifePeriod.getDays(), is(equalTo(12)));
}We can use Period class to calculate number of years, months, and days kalam
lived as shown below. Period's between method works with LocalDate only.
public Period kalamLifePeriod() {
LocalDate deathDate = LocalDate.of(2015, Month.JULY, 27);
return Period.between(dateOfBirth(), deathDate);
}In our day-to-day applications a lot of times we have to parse a text format to
a date or time or we have to print a date or time in a specific format. Printing
and parsing are very common use cases when working with date or time. Java 8
provides a class DateTimeFormatter which is the main class for formatting and
printing. All the classes and interfaces relevant to them resides inside the
java.time.format package.
In India, dd-MM-YYYY is the predominant date format that is used in all the
government documents like passport application form. You can read more about
Date and time notation in India on the
wikipedia.
@Test
public void kalamDateOfBirthFormattedInIndianDateFormat() throws Exception {
final String indianDateFormat = "dd-MM-YYYY";
String dateOfBirth = kalam.formatDateOfBirth(indianDateFormat);
assertThat(dateOfBirth, is(equalTo("15-10-1931")));
}The formatDateofBirth method uses DateTimeFormatter ofPattern method to
create a new formatter using the specified pattern. All the main main date-time
classes provide two methods - one for formatting, format(DateTimeFormatter formatter), and one for parsing, parse(CharSequence text, DateTimeFormatter formatter).
public String formatDateOfBirth(final String pattern) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return dateOfBirth().format(formatter);
}For the common use cases, DateTimeFormatter class provides formatters as
static constants. There are predefined constants for BASIC_ISO_DATE i.e
20111203 or ISO_DATE i.e. 2011-12-03, etc that developers can easily
use in their code. In the code shown below, you can see how to use these
predefined formats.
@Test
public void kalamDateOfBirthInDifferentDateFormats() throws Exception {
LocalDate kalamDateOfBirth = LocalDate.of(1931, Month.OCTOBER, 15);
assertThat(kalamDateOfBirth.format(DateTimeFormatter.BASIC_ISO_DATE), is(equalTo("19311015")));
assertThat(kalamDateOfBirth.format(DateTimeFormatter.ISO_LOCAL_DATE), is(equalTo("1931-10-15")));
assertThat(kalamDateOfBirth.format(DateTimeFormatter.ISO_ORDINAL_DATE), is(equalTo("1931-288")));
}Let's suppose we have to parse 15 Oct 1931 01:15 AM to a LocalDateTime
instance as shown in code below.
@Test
public void shouldParseKalamDateOfBirthAndTimeToLocalDateTime() throws Exception {
final String input = "15 Oct 1931 01:15 AM";
LocalDateTime dateOfBirthAndTime = kalam.parseDateOfBirthAndTime(input);
assertThat(dateOfBirthAndTime.toString(), is(equalTo("1931-10-15T01:15")));
}We will again use DateTimeFormatter ofPattern method to create a new
DateTimeFormatter and then use the parse method of LocalDateTime to create
a new instance of LocalDateTime as shown below.
public LocalDateTime parseDateOfBirthAndTime(String input) {
return LocalDateTime.parse(input, DateTimeFormatter.ofPattern("dd MMM yyyy hh:mm a"));
}In Manipulating dates section, we learnt how we can use plus* and minus*
methods to manipulate dates. Those methods are suitable for simple manipulation
operations like adding or subtracting days, months, or years. Sometimes, we need
to perform advance date time manipulation such as adjusting date to first day of
next month or adjusting date to next working day or adjusting date to next
public holiday then we can use TemporalAdjusters to meet our needs. Java 8
comes bundled with many predefined temporal adjusters for common scenarios.
These temporal adjusters are available as static factory methods inside the
TemporalAdjusters class.
LocalDate date = LocalDate.of(2015, Month.OCTOBER, 25);
System.out.println(date);// This will print 2015-10-25
LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
System.out.println(firstDayOfMonth); // This will print 2015-10-01
LocalDate firstDayOfNextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println(firstDayOfNextMonth);// This will print 2015-11-01
LocalDate lastFridayOfMonth = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println(lastFridayOfMonth); // This will print 2015-10-30- firstDayOfMonth creates a new date set to first day of the current month.
- firstDayOfNextMonth creates a new date set to first day of next month.
- lastInMonth creates a new date in the same month with the last matching day-of-week. For example, last Friday in October.
I have not covered all the temporal-adjusters please refer to the documentation for the same.
You can write your own adjuster by implementing TemporalAdjuster functional
interface. Let's suppose we have to write a TemporalAdjuster that adjusts
today's date to next working date then we can use the TemporalAdjusters
ofDateAdjuster method to adjust the current date to next working date as show
below.
LocalDate today = LocalDate.now();
TemporalAdjuster nextWorkingDayAdjuster = TemporalAdjusters.ofDateAdjuster(localDate -> {
DayOfWeek dayOfWeek = localDate.getDayOfWeek();
if (dayOfWeek == DayOfWeek.FRIDAY) {
return localDate.plusDays(3);
} else if (dayOfWeek == DayOfWeek.SATURDAY) {
return localDate.plusDays(2);
}
return localDate.plusDays(1);
});
System.out.println(today.with(nextWorkingDayAdjuster));
