How to migrate from Date and Calendar to the new dates API in Java 8?

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 introduced a new date API. How can it be used? How do I integrate it with the Date and Calendar that are used in existing and legacy code? What's his relationship with Joda-Time?

Author: hkotsubo, 2017-01-12

2 answers

Legacy API problems

In The class java.util.Date and java.util.Calendar, as well as all subclasses java.util.GregorianCalendar, java.sql.Date, java.sql.Time and java.sql.Timestamp, they are well-known for being a poorly-architected for the classes that are difficult to use due to the fact that the API for them to have been ill-prepared. They work properly if used with due care, but their code accumulates several bad programming practices and problems recurring that disrupt the lives of programmers in Java.

Also, these classes are all mutable, which makes them inappropriate to be used 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 with these classes is that they are not thread-safe. Since these classes are changeable, this until it is expected. In cases where they do not mutate while they are being used, this should not cause problems regarding thread-safety for most of these classes. But with class SimpleDateFormat, the situation is different. Sharing an instance of SimpleDateFormat between multiple threads will cause unpredictable results even if the instance of SimpleDateFormat does not undergo external changes/mutations. This is because during the process of parse or formatting a date, the class SimpleDateFormat changes the internal state of itself.

That's why in Java 8, new classes have been crafted 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. In addition, their API was well planned, discussed, and exercised to stay consistent.

The most commonly used classes are the following:

  • LocalDate - represents a date with no time or zone information time.

  • LocalTime - represents a time with no date or time zone information.

  • OffsetTime - represents a time without date information, but with a fixed time zone (does not take into account Daylight Saving Time).

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

  • ZonedDateTime - represents a date and time with time zone that takes into account Daylight Saving Time.

  • OffsetDateTime - represents a date and time with a fixed time zone (does not take into account Daylight Saving Time).

  • 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, with precision of nanoseconds.

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

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

It is also noted that all of them have a static method now() that constructs the object of the corresponding class according to the time of the system. For example:

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

Time Zones are represented by the class ZoneId. Instance which corresponds to the ZoneId of the local machine can be obtained with the ZoneId.systemDefault(). Another way to get instances of ZoneId is through 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, i.e. 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 a instance of one of the classes of the package java.time, we can do 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 code above, the time zone used is important. Normally you will use ZoneId.systemDefault() or ZoneId.of("Z"), depending on what you are doing. In some cases, you may want to use some other different time zone. If you want to store the time zone in some variable (possibly static) and always (re)use it afterwards, no problem (even it is recommended in many case).

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 is the substitute 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 the static methods parse(String, DateTimeFormatter) that each of these classes has. By example:

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

One detail to watch out for is the use of uuuu instead of yyyy in the ofPattern method. The reason for this is that yyyy does not work in case of dates before Christ. Rarely would this matter, but where it doesn't matter, the two work the same, and where it matters, the uuuu should be used. Therefore, it doesn't make much sense to use yyyy to the detriment of uuuu. More details in this answer .

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

Conversion from Calendar

The legacy class GregorianCalendar is for all intents and purposes equivalent to the new class ZonedDateTime. The methods GregorianCalendar.from(ZonedDateTime) e GregorianCalendar.toZonedDateTime() serve to do the direct conversion:

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

Then having the conversion from Calendar to ZonedDateTime and vice versa, use the methods already described above if you want to get any of the objects of the new API such as LocalDate or LocalTime.

If what you have is an instance of Calendar instead of GregorianCalendar, you can almost always do a cast to GregorianCalendar to use the toZonedDateTime() method. If you do not want to use cast , you can convert Calendar to Instant:

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

It is also possible to construct a Calendar from a Instant using Date as an intermediate:

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

About Joda-Time

As for 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 java.time package and all the classes there are heavily inspired by Joda-Time, although there are some important differences that aim not to repeat some of Joda-Time's mistakes.

 30
Author: Victor Stafusa, 2018-08-22 19:24:29

