A java.util.Date
has no timezone information. It has only a long
value, which is the number of milliseconds since unix epoch (which is 1970-01-01T00:00:00Z
or January 1st 1970 at midnight in UTC).
This value (number of milliseconds since epoch) has many names, I'll just call it timestamp for short.
The timestamp is an "absolute" value, it's the same everywhere in the world.
Example: I've just got the current time (System.currentTimeMillis()
) and the result was 1507722750315
(that's the current number of milliseconds since epoch). This value is the same for everyone in the world. But this same value can correspond to a different date/time in each timezone:
- in UTC, it corresponds to
2017-10-11T11:52:30.315Z
- in São Paulo (Brazil), it's
2017-10-11T08:52:30.315-03:00
- in London, it's
2017-10-11T12:52:30.315+01:00
- in Tokyo, it's
2017-10-11T20:52:30.315+09:00
- in India, it's
2017-10-11T17:22:30.315+05:30
- in Auckland, it's
2017-10-12T00:52:30.315+13:00
(it's already "tomorrow" there)
- and so on...
So, when you do a method that receives a java.util.Date
and tries to convert it to another java.util.Date
, you're not converting anything. The Date
contains only the timestamp value (the "absolute" value that corresponds to a specific point in time). But the Date
itself doesn't know anything about timezones, so it's pointless to do such convertion (the timestamp is the same for the whole world, there's no need to convert it).
"But when I print the date, I see the value in my local time"
Well, this is explained in this article. If you System.out.println
a Date
, or log it, or look its value in a debugger, you'll see something like:
Wed Oct 12 12:33:99 IST 2017
But that's because the toString()
method is being implicity called, and this method converts the timestamp value to the JVM default timezone (so it looks like a "local date/time").
But remember, this Wed Oct etc
string is not the actual value hold by the Date
object: all it has is the timestamp that corresponds to that date/time displayed. But the date object itself doesn't has any notion of local date/time, nor any timezone related information. What you see is just a representation of the timestamp value, in a specific format, for a specific timezone.
What to do then?
If the database field is a date type, then the JDBC driver usually handles it, so you just save the Date
object directly. There's no need to care about formats and timezones, because the Date
has no format, nor any timezone information.
The problem with date types is that any language/database/vendor/application shows it in a different way. Java's Date.toString()
converts it to the JVM default timezone, some databases shows in a specific format (dd/mm/yyyy, or yyyy-mm-dd, etc) and converts to whatever timezone configured in the DB, and so on.
But remember that a date itself has no format. It has just a value, the thing is that the same date can be represented in different formats. Example: October 15th 2017 can be represented as 2017-10-15
, 10/15/2017
, 15th Oct 2017
, 15 de Outubro de 2017
(in pt_BR (portuguese) locale) and so on. The format (the representation) is different, but the Date
value is the same.
So, if you're dealing with Date
objects, the database field is a date-related type and it returns and saves the Date
object, just use it directly.
If you want to display this date, though, (show to an user, or log it), or if the database field is a String
(or a varchar, or any other text-related type), then it's a completely different matter.
When printing a date (or converting it to a String
), you can choose the format and the timezone you're converting to, using a SimpleDateFormat
:
// a java.util.Date with the same timestamp above (1507722750315)
Date date = new Date(1507722750315L);
// choose a format
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// choose a timezone to convert to
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata"));
// convert to a String
String formattedDate = sdf.format(date);
System.out.println(formattedDate); // 2017-10-11 17:22:30
In this case, the output is:
2017-10-11 17:22:30
If I change the timezone to another one, the result will be converted to it:
sdf.setTimeZone(TimeZone.getTimeZone("Europe/London"));
// convert to a String
String formattedDate = sdf.format(date);
System.out.println(formattedDate); // 2017-10-11 12:52:30
Now the result is:
2017-10-11 12:52:30
But the Date
value is still the same: if you call date.getTime()
, it'll return the timestamp value, which is still 1507722750315
. I just changed the representation of this date (the format and the timezone). But the Date
itself has its value unchanged.
Also note that I used IANA timezones names (always in the format Region/City
, like Asia/Kolkata
or Europe/Berlin
).
Avoid using the 3-letter abbreviations (like IST
or CET
) because they are ambiguous and not standard.
You can get a list of available timezones (and choose the one that fits best your system) by calling TimeZone.getAvailableIDs()
.
You can also use the system's default timezone with TimeZone.getDefault()
, but this can be changed without notice, even at runtime, so it's better to explicity use a specific one.
To get a Date
from a String
, don't use the deprecated constructor (new Date("09/10/2017 21:53:17")
). It's better to use a SimpleDateFormat
and set the timezone this date refers to.
In the tests I've made, 09/10/2017
is interpreted as "month/day/year", so I'm going to use the same. I'm also considering that the input is in UTC, but you can change it to whatever timezone you need:
String s = "09/10/2017 21:53:17";
SimpleDateFormat parser = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
// the input is in UTC (change it to the timezone you need)
parser.setTimeZone(TimeZone.getTimeZone("UTC"));
Date d = parser.parse(s);
The date above will be equivalent to September 10th 2017, at 21:53:17 in UTC. If the input is in another timezone, just change "UTC" by the corresponding timezone name.
Check the javadoc for all possible formats used by SimpleDateFormat
.
Java new Date/Time API
The old classes (Date
, Calendar
and SimpleDateFormat
) have lots of problems and design issues, and they're being replaced by the new APIs.
If you're using Java 8, consider using the new java.time API. It's easier, less bugged and less error-prone than the old APIs.
If you're using Java 6 or 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. And for Android, you'll also need the ThreeTenABP (more on how to use it here).
The code below works for both.
The only difference is the package names (in Java 8 is java.time
and in ThreeTen Backport (or Android's ThreeTenABP) is org.threeten.bp
), but the classes and methods names are the same.
The newest JDBC drivers already have support to new Java 8 types (check if yours already has). But if you still need to use java.util.Date
, you can easily convert from/to the new API.
In Java 8, Date
has the new from
and toInstant
methods, to convert from/to the new java.time.Instant
class:
Date date = Date.from(instant);
Instant instant = date.toInstant();
In ThreeTen Backport, you can use the org.threeten.bp.DateTimeUtils
class to convert from/to org.threeten.bp.Instant
:
Date date = DateTimeUtils.toDate(instant);
Instant instant = DateTimeUtils.toInstant(date);
By having an Instant
, you can easily convert to whatever timezone you want (using the ZoneId
class to convert it to a ZonedDateTime
), and then using a DateTimeFormatter
to change the format:
// convert Date to Instant, and then to a timezone
ZonedDateTime z = date.toInstant().atZone(ZoneId.of("Asia/Kolkata"));
// format it
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = fmt.format(z);
System.out.println(formatted); // 2017-10-11 17:22:30
To parse a String
is similar. You use a DateTimeFormatter
, then parse to a LocalDateTime
(because the input String
doesn't have a timezone in it), then you convert it to a timezone (and you can optionally convert it to a Date
if you need):
String input = "09/10/2017 21:53:17";
DateTimeFormatter parser = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");
// parse the input
LocalDateTime dt = LocalDateTime.parse(input, parser);
// convert to a timezone
ZonedDateTime z = dt.atZone(ZoneId.of("Asia/Kolkata"));
// you can format it, just as above
// you can also convert it to a Date
Date d = Date.from(z.toInstant());
If you want to convert to UTC, use the constant ZoneOffset.UTC
instead of the ZoneId
. Also, check the javadoc for more details about the formats (they're not always the same used by SimpleDateFormat
).