DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Introducing Graph Concepts in Java With Eclipse JNoSQL, Part 3: Understanding Janus
  • Introducing Graph Concepts in Java With Eclipse JNoSQL, Part 2: Understanding Neo4j
  • How to Introduce a New API Quickly Using Micronaut
  • Introducing Graph Concepts in Java With Eclipse JNoSQL

Trending

  • Cloud Security and Privacy: Best Practices to Mitigate the Risks
  • Building Reliable LLM-Powered Microservices With Kubernetes on AWS
  • Immutable Secrets Management: A Zero-Trust Approach to Sensitive Data in Containers
  • Strategies for Securing E-Commerce Applications
  1. DZone
  2. Coding
  3. Java
  4. Java 8 Date and Time

Java 8 Date and Time

Let's journey back to the not-too-distant past to see how Java 8 improved date and time APIs and how to put them to use.

By 
Steven Gentens user avatar
Steven Gentens
·
Apr. 29, 18 · Tutorial
Likes (47)
Comment
Save
Tweet
Share
143.5K Views

Join the DZone community and get the full member experience.

Join For Free

Nowadays, several applications still use the java.util.Date and java.util.Calendar APIs, including libraries to make our lives easier working with these types, for example, JodaTime. Java 8, however, introduced new APIs to handle date and time, which allow us to have more fine-grained control over our date and time representation, handing us immutable datetime objects, a more fluent API and in most cases a performance boost, without using additional libraries. Let’s take a look at the basics.

LocalDate/LocalTime/LocalDateTime

Let’s start off with the APIs that are most related to java.util.Date: LocalDate, a date API that represents a date without time; LocalTime, a time representation without a date; and LocalDateTime, which is a combination of the previous two. All of these types represent the local date and/or time for a region, but, just like java.util.Date, they contain zero information about the zone in which it is represented, only a representation of the date and time in your current timezone.

First of all, these APIs support an easy instantiation:

LocalDate date = LocalDate.of(2018,2,13);
// Uses DateTimeformatter.ISO_LOCAL_DATE for which the format is: yyyy-MM-dd
LocalDate date = LocalDate.parse("2018-02-13");

LocalTime time = LocalTime.of(6,30);
// Uses DateTimeFormatter.ISO_LOCAL_TIME for which the format is: HH:mm[:ss[.SSSSSSSSS]]
// this means that both seconds and nanoseconds may optionally be present.
LocalTime time = LocalTime.parse("06:30");

LocalDateTime dateTime = LocalDateTime.of(2018,2,13,6,30);
// Uses DateTimeFormatter.ISO_LOCAL_DATE_TIME for which the format is the
// combination of the ISO date and time format, joined by 'T': yyyy-MM-dd'T'HH:mm[:ss[.SSSSSSSSS]]
LocalDateTime dateTime = LocalDateTime.parse("2018-02-13T06:30");


It’s easy to convert between them:

// LocalDate to LocalDateTime
LocalDateTime dateTime = LocalDate.parse("2018-02-13").atTime(LocalTime.parse("06:30"));

// LocalTime to LocalDateTime
LocalDateTime dateTime = LocalTime.parse("06:30").atDate(LocalDate.parse("2018-02-13"));

// LocalDateTime to LocalDate/LocalTime
LocalDate date = LocalDateTime.parse("2018-02-13T06:30").toLocalDate();
LocalTime time = LocalDateTime.parse("2018-02-13T06:30").toLocalTime();


Aside from that, it’s incredibly easy to perform operations on our date and time representations, using the `plus` and `minus` methods as well as some utility functions:

LocalDate date = LocalDate.parse("2018-02-13").plusDays(5);
LocalDate date = LocalDate.parse("2018-02-13").plus(3, ChronoUnit.MONTHS);

LocalTime time = LocalTime.parse("06:30").minusMinutes(30);
LocalTime time = LocalTime.parse("06:30").minus(500, ChronoUnit.MILLIS);

