How do I migrate from Date and Calendar to the new Java 8 Date API?

Asked

Viewed 3,404 times

28

Until Java 7, we had the classes Date and Calendar to represent dates. To convert them to Strings, the easiest way was with the use of SimpleDateFormat.

Java 8 has introduced a new date API. How can it be used? How do I integrate it with Date and Calendar which are used in existing and legacy code? What is his relationship with Joda-Time?

  • Related: https://answall.com/q/413649/112052

2 answers

30


Legacy API problems

The classes java.util.Date and java.util.Calendar, as well as the subclasses java.util.GregorianCalendar, java.sql.Date, java.sql.Time and java.sql.Timestamp, are notorious for being poorly engineered and difficult to use classes because their API has been poorly designed. They work properly if used with due care, but their code accumulates various bad programming practices and recurring problems that disrupt the lives of programmers in Java.

In addition, these classes are all mutable, which makes them inappropriate to use in some cases. For example:

 Date a = ...;
 Date d = new Date();
 pessoa.setAtualizacao(d); // Define a data de atualização.

 // Em algum lugar bem longe do código acima:
 d.setTime(1234); // A data de atualização muda magicamente de forma misteriosa.

Another problem in these classes is that they are not thread-safe. As these classes are changeable, this is until it is expected. In cases where they do not mutate while being used, this should not cause problems concerning thread-Safety for most of these classes. But with the class SimpleDateFormat, the situation is different. Share an instance of SimpleDateFormat between several threads will cause unpredictable results even if the instance of SimpleDateFormat no external changes/mutations. This is because during the parse or date formatting, the class SimpleDateFormat changes the internal state of itself.

That’s why in Java 8, new classes were designed to replace them.

The new API

First, in the new API all classes are immutable and thread-safe. Only this already makes them much easier to use. Also, their API has been quite planned, discussed and exercised to stay consistent.

The most commonly used classes are the following:

  • LocalDate - Represents a date without time or time zone information.

  • LocalTime - Represents one hour without date or time zone information.

  • OffsetTime - Represents one hour without date information, but with a fixed time zone (no daylight saving time taken into account).

  • LocalDateTime - Represents a date and time, but no time zone.

  • ZonedDateTime - Represents a date and time zone that takes into account daylight saving time.

  • OffsetDateTime - Represents a fixed date and time zone (no daylight saving time taken into account).

  • YearMonth - Represents a date containing only one month and one year.

  • Year - Represents a date corresponding to only one year.

  • Instant - Represents a point in time, accurate to nanoseconds.

They are all interface implementations Temporal, which specifies the behavior common to all of them. And note that their API is much easier to use than Date or Calendar, has a lot of methods to add dates, check who is before or after, extract certain field (day, month, hour, second, etc.), convert between one type and another, etc.

There are also implementations of Temporal more specific to different calendars. Namely: JapaneseDate, ThaiBuddhistDate, HijrahDate and MinguoDate. They are analogous to LocalDate, but on specific calendars, and therefore do not have time or time zone information.

Note also that they all have a static method now() that builds the object of the corresponding class according to the system time. For example:

LocalDate hoje = LocalDate.now();
LocalDateTime horaRelogio = LocalDateTime.now();
Instant agora = Instant.now();

Time zones are represented by the class ZoneId. An instance that corresponds to ZoneId of the local machine can be obtained by the method ZoneId.systemDefault(). Another way to obtain instances of ZoneId is by means of the method ZoneId.of(String). For example:

ZoneId fusoHorarioDaqui = ZoneId.systemDefault();
ZoneId utc = ZoneId.of("Z");
ZoneId utcMais3 = ZoneId.of("+03:00");
ZoneId fusoDeSaoPaulo = ZoneId.of("America/Sao_Paulo");

Note that some time zones are fixed, ie are not affected by daylight saving time rules, while others such as ZoneId.of("America/Sao_Paulo"), are affected by daylight saving time.

Conversion between Date and the new classes

To convert a Date to an instance of one of the package classes java.time, we can do it like this:

Date d = ...;
Instant i = d.toInstant();
ZonedDateTime zdt = i.atZone(ZoneId.systemDefault());
OffsetDateTime odt = zdt.toOffsetDateTime();
LocalDateTime ldt = zdt.toLocalDateTime();
LocalTime lt = zdt.toLocalTime();
LocalDate ld = zdt.toLocalDate();