Complementing the answer of Victor , there are a few more points to watch out for when migrating from one API to another. In the text below I sometimes refer to java.time as "new API" (although it was released in 2014) and the Date, Calendar and other classes like "legacy API"(because that's the term used in the Oracle tutorial).


Accuracy

java.util.Date e java.util.Calendar have millisecond accuracy (3 decimal places in the fraction of seconds), while the package classesjava.time they have nanosecond accuracy (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). So when converting this Date back to Instant, it no longer has these houses decimal.

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

If you want to restore the original value of fractions of a second, 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 with 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

Notice that in String the time is "10: 20:40", but the output was "10:22:43". This is because, according to the documentation , the letter S corresponds to milliseconds. Putting 6 letters S, as we did, does not cause the excerpt 123456 to be interpreted as microseconds (which that's what this value actually represents). Instead, the SimpleDateFormat interprets as 123456 milliseconds, which in turn corresponds to "2 minutes, 3 seconds and 456 milliseconds" - and this value is added to the time obtained. So the result is 10:22:43 (whether this algorithm makes sense or not is another story, but the fact is that SimpleDateFormat it does many other strange things besides that ).

In the above case, when printing the Date, internally the 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 (IE 456 microseconds), and in fact 456 is the value of milliseconds (since Date has no microsecond accuracy), so it should be shown as .456 (or 456000, since the format indicates 6 digits). But when putting 6 letters S, the documentation says that values numbers are filled with zeros on the left if the value has fewer digits than the number of letters. That's 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 just don't work. One way to solve is to simply treat the decimal places 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 fractions of a second have been interpreted correctly. This is because in the new API the letter S means "fractions of a second" (and no more milliseconds), and can interpret up to 9 decimal places. This brings us to another important point.


Formatting and Parsing

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

  • S has a slightly different meaning and functioning: in the legacy API it gives wrong results when it has more than 3 decimal places
  • in java.time I used u for the year instead of y (and the answer from Victor already explains the difference very well)

This is an important point: just because a format worked with SimpleDateFormat, doesn't mean it will work the same way with DateTimeFormatter. The letter u, by for example, it means "year" in java.time, but in 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 the documentation for more details (and even in the legacy API there are some differences, such as the letter X, which was only added in Java 7 - see that in the documentation of Java 6 it does not exist).

In addition, there are more options for formatting and parsing. For example, the format we are using in the examples above (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 predefined constantDateTimeFormatter.ISO_LOCAL_DATE_TIME. The difference for the previous example is that using .SSSSSS, it can only interpret Strings that have exactly 6 decimal places. ISO_LOCAL_DATE_TIME is more flexible, as it allows from zero to 9 decimal places.

We can simulate this behavior (having a field with a variable amount 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 in the example above the final excerpt with LocalDate. This class only has the day, month and year, but to get it from a String that contains date and time, I had to use the same parser. This is because the parser must be able to interpret the String integer, even if some fields are not used afterwards. That is, the parser interprets the String and the LocalDate takes only what it needs (Day, Month and year), discarding the rest.

See also that the parser is capable of interpreting both Strings without a fraction of seconds and with 9 decimal places. DateTimeFormatterBuilder has many options that are not possible to do with SimpleDateFormat, so the migration from one to another is not so direct (it is not enough to copy and paste the same format and find that everything will work the same way, and the new API still gives you more options and better alternatives to get the same results).


Modes of parsing

Detailing a little more the modes of parsing (which the answer from Victor mentions), in java.time there are three:

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

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

Or mode STRICT it is the most restricted: it 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 give error (throw a DateTimeParseException).

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


Dates and timezones

A Date, despite the name, does not represent a date - in the sense of representing only a single value of 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 by his turn is " January 1, 1970, at midnight on UTC ").

The detail of the timestamp is that it is the same worldwide, but the corresponding date and time can change depending on where you are. For example, the timestamp 1553163640000 corresponds to:

  • March 21, 2019 at 07:20: 40 am São Paulo
  • March 21, 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, it uses the timezone default which is set in the JVM to know which 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 show the timestamp value and also print the Date itself. The output 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 set to the timezone default that is currently set. But make no mistake: These date, time, and timezone values are just a representation of the date, but Date itself does not have these values (the only value it has is timestamp). Another misconception is that Date " is in a timezone", but he has no information about it. When the date is printed, the timezone default is used only 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 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 timestamp value (the difference, of course, it is the accuracy, already explained above).

Conversion to the 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 that are 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 available timezones using the method getAvailableZoneIds(). The list may vary because this information is embedded in the JVM, but it can be updated without having to change the Java version. The update is important because Iana (the body responsible for maintaining the timezone database that Java and many other languages, systems and applications use) is always releasing new versions. This happens because the time zone rules are defined by governments and change all the time .

Many languages and APIs have methods/functions to convert a date (day, month, and year only) to a timestamp and vice versa, but deep down they are just using some arbitrary time and timezone (they usually use "midnight" in the timezone default of their respective configuration) and "hiding this complexity" from you (some even allow you to change the timezone, but, while others do not even allow such a change).

The java.time, in turn, is more explicit and requires you to always indicate some timezone. On the one hand it may seem like an unnecessary "bureaucracy", but on the other hand 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 would pass the wrong idea (that many APIs pass) that a date (only day, month and year) it can be "magically" converted to a timestamp(being that, without knowing the time and timezone, such a conversion is not possible).


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

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

When the name does not exist, an instance that matches UTC is returned:

Sun.useful.calendar.ZoneInfo[id="GMT",offset=0,dstSavings=0,useDaylight=false, transitions=0, lastRule=null]

Notice 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 beats and will only be noticed when wrong dates begin to appear. 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 accepts abbreviations:

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

A problem is that abbreviations are ambiguous and do not actually represent timezones (see more details on the Wiki of the tag timezone, in the "abbreviations"section). "IST", For example, is used in India, Ireland and Israel, so which of these is returned?

Sun.useful.calendar.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 the timezone from India (as they currently use the offset +05:30).

ZoneId, in turn, it does not accept abbreviations, so ZoneId.of("IST") throws a 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 for India, Asia/Jerusalem for Israel or Europe/Dublin for Ireland, for example example).


java.sql

The classes of the package java.sql (Date, Time e Timestamp) they inherit from java.util.Date, and therefore also have their main characteristic: they do not represent a single date and time value, but a timestamp. Therefore they are also affected by the timezone default of the 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 output is:

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

Notice that after I changed the timezone default the value of sqlDate apparently changed.

This happens because java.sql.Date.valueOf it takes the day, month and year of the LocalDate, joins with "midnight on timezone default from the JVM" and gets the corresponding timestamp. In the example above, the timezone default is America/Sao_Paulo, so 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 at 18h in Los Angeles . So when changing the timezone default to America/Los_Angeles the sqlDate is shown with the value "wrong".

Is the same as with java.util.Date: the internal timestamp value does not change, but when printing the date, the toString() method uses the timezone default to know which date/time values will be shown.

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


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 I am now recreating the sqlDate with valueOf, with a different timezone default. Now the output is:

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

A date now looks "correct", but notice that the timestamp created was different. This is because the valueOf method always uses midnight on the timezone default which is set at the time it is called. If any other application running on the same JVM calls TimeZone.setDefault, or if someone disfigures the JVM or server time zone, this code will be affected.

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


If the database you are using has a driver compatible with JDBC 4.2 , you can work directly with java.time classes, using the setObject methods of the class java.sql.PreparedStatement and getObject from class java.sql.ResultSet. An example with Instant series:

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.


Java alternatives

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

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

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

  • Conversion methods such as Date.toInstant() e Date.from(Instant) only exist in Java > = 8, but the 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 e 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 details.

Another difference is that in Java 8 one can use the syntax of method reference, while in backport constants have been created that simulate this behavior (since method reference does not exist in Java

// 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 o java.time it is also available (here has instructions to use it), but if you want to use ThreeTen Backport, at this link has 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 your own website there is a warning about this ), if you are still stuck with Java 5 and want to use something better than Date and Calendar, it's a good alternative.

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


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

 12
Author: hkotsubo, 2020-10-07 17:11:15