LocalDateTime dateTime = LocalDateTime.parse("2018-02-13T06:30").plus(Duration.ofHours(2));

// using TemporalAdjusters, which implements a few useful cases:
LocalDate date = LocalDate.parse("2018-02-13").with(TemporalAdjusters.lastDayOfMonth());


Now how would we move from java.util.Date to LocalDateTime and its variants? Well, that’s simple: We can convert from a Date type to the Instant type, which is a representation of the time since the epoch of 1 January 1970, and then we can instantiate a LocalDateTime using the Instant and the current zone.

LocalDateTime dateTime = LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());


To convert back to a date, we can simply use the Instant that the Java 8 time type represents. One thing to take note of, though, is that although LocalDate, LocalTime and LocalDateTime do not contain any Zone or Offset information, they do represent the local date and/or time in a specific region, and as such they do hold the offset present in that region. Thus, we are required to provide an offset to correctly convert the specific type to an Instant.

// represents Wed Feb 28 23:24:43 CET 2018
Date now = new Date();

// represents 2018-02-28T23:24:43.106
LocalDateTime dateTime = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault());

// represent Wed Feb 28 23:24:43 CET 2018
Date date = Date.from(dateTime.toInstant(ZoneOffset.ofHours(1)));
Date date = Date.from(dateTime.toInstant(ZoneId.systemDefault().getRules().getOffset(dateTime)));


Difference in Time: Duration and Period

As you’ve noticed, in one of the above examples we’ve used a Duration object. Duration and Period are two representations of time between two dates, the former representing the difference of time in seconds and nanoseconds, the latter in days, months and years.

When should you use these? Period when you need to know the difference in time between two LocalDaterepresentations:

Period period = Period.between(LocalDate.parse("2018-01-18"), LocalDate.parse("2018-02-14"));


Duration when you’re looking for a difference between a representation that holds time information:

Duration duration = Duration.between(LocalDateTime.parse("2018-01-18T06:30"), LocalDateTime.parse("2018-02-14T22:58"));


When outputting Period or Duration using toString(), a special format will be used based on ISO-8601 standard. The pattern used for a Period is PnYnMnD, where n defines the number of years, months or days present within the period. This means that P1Y2M3D defines a period of 1 year, 2 months, and 3 days. The ‘P’ in the pattern is the period designator, which tells us that the following format represents a period. Using the pattern, we can also create a period based on a string using the parse() method.

// represents a period of 27 days
Period period = Period.parse("P27D");


When using Durations, we move away slightly from the ISO-8601 standard, as Java 8 does not use the same patterns. The pattern defined by ISO-8601 is PnYnMnDTnHnMn.nS. This is basically the Period pattern, extended with a time representation. In the pattern, T is the time designator, so the part that follows defines a duration specified in hours, minutes and seconds.

Java 8 uses two specific patterns for Duration, namely PnDTnHnMn.nS when parsing a String to a Duration, and PTnHnMn.nS when calling the toString() method on a Duration instance.

Last but not least, we can also retrieve the various parts of a period or duration, by using the corresponding method on a type. However, it’s important to know that the various datetime types also support this through the use of ChronoUnit enumeration type. Let’s take a look at some examples:

// represents PT664H28M
Duration duration = Duration.between(LocalDateTime.parse("2018-01-18T06:30"), LocalDateTime.parse("2018-02-14T22:58"));

// returns 664
long hours = duration.toHours();

// returns 664
long hours = LocalDateTime.parse("2018-01-18T06:30").until(LocalDateTime.parse("2018-02-14T22:58"), ChronoUnit.HOURS);


Working With Zones and Offsets: ZondedDateTime and OffsetDateTime