In the above code, the time zone used is important. Normally you will use the ZoneId.systemDefault() or the ZoneId.of("Z"), depending on what you are doing. In some cases, you may want to use some other time zone. If you want to store the time zone in some variable (possibly static) and always (re)use later, no problem (even recommended in many cases).

Obviously, there are several other ways to get instances of the classes defined above.

To convert back to Date:

ZonedDateTime zdt = ...;
Instant i2 = zdt.toInstant();
Date.from(i2);

Parse and formatting with String

To convert any of them to String, you use the class java.time.format.DateTimeFormatter. She’s the replacement for SimpleDateFormat. For example:

DateTimeFormatter fmt = DateTimeFormatter
        .ofPattern("dd/MM/uuuu")
        .withResolverStyle(ResolverStyle.STRICT);
LocalDate ld = ...;
String formatado = ld.format(fmt);

The reverse process is done with static methods parse(String, DateTimeFormatter) that each of these classes has. For example:

DateTimeFormatter fmt = ...;
String texto = ...;
LocalDate ld = LocalDate.parse(texto, fmt);

One detail to consider is the use of uuuu instead of yyyy in the method ofPattern. The reason is that yyyy does not work in case of dates before Christ. Rarely would it matter, but where it doesn’t matter, the two work the same, and where it matters, the uuuu should be used. Therefore, there is little point in using the yyyy to the detriment of uuuu. More details in this reply (in English).

Another detail is the withResolverStyle(ResolverStyle) which tells what to do with poorly formed dates. There are three possibilities: STRICT, SMART and LENIENT. The STRICT does not allow anything that is not strictly in the standard. The mode LENIENT allows him to interpret 31/06/2017 as 01/07/2017, for example. The SMART tries to guess which is the best way, interpreting 31/06/2017 as 30/06/2017. The pattern is the SMART, but I recommend using the STRICT always, as it does not tolerate poorly formed dates and does not attempt to guess what a poorly formed date could be. See some tests about this in the ideone.

Conversion of Calendar

The legacy class GregorianCalendar is for all purposes equivalent to a new class ZonedDateTime. The methods GregorianCalendar.from(ZonedDateTime) and GregorianCalendar.toZonedDateTime() serve to make direct conversion:

ZonedDateTime zdt1 = ...;
GregorianCalendar gc = GregorianCalendar.from(zdt1);
ZonedDateTime zdt2 = gc.toZonedDateTime();

Having then the conversion of Calendar for ZonedDateTime and vice versa, use the above described methods if you want to get some of the new API objects such as LocalDate or LocalTime.

If what you have is an instance of Calendar instead of GregorianCalendar, will almost always be able to make a cast for GregorianCalendar to use the method toZonedDateTime(). If you don’t want to use the cast, it is possible to convert the Calendar for Instant:

Calendar c2 = ...;
Date d2 = c2.getTime();
Instant i2 = d2.toInstant();

It is also possible to build a Calendar from a Instant using the Date as an intermediary:

Instant i = ...;
Date d1 = Date.from(i);
Calendar c = new GregorianCalendar();
c.setTime(d1);

About Joda-Time

As to the Joda-Time, it is an API that was developed for a few years exactly with the intention of replacing the Date and the Calendar. And she did it! The package java.time and all classes there are heavily inspired in Joda-Time, although there are some important differences that aim to not repeat some of the mistakes of Joda-Time.

  • +1 it is very good to have all this information about the team package in one place, it makes it easier when a doubt arises. Speaking of doubt, I did not understand very well what I meant by "fixed time zone" of offsettime and offsetdatetime.

  • 1

    @diegofm Edited response.

  • 1

    Great answer, but the java.time nay is the "Joda-Time with renamed package name". Despite having several classes with the same names, in fact there are several differences between the 2 Apis, some even conceptual, and there is a good summary done by the author in his blog: http://blog.joda.org/2009/11/why-jsr-310-isn-joda-time_4941.html - highlight for the end "I Took the Decision that I didn’t want to add an API to the JDK that had known design Flaws. And the changes required weren’t just minor. As a result, JSR-310 Started from Scratch, but with an API 'Inspired by Joda-Time".

  • 1

    @hkotsubo Curious. At the time I wrote this reply, I saw this information on some blog I saw somewhere. However, anyway, I rewrote that last piece. Thank you.

  • 1

    In fact, by the time the JSR310 started to gain more prominence, I also thought the Apis would be the same, but the author took the opportunity to improve and correct points that he didn’t think were ideal, which in my opinion was great. This article lists the main differences between the Apis: http://blog.joda.org/2014/11/converting-from-joda-time-to-javatime.html

