Cleaner, readable, and powerful coding with Java SE 8 New DateTime API JSR 310 .....
|
Java SE 8, JSR 310 |
In this article of the “Java SE 8 new features tour” series, we will dig deep into the explanation, and explore the code of JSR 310 specification, on Calculating timespans with the new DateTime API, Calculating time spans with Instant and Duration, Representing date and time values, Formatting date and time values, and Supporting time-zone offsets.
In the previous article, “Processing Collections with Streams API”; I have dived into the explanation and exploration on how to traverse collections with streams, creating streams from collections and arrays, and finally aggregating stream values.
The source code is hosted on my Github account: clone it from here.
Table of Contents
- Calculating time spans with Instant and Duration
- Representing Date and Time Values
- Formatting Date and Time Values
- Supporting Time-Zone Offsets
Introduction
Java SE 8 includes a complete new API for managing date and time values. The classes that actually hold the data in this new API are all immutable and thread safe. So that means you don't have to worry about passing objects around in a multi threading environment. And if you're using them in parallel streams, everything will always work perfectly. All of the classes in this new API are members of the package java.time
. And I'll start with two of the core classes named Instant
and Duration
.
How It Works
I'll start with this example in the package eg.com.tm.java8.features.datetime.InstDuration
of project Java8Features
in a class code named InstantAndDuration
. And I'll place all this code inside the main
method. The first class I'll describe is named Instant
. I'll type the name of the class and press Ctrl + Space, and choose the class from the java.time
package, and it's imported above. An instant object represents an instant on the Java timeline. Just as with the date
class which is a part of the old way of managing dates and times, an instance represents a number of milliseconds.
Since the Java epoch time, January 1st, 1970, to declare an instant
object, I'll declare it with its type, and give it a name of start. And then I'll call a static
method of the instant class called now()
. And this represents the moment on the current machine when the code was called. Then I'll output that value as a string
, using standard system output. Run the code and you will see the output starting off with the date in year, month, date format and then the time after the letter T
.
package eg.com.tm.java8.features.datetime.InstDuration;
import java.time.Instant;
import static java.time.Instant.now;
public class InstantAndDuration {
public static void main(String[] args) {
Instant start = now();
System.out.println(start);
}
}
Result
2016-08-05T21:21:59.601Z
Once you have a moment in time, you can use it to calculate a difference between that and another moment in time. So I'll create another instant
, which I'll call end
. And I'll get its value from the method now()
as well. Then I'll use system output, and output that value. Notice that there is a slight difference between the two values, and that's the amount of time it is taking on my system. To process this line of code, that's outputting the start value.
public static void main(String[] args) {
Instant start = now();
System.out.println(start);
Instant end = now();
System.out.println(end);
}
Result
2016-08-05T21:33:55.971Z
2016-08-05T21:33:56.048Z
If I were to move that line of code down, so I wasn't doing any other processing between the two calls to the now
method, the two values would be identical, or they might be off by a thousandth of a second.
public static void main(String[] args) {
Instant start = now();
Instant end = now();
System.out.println(start);
System.out.println(end);
}
Result
2016-08-05T21:34:43.365Z
2016-08-05T21:34:43.365Z
Now, I'll show you how to calculate the difference between these two values. When you compare two instants to each other, you'll get an object called a duration. It's represented by the Duration
class, which is also a member of Java.time
. I'll name this object elapsed
. An I'll call a static
method of the duration class called between(Temporal startInclusive, Temporal endExclusive)
. Notice that it's looking for objects typed as something called Temporal
. The Instant
class is a sub-class of Temporal
.
Instant start = now();
Instant end = now();
System.out.println(start);
System.out.println(end);
Duration elapsed = Duration.between(start, end);
System.out.println("Elapsed: "+ elapsed);
Result
Elapsed: PT0S
I'll pass in start
and end
as my two temporal values. And then I'll output the difference. I'll pass in a literal label of elapsed
, and then I'll pass in my variable. That duration object starts with the letter p
and then t
for time. This is again an ISO formatted value. And then it shows me zero seconds. Well, let's see what happens if we toss in a call to the sleep
method. I'll place the cursor here between the start
and end
calls. And I'll use the Thread
class.
I'll press the period, and then press Ctrl+Space. And then I'll call the sleep()
method and pass in a value of 1,000
. Meaning sleep for one second. The sleep
method can throw an error, so I'll use a quick fix, and I'll add a throws
declaration to the main
methods signature. I'll save and run the code, and I see that my lapse time is now 1.001 seconds. You can never really count on things being exact, it all depends on what's going on, on the processing computer.
public static void main(String[] args) throws InterruptedException {
Instant start = now();
Thread.sleep(1000);
Instant end = now();
System.out.println(start);
System.out.println(end);
Duration elapsed = Duration.between(start, end);
System.out.println("Elapsed: "+ elapsed);
Result
Elapsed: PT1.001S
Next, I'll take this printline
call, and move it back to its original location. So now, after I get the start
value, I'll be executing a printline
command. And I'll be sleeping for one second. And I'll run the code. And now my lapse time is 1.057 seconds.
public static void main(String[] args) throws InterruptedException {
Instant start = now();
System.out.println(start);
Thread.sleep(1000);
Instant end = now();
System.out.println(end);
Duration elapsed = Duration.between(start, end);
System.out.println("Elapsed: "+ elapsed);
}
Result
2016-08-05T22:28:42.685Z
2016-08-05T22:28:43.769Z
Elapsed: PT1.084S
To make this a little bit more readable, I'll add a call to the method of the duration object using elapsed.to millis
. That means, get the milliseconds equivalent. And I'll append to that, milliseconds, and I'll run the code.
Duration elapsed = Duration.between(start, end);
System.out.println("Elapsed: "+ elapsed.toMillis() +" milliseconds");
Result
2016-08-05T22:32:52.657Z
2016-08-05T22:32:53.731Z
Elapsed: 1074 milliseconds
Conclusion
And now I see, a readable value, of 1,054 milliseconds. So, that's the Instant
class and the Duration
class. Two of the core classes, of the new date time API, in Java SE 8.
Introduction
I previously described how to use the instant
class in the new date time API to represent a moment in the Java timeline. Here are three more useful classes to represent parts of dates and times. They're called local date, local time, and local date time. Let's say, for example, that you only want to represent a date value. And you don't care about times or seconds or milliseconds but only the current date. Create an instance of a class named LocalDate
.
How It Works
I'm working in a package eg.com.tm.java8.features.datetime.localdt
of project Java8Features
. In a class code named LocalDateAndTime
. with a main
method. I'll start with the name of the class LocalDate
. And when I press Ctrl + Space, I'll choose the class from the java.time
package. I'll name the object currentDate
and I'll get its value with localDate.now
. Notice that there's consistency in the syntax between working with an instant
, a date
, a time
and a date time
.
To get the current value on the current machine, you always use the now
method. Now, I'll output that date
in its default format. I'll save and run the code, and it shows me the date
in year-month-date format.
package eg.com.tm.java8.features.datetime.localdt;
import static java.time.LocalDate.now;
import java.time.LocalDate;
public class LocalDateAndTime {
public static void main(String[] args) throws InterruptedException {
LocalDate currentDate = now();
System.out.println(currentDate);
}
}
Result
2016-08-06
You can also create a date
object using specific year, month, and date values. Once again, I'll create an object typed as LocalDate
. And I'll name this one specificDate
. To get this value, call LocalDate.of
.
And there are a couple of available versions. I'm going to use the one that takes three integer values. They aren't named in the documentation but they represent the year, the month, and the day. I'll patch in the values of 2,000, 1, and 1. Now, in the older version of the date time
API using the date
class. When you were dealing with months, you always had to do it with a 0 based off set. So for January, you'd use 0, for February 1 and so on.
And that wasn't particularly intuitive. In the new day time API, everything is 1 based. So 1 means January, 2 means February and so on. Just as you would normally expect.
I'll once again use system output, and this time, I'll put the new specific date. And when I save and run that, I get the value that I put in, January 1st, 2000.
LocalDate specificDate = LocalDate.of(2016,1,1);
System.out.println(specificDate);
Result
2016-01-01
If you only want to represent a time value, use the LocalTime
class, I'll type the name of the class and import it, I'll name the object currentTime
and I'll get its value from LocalTime.now
.
Again, using the same sort of syntax as with localdate
and with instant
. Then I'll use system output. And I'll output that current time. The default value for the time is in 24 hour notation, and it shows the hour, the minute, the second, and the milliseconds.
LocalTime currentTime = LocalTime.now();
System.out.println(currentTime);
Result
01:18:11.779
I'll use LocalTime
. I'll name this specificTime
. And, just as with the local date class, I'll call the method named of
. Again, there are a number of different versions taking varying numbers of arguments.
I'll use the version that's looking for three integer values and I'll type in 14, 0 and 45. And then I'll output that value to the console. And there's the result. 14, 00 and 45 seconds. Notice that because I didn't provide a milliseconds value the formatted version of that time doesn't show values after the dot.
LocalTime specificTime = LocalTime.of(14,0,45);
System.out.println(specificTime);
Result
14:00:45
Finally, I'll show how to use the LocalDateTime
class.
I'll type the name of the class and import it. I'll name this object currentDT
. And I'll get its value from LocalDateTime.now
. When you output a date time value, you'll get a long format ISO date time. Starting with the date, and ending with the time. And if there are milliseconds in the value, they'll be displayed. And finally, I'll create a specific date, time and I'll do this by combining my specific date and my specific time.
That code will look like the next code. I'll create a LocalDateTime
object. I'll name it, specificDT
and I'll call LocalDateTime.of
again. And this time, I'll use this first version of the method that accepts a local date
object and a local time
object. You can also construct your date time
value from combinations of years, months, dates, and time values. I'll pass in my specific date
and my specific time
. And then, I'll output it to the console. And when I run that code, my specific date time
is a combination of my specific date
and my specific time
.
LocalDateTime currentDT = LocalDateTime.now();
System.out.println(currentDT);
LocalDateTime specificDT = LocalDateTime.of(specificDate, specificTime);
System.out.println(specificDT);
Result
2016-08-06T01:30:46.625
2016-01-01T14:00:45
Conclusion
So, those are the three classes that you can use to represent date
and time
values on the local machine in the current time zone. There are also classes you can use to get time zone sensitive values. And I'll describe those next.
Introduction
I've previously described how to use the LocalDate
, LocalTime
, and LocalDateTime
classes to represent time
values. To present this information to a user, you'll need to format it. And for that purpose, there's a new class named DateTimeFormatter
. I'll show you how to create formatters using some simple syntax. And then how to do very custom work using a class called Daytime
Formatter Builder.
How It Works
I'm working in a package eg.com.tm.java8.features.datetime.format
of project Java8Features
. In a class code named DateTimeFormater
with a main
method.
First, I'll create a date
. I'll give it a type of LocalDate
, making sure to import
that class. And I'll name it currentDate
. And I'll get its value from LocalDate.now
. Next, I'll create a formatter
object. I'll type the name of the class, DateTimeFormatter
, and select it from the java.time.format
package. I'll name this object df
. Now there are a number of ways of creating a formatter. One of the simplest is to use a constant of the DateTimeFormatter
class.
I'll once again type in DateTimeFormatter
. And then, after I type in the period, I see a list of all of the available constants. I'll choose ISO Date
. And this will provide the default formatting for this object. Then, I'll use System Output. I'll call the formatted object format method and pass in the date
object. And here is the result. I'm outputting the value in year month date format with the month and the date padded out to two characters each. Now you can do the same sort of thing with times and date times.
package eg.com.tm.java8.features.datetime.format;
import static java.time.LocalDate.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateTimeFormater {
public static void main(String[] args) {
LocalDate currentDate = LocalDate.now();
DateTimeFormatter df = DateTimeFormatter.ISO_DATE;
System.out.println(df.format(currentDate));
}
}
Result
2016-08-06
I'll take the above bit of code and I'll duplicate it a couple of times and I'll make some changes. In the second version, I'll change the type from local date to local time. The object name to current time, and the name of the class I'm using to get the value to local time. I'll change the name of the date time formatter from DF
to TF
for time formatter. And I'll change the constant I'm using to ISO Time. And then, I'll change the object that I'm formatting. I'll be sure to import the LocalTime
class.
And then, I'll make similar changes to the third version. The class that I'll be working with this time is LocalDateTime
. I'll be sure to import it. I'll name this object, current DT
. And I'll change the class that I'm calling the now
method from. I'll change the formatter to DTF
for DateTimeFormatter
. And I'll change the constant to ISO Date Time. And then I'll format the current DT
object. I'll be sure I'm using the right formatters in each version of the code. I'll save the change, and run the code.
public static void main(String[] args) {
LocalDate currentDate = LocalDate.now();
DateTimeFormatter df = DateTimeFormatter.ISO_DATE;
System.out.println(df.format(currentDate));
LocalTime currentTime = LocalTime.now();
DateTimeFormatter dt = DateTimeFormatter.ISO_TIME;
System.out.println(dt.format(currentTime));
LocalDateTime currentDT = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME;
System.out.println(dtf.format(currentDT));
}
And there are the three formatted values. Now so far, I haven't really accomplished that much, because I've used the constants that represent the default formatting. But let's take a look at some custom formats that are available.
Result
2016-08-09
20:37:11.535
2016-08-09T20:37:11.538
I move down to below the existing code. And I'll create another DateTimeFormatter
, I'll name this one f_long
for the long date format and I'll get its value by calling a method of the DateTimeFormatter
class called a Localised Date.
Notice that there are methods for date, time and date-time, with a variety of arguments. I'll choose this one, of localized date, and I'll pass in a constant of a class called, FormatStyle
. Be sure to import this class. And then after you type the period, you'll see that there are four constants available, full
, long
, medium
and short
. I'll choose the long
version and then I'll output the formatted date by calling F _ long.format
and I'll pass in the current DT
object.
DateTimeFormatter f_long = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);
System.out.println(f_long.format(currentDT));
When I run this code, I get the long
version of the date
.
Result
August 9, 2016
I'll show you another version of this by duplicating these two lines of code and for this version, I'll change the formatter name to f_short
, I'll change the constant I'm using to short
also. And I'll change the name of the formatter that I'm calling. So the long version is the name of the months spelled out. A comma after the date
, and then the year
in four digit format, and the short version at least for the current locale, is the month
and date
, without padding, with slashes separating values, and a two character year
.
DateTimeFormatter f_short = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
System.out.println(f_short.format(currentDT));
Result
8/9/16
And next, I'll show you how to use locales. I'll create a couple of string
s. The first will be called fr_ short
, for French, in short format. To get that value, I'll call my f_short
formatter and then I will call method name withLocal()
. To get a locale value, I'll use the Local
class, this is an existing class that's been available in the previous versions of Java. It's a member of the package Java.util
.
And then, I can call one of the many constants representing various locales. I'll use French. And then from there, I'll call the format
method, and pass in the current date time.
I'll duplicate that line of code and for this version, I'll use fr_long
. I'll use the long formatter and otherwise, the code will be the same. And then, I will output those two values fr_short
and fr_long
.
String fr_short = f_short.withLocale(Locale.FRENCH).format(currentDT);
String fr_long = f_long.withLocale(Locale.FRENCH).format(currentDT);
System.out.println(fr_short);
System.out.println(fr_long);
And here's the result. Notice for fr_short
that the month and the day are reversed from the US version. And that's because in Europe, the date is stated first, and then the month, and then the year. And when I use the long version, I get the months spelled in French.
Result
09/08/16
9 août 2016
Finally, I'll show you how to build completely custom formatters using a class called the date time formatter builder. It uses the builder design pattern, where you can call multiple methods, each returning an instance of the current builder.
I'll type the name of the class and make sure that it's been imported. And I'll name the object b
. I'll instantiate it with the new
keyword, and the constructor method.
Now, at the end of that code, I won't put in the semicolon because I want to immediately call a series of methods that let me build the formatter from scratch. I'll start with a method named Append Value. Notice there's Append Instant, Append Literal, Append Localized and many many others. I'm going to call a method named appendValue()
that accepts an instance of a class named TemporalField
and then I'll use an enumerator named ChronoField
which is extended from that TemporalField
.
And from there, I'll use a constant name month of the year. Next, I will append a literal value. This can be any character or any string
. And just to make this thoroughly unique, I'll put in a couple of pipe characters. Now I'll take these two lines of code and duplicate them for the second version instead of month of year. I'll put in day of month. Notice there's also day of week and day of year. Then I'll duplicate that line of code and move it down. And I'll finish the expression with ChronoField.year
.
Once you've created the builder object, you can get the formatter. I'll create new object, typed as DateTimeFormatter
. I'll name it f
for Formatter
. And called builder objects, to formatter methods, and then finally, I'll format the current date time value. I'll use system output and call f.format()
, and pass in currentDT
.
DateTimeFormatterBuilder b = new DateTimeFormatterBuilder()
.appendValue(ChronoField.DAY_OF_YEAR)
.appendLiteral("||")
.appendValue(ChronoField.DAY_OF_MONTH)
.appendLiteral("||")
.appendValue(ChronoField.YEAR);
DateTimeFormatter f = b.toFormatter();
System.out.println(f.format(currentDT));
And now, when I run my code, I get completely customized format.
Result
222||9||2016
Conclusion
You can use the DateTimeFormatter
builder to build any format you like. And because it uses the builder design pattern, it's easy to create and to maintain the code.
Introduction
The new date time API offers a number of classes that let you manage time zones. Creating day time objects that are offset from Greenwich Mean Time, by certain number of hours, or by particular locations, and calculating differences between time zones.
How It Works
I'm working in a package eg.com.tm.java8.features.datetime.zone
of project Java8Features
. In a class code named TimeZones
. with a main
method.
In its main
method, I've created a DateTimeFormatter
and a LocalDateTime
object. The LocalDateTime
represents the current date and time on my system, in my time zone. And that's Egyptian Time because I'm on the Middle East.
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
LocalDateTime currentDT = LocalDateTime.now();
System.out.println(dtf.format(currentDT));
And then I'm outputting a formatted value to the console. I'm outputting the value using a short
format. And in Egypt notation, it's month, day and year.
Result
8/9/16 10:22 PM
In order to represent a time zone based date time value, use the class ZonedDateTime
. Just like LocalDateTime
, it's immutable and thread safe. I'll type the name of the class, and then press Control + Space to add the import statement. And I'll name the object gmt
for Greenwich Mean Time.
There are a few different ways of creating this object. I'll show you how to create the object calculating an offset from Greenwich Mean Time. I'll use the ZonedDateTime
class again, and after I type the period, I'll see that there are many methods available. I can call now()
again, to get the date time value in my area. I can call of()
methods that let me do various calculations. I can parse string
s, but I'm going to use this version of the now method. I'll pass in an instance of the ZoneId
class.
A ZoneId
represents a certain number of hours offset from Greenwich Mean Time. And I'll get that value by calling a method named ZoneId.of()
. And I'll pass in a literal string
of "GMT+0". That means, show me the current date and time value in Greenwich Mean Time.
ZonedDateTime gmt = ZonedDateTime.now(ZoneId.of("GMT+0"));
System.out.println(dtf.format(gmt));
Now, I'll duplicate my code that's outputting the value to the console. I'll move that down, and I'll change this version to output gmt
. I'll run the code, and there's the result.
Result
8/9/16 8:28 PM
I'm in the Middle East Egypt, and right now, Greenwich Mean Time is two hours ahead.
Here is another approach to getting a ZonedDateTime
. Let's say you wanted to get the ZoneDateTime
in New York. There are many built-in string
s, or constants, that will let you name particular locations, and you'll get back the correct ZoneId
for that location, and you won't have to worry about the math
yourself. I'll create another ZonedDateTime
object, and this time, I'll name it ny
for New York, and I'll get its value by calling ZonedDateTime.now()
, and again I'll pass in ZoneId.of()
, but this time, I'll pass in a string
of America/New_York
.
Make sure to spell this string exactly as you see it here. I'll create a line of code to output that value. I'll save the change, and run it. And New York is on East Coast time, three hours ahead of Pacific time.
ZonedDateTime ny = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println(dtf.format(ny));
Result
8/9/16 4:36 PM
To find out about all of the available string
s, you can call a method of the ZoneId
class called getAvailableZoneIds()
. You'll get back a set. I'll type Set
and press Control + Space, and then choose set from Java.util
.
And I'll set the generic type of the items in this set to String
. I'm name the set zones. And then, I'll call the method, ZoneId.getAvailableZoneIds
. Then I'll loop through the string
s with the forEach()
method. And then I'll pass in a Lambda expression. So I can deal with each of the items in turn.
Set<string> zones = ZoneId.getAvailableZoneIds();
zones.forEach(z -> System.out.println(z));</string>
Result
When I run that code, I see all of the available string
s.
Asia/Aden
America/Cuiaba
Etc/GMT+9
Etc/GMT+8
Africa/Nairobi
America/Marigot
Asia/Aqtau
Pacific/Kwajalein
America/El_Salvador
Asia/Pontianak
Africa/Cairo
Pacific/Pago_Pago
Africa/Mbabane
Asia/Kuching
Pacific/Honolulu
Pacific/Rarotonga
America/Guatemala
Australia/Hobart
Europe/London
America/Belize
America/Panama
Asia/Chungking
America/Managua
America/Indiana/Petersburg
Asia/Yerevan
Europe/Brussels
GMT
Europe/Warsaw
America/Chicago
Asia/Kashgar
Chile/Continental
Pacific/Yap
CET
Etc/GMT-1
Etc/GMT-0
Europe/Jersey
America/Tegucigalpa
Etc/GMT-5
Europe/Istanbul
America/Eirunepe
Etc/GMT-4
America/Miquelon
Etc/GMT-3
Europe/Luxembourg
Etc/GMT-2
Etc/GMT-9
America/Argentina/Catamarca
Etc/GMT-8
Etc/GMT-7
.................
Now, there are so many it might be hard to find the one that you're looking for. So let's say that I wanted to look for London
.
And use the time zone for that particular location, at this particular time of year. As I showed earlier in the article, I could use a predicate
to search the string
s. I'll create a predicate object. And I'll set the generic type to String
. And I'll name the object condition. Then I'll implement the predicate with a lambda expression. I'll pass in str
, and then I'll implement the predicate with a condition, str.contains
, and I'll pass in a string
of London
.
Then I'll refactor my Lamba expression. I'm going to wrap System.out.println()
in braces. Then I'll expand the code to make it a little bit easier to work with. I'll add the semi colon at the end of the print line, and then I'll create an if
statement. And I'll set the condition to condition.test()
, and I'll pass in z
for the current zone. I'll move the println()
statement, to within the condition, and now I'll only printout string
s that match my predicate test.
Set<string> zones = ZoneId.getAvailableZoneIds();
Predicate<string> condition = str -> str.contains("London");
zones.forEach(z -> {
if(condition.test(z))
System.out.println(z);
});</string></string>
I'll save the change and run the code, and there's the result. I find that the correct string
for London
is:
Result
Europe/London
Conclusion
So that's a little bit about working with time zones. Again, use the ZonedDateTime
class instead of LocalDateTime
to represent values that you can modify and calculate against. The ZoneId
represents an offset from Greenwich Mean Time. And there's also a class called Zone Offset that you can use to calculate different time zones against each other.
Resources
- The Java Tutorials, Trail: Date Time
- The LocalDate API
- JSR 310: Date and Time API
- JSR 337: Java SE 8 Release Contents
- OpenJDK website
- Java Platform, Standard Edition 8, API Specification
I hope you enjoyed reading it, as I enjoyed writing it. Please share if you like it, spread the word.
History
- 14th August, 2016: Initial version
CodeProject