Thus far, we’ve shown how the new date APIs have made a few things a little easier. What really makes a difference, however, is the ability to easily use date and time in a timezone context. Java 8 provides us with ZonedDateTime and OffsetDateTime, the first one being a LocalDateTime with information for a specific Zone (e.g. Europe/Paris), the second one being a LocalDateTime with an offset. What’s the difference? OffsetDateTime uses a fixed time difference between UTC/Greenwich and the date that is specified, whilst ZonedDateTime specifies the zone in which the time is represented, and will take daylight saving time into account.

Converting to either of these types is very easy:

OffsetDateTime offsetDateTime = LocalDateTime.parse("2018-02-14T06:30").atOffset(ZoneOffset.ofHours(2));
// Uses DateTimeFormatter.ISO_OFFSET_DATE_TIME for which the default format is
// ISO_LOCAL_DATE_TIME followed by the offset ("+HH:mm:ss").
OffsetDateTime offsetDateTime = OffsetDateTime.parse("2018-02-14T06:30+06:00");

ZonedDateTime zonedDateTime = LocalDateTime.parse("2018-02-14T06:30").atZone(ZoneId.of("Europe/Paris"));
// Uses DateTimeFormatter.ISO_ZONED_DATE_TIME for which the default format is
// ISO_OFFSET_DATE_TIME followed by the the ZoneId in square brackets.
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2018-02-14T06:30+08:00[Asia/Macau]");
// note that the offset does not matter in this case.
// The following example will also return an offset of +08:00
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2018-02-14T06:30+06:00[Asia/Macau]");


When switching between them, you have to keep in mind that converting from a ZonedDateTime to OffsetDateTimewill take daylight saving time into account, while converting in the other direction, from OffsetDateTime to ZonedDateTime, means you will not have information about the region of the zone, nor will there be any rules applied for daylight saving time. That is because an offset does not define any time zone rules, nor is it bound to a specific region.

ZonedDateTime winter = LocalDateTime.parse("2018-01-14T06:30").atZone(ZoneId.of("Europe/Paris"));
ZonedDateTime summer = LocalDateTime.parse("2018-08-14T06:30").atZone(ZoneId.of("Europe/Paris"));

// offset will be +01:00
OffsetDateTime offsetDateTime = winter.toOffsetDateTime();
// offset will be +02:00
OffsetDateTime offsetDateTime = summer.toOffsetDateTime();

OffsetDateTime offsetDateTime = zonedDateTime.toOffsetDateTime();

OffsetDateTime offsetDateTime = LocalDateTime.parse("2018-02-14T06:30").atOffset(ZoneOffset.ofHours(5));
ZonedDateTime zonedDateTime = offsetDateTime.toZonedDateTime();


Now, what if we would like to know what the time for a specific zone or offset is in our own timezone? There are some handy functions defined for that as well!

// timeInMacau represents 2018-02-14T13:30+08:00[Asia/Macau]
ZonedDateTime timeInMacau = LocalDateTime.parse( "2018-02-14T13:30" ).atZone( ZoneId.of( "Asia/Macau" ) );
// timeInParis represents 2018-02-14T06:30+01:00[Europe/Paris]
ZonedDateTime timeInParis = timeInMacau.withZoneSameInstant( ZoneId.of( "Europe/Paris" ) );

OffsetDateTime offsetInMacau = LocalDateTime.parse( "2018-02-14T13:30" ).atOffset( ZoneOffset.ofHours( 8 ) );
OffsetDateTime offsetInParis = offsetInMacau.withOffsetSameInstant( ZoneOffset.ofHours( 1 ) );


It would be a hassle if we would have to manually convert between these types all the time to get the one we need. This is where Spring Framework comes to our aid. Spring provides us with quite a few datetime converters out of the box, which are registered on the ConversionRegistry and can be found in the org.springframework.format.datetime.standard.DateTimeConverters class.

When using these converters, it is important to know that it will not convert time between regions or offsets. The ZonedDateTimeToLocalDateTimeConverter, for example, will return the LocalDateTime for the zone it was specified in, not the LocalDateTime that it would represent in the region of your application.