12

Complementing the victor’s response, follow a few more points to note when migrating from one API to another. In the text below I sometimes refer to the java.time as "new API" (despite being released in 2014) and the Date, Calendar and other classes such as "legacy API" (as this is the term used in oracle tutorial).


Precision

java.util.Date and java.util.Calendar have millisecond accuracy (3 decimal places in the fraction of seconds), while package classes java.time are nanosecond accurate (9 decimal places).

This means that converting the new API to the legacy API entails loss of accuracy. Ex:

// Instant com 9 casas decimais (123456789 nanossegundos)
Instant instant = Instant.parse("2019-03-21T10:20:40.123456789Z");
// converte para Date (mantém apenas 3 casas decimais)
Date date = Date.from(instant);
System.out.println(date.getTime()); // 1553163640123

// ao converter de volta para Instant, as casas decimais originais são perdidas
Instant instant2 = date.toInstant();
System.out.println(instant2); // 2019-03-21T10:20:40.123Z
System.out.println(instant2.getNano()); // 123000000

When converting from java.time.Instant for java.util.Date, only the first 3 decimal places are kept (the rest are simply discarded). Therefore, when converting this Date back to Instant, he no longer has these decimal places.

But notice that in the end, getNano() returns 123000000. Even if the Date is only millisecond accurate, internally one Instant always keeps the value in nanoseconds.

If you want to restore the original value of the second fractions, it must be saved separately. To restore it, just use a java.time.temporal.ChronoField:

// Instant com 9 casas decimais (123456789 nanossegundos)
Instant instant = Instant.parse("2019-03-21T10:20:40.123456789Z");
// converte para Date (mantém apenas 3 casas decimais)
Date date = Date.from(instant);
// guardar o valor da fração de segundos
int nano = instant.getNano();

.....
// converter de volta para Instant e restaurar o valor dos nanossegundos
Instant instant2 = date.toInstant().with(ChronoField.NANO_OF_SECOND, nano);
System.out.println(instant2); // 2019-03-21T10:20:40.123456789Z
System.out.println(instant2.getNano()); // 123456789

Parsing to more than 3 decimal places

This limitation of 3 decimal places also applies to Parsing. For example, if we try to do the Parsing of a String containing 6 decimal places in the fraction of seconds:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
Date date = sdf.parse("2019-03-21T10:20:40.123456");
System.out.println(date); // Thu Mar 21 10:22:43 BRT 2019

Note that in the String the time is "10:20:40", but the exit was "10:22:43". This happens because, according to documentation, the letter S corresponds to milliseconds. Placing 6 letters S, as we did, does not cause the 123456 be interpreted as microseconds (which is what this value actually represents). Instead, the SimpleDateFormat interprets as 123456 millisecond, which in turn corresponds to "2 minutes, 3 seconds and 456 milliseconds" - and this value is added to the time obtained. Therefore the result is 10:22:43 (whether or not this algorithm makes sense is another story, but the fact is that SimpleDateFormat does a lot of other weird things besides this).

In the above case, when printing the Date, internally is called your method toString(), that omits the milliseconds. So let’s use the same SimpleDateFormat above to try to print the milliseconds:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
Date date = sdf.parse("2019-03-21T10:20:40.123456");
System.out.println(sdf.format(date)); // 2019-03-21T10:22:43.000456

Notice that the result has .000456 (or 456 microseconds), whereas in fact 456 is the value of milliseconds (since Date does not have microsecond accuracy), so it should be shown as .456 (or 456000, since the format indicates 6 digits). But when placing 6 letters S, the documentation says that numerical values are filled with zeros on the left if the value has fewer digits than the number of letters. That is why 456 was shown as 000456.

That is, if you are dealing with more than 3 decimal places in the fraction of seconds, Date, Calendar and SimpleDateFormat simply do not work. One way to solve is simply to treat the decimals separately, for example:

