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.
Related: https://answall.com/q/413649/112052
– hkotsubo