ZonedDateTime zonedDateTime = LocalDateTime.parse("2018-01-14T06:30").atZone(ZoneId.of("Asia/Macau"));
// will represent 2018-01-14T06:30, regardless of the region your application has specified
LocalDateTime localDateTime = conversionService.convert(zonedDateTime, LocalDateTime.class);

Last but not least, you can consult ZoneId.getAvailableZoneIds() to find all the available time zones, or use the map ZoneId.SHORT_IDS, which contains an abbreviated version for a few time zones such as EST, CST and more.

Formatting - Using the DateTimeFormatter

Of course, various regions in the world use different formats to specify the time. One application might use MM-dd-yyyy, whilst another uses dd/MM/yyyy. Some applications want to remove all confusion and represent their dates by yyyy-MM-dd. When using java.util.Date, we would quickly move to using multiple formatters. The DateTimeFormatter class, however, provides us with optional patterns, so that we can use a single formatter for several formats! Let’s take a look using some examples.

// Let’s say we want to convert all of patterns mentioned above.
// 09-23-2018, 23/09/2018 and 2018-09-23 should all convert to the same LocalDate.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd][dd/MM/yyyy][MM-dd-yyyy]");
LocalDate.parse("09-23-2018", formatter);
LocalDate.parse("23/09/2018", formatter);
LocalDate.parse("2018-09-23", formatter);


The square brackets in a pattern define an optional part in the pattern. By making our various formats optional, the first pattern that matches the string will be used to convert our date representation. This might get quite difficult to read when you’re using multiple patterns, so let’s take a look at creating our DateTimeFormatter using the builder pattern.

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ) )
.optionalStart().appendPattern( "dd/MM/yyyy" ).optionalEnd()
.optionalStart().appendPattern( "MM-dd-yyyy" ).optionalEnd()
.toFormatter();


These are the basics to include multiple patterns, but what if our patterns only differ slightly? Let’s take a look at yyyy-MM-dd and yyyy-MMM-dd.

// 2018-09-23 and 2018-Sep-23 should convert to the same LocalDate.
// Using the ofPattern example we’ve used above will work:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy-MMM-dd]" );
LocalDate.parse( "2018-09-23", formatter );
LocalDate.parse( "2018-Sep-23", formatter );

// Using the ofPattern example where we reuse the common part of the pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyy-[MM-dd][MMM-dd]" );
LocalDate.parse( "2018-09-23", formatter );
LocalDate.parse( "2018-Sep-23", formatter );


However, you should not use a formatter that supports multiple formats when converting to a string, because when we would use our formatter to format our date to a string representation, it will also use the optional patterns.

LocalDate date = LocalDate.parse("2018-09-23");
// will result in 2018-09-232018-Sep-23
date.format(DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy-MMM-dd]" ));
// will result in 2018-09-23Sep-23
date.format(DateTimeFormatter.ofPattern( "yyyy-[MM-dd][MMM-dd]" ));


Since we’re in the 21st century, obviously we have to take globalization into account, and we’ll want to offer localized dates for our users. To ensure that your DateTimeFormatter returns a specific locale, you can simply do the following:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "EEEE, MMM dd, yyyy" ).withLocale(Locale.UK);

DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("yyyy-MMM-dd" ).toFormatter(Locale.UK);


To find which Locales are available, you can use Locale.getAvailableLocales().

Now it could be that the date pattern you receive holds more information than the type you’re using. A DateTimeFormatter will throw an exception as soon as a provided date representation isn’t in accords with the pattern. Let’s take a closer look at the issue and how to work around it.

// The issue: this will throw an exception.
LocalDate date = LocalDate.parse("2018-02-15T13:45");
// We provide a DateTimeFormatter that can parse the given date representation.
// The result will be a LocalDate holding 2018-02-15.
LocalDate date = LocalDate.parse("2018-02-15T13:45", DateTimeFormatter.ISO_LOCAL_DATE_TIME);