String s = "2019-03-21T10:20:40.123456";
String[] partes = s.split("\\.");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date date = sdf.parse(partes[0]);
// completar com zeros à direita, para sempre ter o valor em nanossegundos
int nanossegundos = Integer.parseInt(String.format("%-9s", partes[1]).replaceAll(" ", "0"));
System.out.println(sdf.format(date)); // 2019-03-21T10:20:40
System.out.println(nanossegundos); // 123456000

// o Date foi gerado sem os milissegundos, já que o parsing foi feito sem eles
// se quiser ser preciso mesmo, devemos somar os milissegundos ao Date
date.setTime(date.getTime() + (nanossegundos / 1000000));

Already in the API java.time it is possible to do the Parsing of the 6 decimal places without problems:

DateTimeFormatter parser = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSS");
LocalDateTime dt = LocalDateTime.parse("2019-03-21T10:20:40.123456", parser);
System.out.println(dt); // 2019-03-21T10:20:40.123456

Now yes the second fractions have been interpreted correctly. This happens because in new API to letter S means "fractions of a second" (and no longer milliseconds), and can interpret up to 9 decimal places. This brings us to another important point.


Formatting and Parsing

As we have seen above, the parameter we passed to SimpleDateFormat (yyyy-MM-dd'T'HH:mm:ss.SSSSSS) does not work exactly the same as in java.time:

  • the S has a slightly different meaning and function: in legacy API gives wrong results when it has more than 3 decimal places
  • in the java.time used u next year instead of y (and the victor’s response already explains very well the difference)

This is an important point: just because a format worked with SimpleDateFormat, doesn’t mean it’s gonna work the same way with DateTimeFormatter. The letter u, for example, it means "year" in java.time, but in the legacy API it means "day of the week". And there are new letters that have been added in Java 8, such as Q for the quarter, e for the "localized day of the week" (i.e., based on the Locale), among others. Always refer to documentation for more details (and even in the legacy API there are some differences, such as the letter X, that was just added in Java 7 - see that in the java 6 documentation she doesn’t exist).

In addition, there are more options for formatting and Parsing. For example, the format we are using in the above examples (which is defined by the standard ISO 8601) can be interpreted directly:

LocalDateTime dt = LocalDateTime.parse("2019-03-21T10:20:40.123456");

Internally this method uses the default constant DateTimeFormatter.ISO_LOCAL_DATE_TIME. The difference to the previous example is that using .SSSSSS, he can only interpret Strings which have exactly 6 decimal places. Already the ISO_LOCAL_DATE_TIME is more flexible as it allows zero to 9 decimal places.

We can simulate this behavior (having a field with a variable number of digits), using a java.time.format.DateTimeFormatterBuilder:

DateTimeFormatter parser = new DateTimeFormatterBuilder()
    .appendPattern("uuuu-MM-dd'T'HH:mm:ss")
    .optionalStart() // frações de segundo opcionais
    .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) // de 0 a 9 dígitos
    .toFormatter();

LocalDateTime dt = LocalDateTime.parse("2019-03-21T10:20:40", parser);
System.out.println(dt); // 2019-03-21T10:20:40

LocalDate date = LocalDate.parse("2019-03-21T10:20:40.123456789", parser);
System.out.println(date); // 2019-03-21

Note the example above for the final stretch with LocalDate. This class only has the day, month and year, but to get it from a String which contains date and time, I had to use the same parser. This happens because the parser must be able to interpret the String whole, even if some fields are not used. That is, the parser interprets the String and the LocalDate only take what you need (day, month and year), discarding the rest.

See also that the parser is able to interpret so much Strings no fraction of a second as high as 9 decimal places. DateTimeFormatterBuilder has many options that are not possible to do with SimpleDateFormat, so the migration from one to the other is not so direct (don’t just copy and paste the same format and think everything will work the same way, and the new API still gives you more options and better alternatives to get the same results).


Ways of Parsing

Detailing a little more the modes of Parsing (that the victor’s response mentions), in the java.time there are three:

The way LENIENT allows invalid dates and makes automatic adjustments. For example, 31/06/2017 is adjusted to 01/07/2017. In addition, this mode accepts values outside the defined limits for each field, such as day 32, month 15, etc. For example, 32/15/2017 is adjusted to 01/04/2018.

The way SMART also makes some adjustments when the date is invalid, so 31/06/2017 is interpreted as 30/06/2017. The difference to LENIENT is that this mode does not accept values outside the limits of the fields (month 15, day 32, etc), so 32/15/2017 gives error (throws a DateTimeParseException). That’s the way default when you create a DateTimeFormatter.

The way STRICT is the most restricted: does not accept values out of bounds and does not make adjustments when the date is invalid, so 31/06/2017 and 32/15/2017 make a mistake (throw a DateTimeParseException).

Already SimpleDateFormat only has two modes: lenient and nonlenient (which can be configured using the method setLenient). The default is to be lenient, which causes the "strange" behaviors already cited (such as the mess that is made in the Parsing of 6 decimal places in the second fractions, which could be avoided by setting it to nonlenient).


Dates and timezones

A Date, despite the name, it does not represent a date - in the sense of representing only a single value day, month, year, hour, minute and second. In fact this class represents an instant, a point in the timeline. The only value it holds is a long containing the timestamp: the amount of milliseconds since the Unix Epoch (which in turn is "January 1, 1970 at midnight in UTC").

The detail of the timestamp is that it is the same all over the world, but the corresponding date and time may change according to where you are. For example, the timestamp 1553163640000 corresponds to:

  • March 21, 2019 at 07:20:40 am São Paulo
  • 21 March 2019 at 11:20:40 in Berlin
  • 22 from March 2019 to 00:20:40 in Samoa

In all these places, the timestamp is the same: any computer, anywhere in the world, that ran System.currentTimeMillis() (or any other code that gets the current timestamp) at that exact instant would get the same result (1553163640000). However, the date and time corresponding to this timestamp are different, depending on the Timezone being used.

Date represents the timestamp, not the dates and times corresponding to a Timezone. The problem is that when you print a Date, he uses the Timezone default that is set in the JVM to know what date and time to display:

// Date correspondente ao timestamp 1553163640000
Date date = new Date(1553163640000L);
TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
System.out.println(date.getTime() + " - " + date);
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin"));
System.out.println(date.getTime() + " - " + date);
TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Samoa"));
System.out.println(date.getTime() + " - " + date);

I use TimeZone.setDefault to change the Timezone default of the JVM, and then use getTime() to display the timestamp value and also print the timestamp itself Date. The exit is:

1553163640000 - Thu Mar 21 07:20:40 BRT 2019
1553163640000 - Thu Mar 21 11:20:40 CET 2019
1553163640000 - Wed Mar 20 23:20:40 SST 2019

Note that the timestamp value has not changed, but the date and time values have been adjusted to Timezone default which is currently set. But make no mistake: these date, time and Timezone values are only a representation of the date, but the Date in itself does not have these values (the only value it has is timestamp). Another misconception is that the Date "is in a Timezone", but he has no information about it. When the date is printed, the Timezone default is only used to convert the timestamp to a date and time. But the Date itself is not in that Timezone.

That said, it takes attention to convert Date from/to the java.time. The only direct conversion that does not involve timezones is between Date and Instant, since both represent the same concept: the two classes only have the value of the timestamp (the difference, of course, is the precision, already explained above).

Conversion to other classes will always require a Timezone. Of course you can use Timezone default if you want:

// Date correspondente ao timestamp 1553163640000
Date date = new Date(1553163640000L);
// usar timezone default
ZonedDateTime zdt = date.toInstant().atZone(ZoneId.systemDefault());
// converte para LocalDate
LocalDate dt = zdt.toLocalDate();

But it is important to remember that any application can run TimeZone.setDefault and change the Timezone default, affecting all applications running on the same JVM. If you want to use a specific Timezone, be explicit in the conversion:

// usar timezone específico
ZonedDateTime zdt = date.toInstant().atZone(ZoneId.of("America/Sao_Paulo"));

You can get the list of timezones available using the method getAvailableZoneIds(). The list may vary because that information is embedded in the JVM, but it can be updated without having to change the Java version. Upgrading is important because the IANA (body responsible for maintaining the timezone information bank that Java and many other languages, systems and applications use) is always releasing new versions. This is because time zones rules are set by governments and change all the time.