Let’s create a formatter that can handle the ISO date, time, and datetime patterns.

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional( DateTimeFormatter.ISO_LOCAL_DATE )
.optionalStart().appendLiteral( "T" ).optionalEnd()
.appendOptional( DateTimeFormatter.ISO_LOCAL_TIME )
.toFormatter();


Now we can perfectly execute all of the following:

// results in 2018-03-16
LocalDate date = LocalDate.parse( "2018-03-16T06:30", formatter );
LocalDate date = LocalDate.parse( "2018-03-16", formatter );
// results in 06:30
LocalTime time = LocalTime.parse( "2018-03-16T06:30", formatter );
LocalTime time = LocalTime.parse( "06:30", formatter );
LocalDateTime localDateTime = LocalDateTime.parse( "2018-03-16T06:30", formatter );


LocalDateTime? What if you’d expect a LocalTime and you are given a date representation or vice versa?

// will throw an exception
LocalDateTime localDateTime = LocalDateTime.parse("2018-03-16", formatter);
LocalDate localDate = LocalDate.parse("06:30", formatter);


For these last two cases, there is no single correct solution, but it depends on what you require, or what those dates and times represent or could represent. The magic is found in the use of TemporalQuery, which you can use to create default values for a part of the pattern.

If we start with a LocalDateTime, and you just want the LocalDate or LocalTime, you’ll receive the corresponding part of the LocalDateTime. To create a LocalDateTime, we’ll need default values for the date and time it is holding. Let’s say that if you do not provide information about a date, we’ll return today’s date, and if you don’t provide a time, we’ll assume you meant the start of the day.

Since we’re returning a LocalDateTime, it won’t be parsed to a LocalDate or LocalTime, so let’s use the ConversionService to get the correct type.

TemporalQuery<TemporalAccessor> myCustomQuery = new MyCustomTemporalQuery();
// results in 2018-03-16
LocalDateTime localDateTime = conversionService.convert( formatter.parse( "2018-03-16", myCustomQuery ), LocalDateTime.class );
// results in 00:00
LocalTime localTime = conversionService.convert( formatter.parse( "2018-03-16", myCustomQuery ), LocalTime.class );

class MyCustomTemporalQuery implements TemporalQuery<TemporalAccessor>
{
    @Override
    public TemporalAccessor queryFrom( TemporalAccessor temporal ) {
        LocalDate date = temporal.isSupported( ChronoField.EPOCH_DAY )
        ? LocalDate.ofEpochDay( temporal.getLong( ChronoField.EPOCH_DAY ) ) : LocalDate.now();
        LocalTime time = temporal.isSupported( ChronoField.NANO_OF_DAY )
        ? LocalTime.ofNanoOfDay( temporal.getLong( ChronoField.NANO_OF_DAY ) ) : LocalTime.MIN;
        return LocalDateTime.of( date, time );
    }
}


Using TemporalQuery allows us to check which information is present and to provide defaults for any information that is missing, enabling us to easily convert to the required type, using the logic that makes sense in our application.

To learn how to compose valid time patterns, check out the DateTimeFormatter documentation.

Conclusion

Most new features require some time to understand and get used to, and the Java 8 Date/Time API is no different. The APIs provide us with better access to the correct format necessary, as well as a more standardized and readable manner of working with date time operations. Using these tips and tricks, we can pretty much cover all of our use cases.

Java (programming language)

Published at DZone with permission of Steven Gentens. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Introducing Graph Concepts in Java With Eclipse JNoSQL, Part 3: Understanding Janus
  • Introducing Graph Concepts in Java With Eclipse JNoSQL, Part 2: Understanding Neo4j
  • How to Introduce a New API Quickly Using Micronaut
  • Introducing Graph Concepts in Java With Eclipse JNoSQL

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!