Many languages and Apis have methods/functions to convert a date (only day, month and year) to a timestamp and vice versa, but in the background they are only using some arbitrary time and Timezone (they usually use "midnight" in Timezone default of its respective configuration) and "hiding this complexity" from you (some even allow you to change the Timezone, but it is not always something trivial, while others do not allow such a change).

The java.time, In turn, it is more explicit and requires that you always indicate some Timezone. On the one hand it may seem like an unnecessary "bureaucracy", but on the other it allows you to use different timezones, ensuring more flexibility, control and more correct results. Hiding this complexity would make the API more "simple", on the other hand it would give the wrong idea (that many Apis pass) that a date (only day, month and year) can be "magically" converted to a timestamp (without knowing the time and Timezone, such a conversion is not possible).


An important detail is that the class TimeZone does not validate Timezone name:

System.out.println(TimeZone.getTimeZone("nome que não existe"));

When the name does not exist, an instance corresponding to UTC is returned:

sun.util.Zoneinfo[id="GMT",offset=0,dstSavings=0,useDaylight=false,Transitions=0,lastRule=null]

Note that the offset is zero and there is no daylight saving time (dstSavings=0). That is, it is the same as UTC. Therefore, typos can pass beaten and will only be noticed when starting to appear wrong dates. Already ZoneId does not accept names that do not exist:

ZoneId.of("nome que não existe");

This code throws an exception:

java.time.Datetimeexception: Invalid ID for Region-based Zoneid, invalid format: name that does not exist

Another detail is that TimeZone accepted abbreviations:

System.out.println(TimeZone.getTimeZone("IST"));

The problem is that abbreviations are ambiguous and do not represent timezones in fact (see more details on wiki of the tag Timezone, in the section "Abbreviations"). "IST", for example, is used in India, Ireland and Israel, so which of these is returned?

sun.util.Zoneinfo[id="IST",offset=19800000,dstSavings=0,useDaylight=false,Transitions=7,lastRule=null]

In this case the offset is 19800000 milliseconds, which corresponds to 5 and a half hours. Therefore, it corresponds to India’s Timezone (as they currently use offset +05:30).

ZoneId, in turn, does not accept abbreviations, so ZoneId.of("IST") spear one java.time.zone.ZoneRulesException: Unknown time-zone ID: IST.

These details are important when migrating from one API to another, as it is not enough to pass the same names/abbreviations as a parameter. If the code uses abbreviations, you will have to make a decision about them and use a specific Timezone name (Asia/Kolkata to India, Asia/Jerusalem to Israel or Europe/Dublin for Ireland, for example).


java.sql

The classes of the package java.sql (Date, Time and Timestamp) inherit from java.util.Date, and therefore they also have their main characteristic: they do not represent a single date and time value, but a timestamp. That’s why they’re also affected by Timezone default jvm:

TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
LocalDate date = LocalDate.of(2018, 1, 1); // 1 de janeiro de 2018
java.sql.Date sqlDate = java.sql.Date.valueOf(date);
System.out.println("LocalDate=" + date + ", sqlDate=" + sqlDate);

// mudar o timezone default
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println("LocalDate=" + date + ", sqlDate=" + sqlDate);

The exit is:

Localdate=2018-01-01, sqlDate=2018-01-01
Localdate=2018-01-01, sqlDate=2017-12-31

Note that after I changed Timezone default the value of sqlDate apparently changed.

This is because java.sql.Date.valueOf day, month and year of LocalDate, together with "midnight on Timezone default of JVM" and gets the corresponding timestamp. In the above example, Timezone default is America/Sao_Paulo, then the timestamp (obtained with sqlDate.getTime()) is 1514772000000, which in fact corresponds to midnight of the day 01/01/2018 in São Paulo. Only that same timestamp corresponds to 31/12/2018 18h in Los Angeles. That’s why by changing the Timezone default for America/Los_Angeles the sqlDate is shown with the value "wrong".

It’s the same as with java.util.Date: the internal value of the timestamp does not change, but when printing the date, the method toString() uses the Timezone default to know what date/time values will be shown.

The classes java.sql.Time and java.sql.Timestamp also suffer from these same problems, as both are subclasses of java.util.Date.


The method valueOf is also affected by Timezone default:

TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
LocalDate date = LocalDate.of(2018, 1, 1); // 1 de janeiro de 2018
java.sql.Date sqlDate = java.sql.Date.valueOf(date);
System.out.println("LocalDate=" + date + ", sqlDate=" + sqlDate);
System.out.println(sqlDate.getTime());

TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
sqlDate = java.sql.Date.valueOf(date); // recriar o sqlDate, com o mesmo LocalDate
System.out.println("LocalDate=" + date + ", sqlDate=" + sqlDate);
System.out.println(sqlDate.getTime());

Notice that now I’m recreating the sqlDate with valueOf, with a Timezone default different. Now the output is:

Localdate=2018-01-01, sqlDate=2018-01-01
1514772000000
Localdate=2018-01-01, sqlDate=2018-01-01
1514793600000

The date now looks "correct", but note that the created timestamp was different. This is because the method valueOf always use midnight on Timezone default that is set at the time it is called. If any other application running on the same JVM call TimeZone.setDefault, or if someone defaults the JVM or server time zone, this code will be affected.

But see that the LocalDate always keeps the same value, because this class has only the numerical values of the day, month and year, without any information about schedules or timezones. Therefore, its value remains unchanged, regardless of which Timezone default.


If the database you are using has a driver compatible with the JDBC 4.2, it is possible to work directly with the classes of the java.time, using the methods setObject class java.sql.PreparedStatement and getObject class java.sql.ResultSet. An example with Instant would be:

Instant instant = ...
PreparedStatement ps = ...
// seta o java.time.Instant
ps.setObject(1, instant);

// obter o Instant do banco
ResultSet rs = ...
Instant instant = rs.getObject(1, Instant.class);
// converter o instant para um timezone
ZonedDateTime zdt = instant.atZone(ZoneId.of("America/Sao_Paulo"));
...

Just remembering that not all databases support all types of java.time. See the documentation and see which classes are mapped to which types in the database.


Alternatives to Java < 8

For Java 6 and 7 there is the Threeten Backport, an excellent backport of java.time, created by Stephen Colebourne (the same creator of the API java.time, inclusive).

Most features of Java 8 are present, with some differences:

  • instead of being in the package java.time, classes are in the package org.threeten.bp

  • conversion methods as Date.toInstant() and Date.from(Instant) only exist in Java >= 8, but backport has the class org.threeten.bp.DateTimeUtils to make these conversions. Examples:

    // Java >= 8, java.util.Date de/para java.time.Instant
    Date date = new Date();
    Instant instant = date.toInstant();
    date = Date.from(instant);
    
    // Java 6 e 7 (ThreeTen Backport), java.util.Date de/para org.threeten.bp.Instant 
    Date date = new Date();
    Instant instant = DateTimeUtils.toInstant(date);
    date = DateTimeUtils.toDate(instant);
    

The class DateTimeUtils also has conversion methods between java.sql.Date and java.time.LocalDate, java.util.TimeZone for java.time.ZoneId, etc. Basically, there is an equivalent for each conversion method that was added in Java 8. See the documentation for more details.

Another difference is that in Java 8 you can use the syntax of method Reference, while in the backport constants have been created that simulate this behavior (since the method Reference does not exist in Java <= 7):

// Java >= 8, usando method reference (LocalDate::from)
DateTimeFormatter parser = DateTimeFormatter.ofPattern("dd/MM/uuuu");
LocalDate date = parser.parse("20/10/2019", LocalDate::from);

// Java 6 e 7 (ThreeTen Backport), usando LocalDate.FROM para simular o method reference LocalDate::from
DateTimeFormatter parser = DateTimeFormatter.ofPattern("dd/MM/uuuu");
LocalDate date = parser.parse("20/10/2019", LocalDate.FROM);

For Android the java.time is also available (here has instructions to use it), but if you want to use Threeten Backport, at this link has the instructions to use it.


And for Java 5, there is the "predecessor of java.time" (also created by Stephen Colebourne): the Joda-Time. Despite being a terminated project (on its own website there’s a warning about this), if you are still attached to Java 5 and want to use something better than Date and Calendar, is a good alternative.

Joda-Time is not 100% identical to java.time, but many of its concepts and ideas have been taken advantage of in Java 8 (including some classes and methods have the same names). The main similarities and differences between Apis are explained here and here.


This is just a summary, because covering the entire API would be too long and the focus here was on the question ("How migrate for the new API"). You can see more information about the API in the Oracle tutorial.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.