java.util.Datehas no timezone information。它只有一个 long 值,它是自 unix 纪元以来的毫秒数(即 1970-01-01T00:00:00Z 或 1970 年 1 月 1 日st UTC 午夜)。
这个值(自纪元以来的毫秒数)有many names,我简称为timestamp。
时间戳是一个“绝对”值,在世界各地都是一样的。
示例:我刚刚得到当前时间 (System.currentTimeMillis()),结果是 1507722750315(即当前数字自纪元以来的毫秒数)。这个价值对世界上的每个人都是一样的。但是这个相同的值可以对应每个时区的不同日期/时间:
- 在UTC中,它对应于
2017-10-11T11:52:30.315Z
- 在圣保罗(巴西),它是
2017-10-11T08:52:30.315-03:00
- 在伦敦,它是
2017-10-11T12:52:30.315+01:00
- 在东京,它是
2017-10-11T20:52:30.315+09:00
- 在印度,它是
2017-10-11T17:22:30.315+05:30
- 在奥克兰,它是
2017-10-12T00:52:30.315+13:00(那里已经是“明天”了)
- 等等……
因此,当您执行接收java.util.Date 并尝试将其转换为另一个java.util.Date 的方法时,您没有转换任何内容。 Date 仅包含时间戳值(对应于特定时间点的“绝对”值)。但是Date本身对时区一无所知,所以做这样的转换是没有意义的(时间戳全世界都是一样的,没必要转换)。
“但是当我打印日期时,我看到的是本地时间的值”
嗯,这是解释in this article。如果你 System.out.println 和 Date,或者记录它,或者在调试器中查看它的值,你会看到类似:
2017 年 10 月 12 日星期三 12:33:99 IST
但这是因为 toString() 方法被隐式调用,并且此方法将时间戳值转换为 JVM 默认时区(因此它看起来像“本地日期/时间”)。
但请记住,这个Wed Oct etc 字符串不是由Date 对象持有的实际值:它所拥有的只是与显示的日期/时间相对应的时间戳。但是日期对象本身没有任何本地日期/时间的概念,也没有任何与时区相关的信息。您所看到的只是特定时区的时间戳值的表示,采用特定格式。
那该怎么办?
如果数据库字段是 date 类型,则 JDBC 驱动程序通常会处理它,因此您只需直接保存 Date 对象即可。无需关心格式和时区,因为Date 没有格式,也没有任何时区信息。
日期类型的问题是任何语言/数据库/供应商/应用程序都以不同的方式显示它。 Java 的 Date.toString() 将其转换为 JVM 默认时区,一些数据库以特定格式(dd/mm/yyyy 或 yyyy-mm-dd 等)显示并转换为数据库中配置的任何时区,等等。
但请记住,日期本身没有格式。它只有一个值,问题是相同的日期可以以不同的格式表示。示例:October 15th 2017 可以表示为2017-10-15、10/15/2017、15th Oct 2017、15 de Outubro de 2017(在 pt_BR(葡萄牙语)语言环境中)等等。格式(representation)不同,但Date 值是一样的。
所以,如果你在处理Date对象,数据库字段是一个与日期相关的类型,它返回并保存Date对象,直接使用就行了。
如果您想显示这个日期,(显示给用户,或记录它),或者如果数据库字段是 String(或 varchar,或任何其他文本-related 类型),那么就完全不同了。
打印日期(或将其转换为String)时,您可以使用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
在这种情况下,输出是:
2017-10-11 17:22:30
如果我将时区更改为另一个时区,结果将转换为它:
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
现在结果是:
2017-10-11 12:52:30
但是Date的值还是一样的:如果你调用date.getTime(),它会返回时间戳值,它仍然是1507722750315。我只是更改了此日期的表示(格式和时区)。但Date 本身的值不变。
还请注意,我使用了IANA timezones names(始终采用Region/City 的格式,例如Asia/Kolkata 或Europe/Berlin)。
避免使用三个字母的缩写(如IST 或CET),因为它们是ambiguous and not standard。
您可以致电TimeZone.getAvailableIDs() 获取可用时区列表(并选择最适合您系统的时区)。
你也可以使用系统的默认时区TimeZone.getDefault(),但是这个can be changed without notice, even at runtime,所以最好明确地使用一个特定的。
要从String 获取Date,请勿使用已弃用的构造函数 (new Date("09/10/2017 21:53:17"))。最好使用SimpleDateFormat 并设置此日期所指的时区。
在我所做的测试中,09/10/2017 被解释为“月/日/年”,所以我将使用相同的。我也在考虑输入是 UTC,但您可以将其更改为您需要的任何时区:
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);
上述日期相当于 2017 年 9 月 10 日th 21:53:17,UTC。如果输入在另一个时区,只需将“UTC”更改为相应的时区名称即可。
Check the javadoc 用于SimpleDateFormat 使用的所有可能格式。
Java 新的日期/时间 API
旧的类(Date、Calendar 和 SimpleDateFormat)有 lots of problems 和 design issues,它们正在被新的 API 取代。
如果您使用的是 Java 8,请考虑使用 new java.time API。更简单,less bugged and less error-prone than the old APIs。
如果您使用的是 Java 6 或 7,则可以使用 ThreeTen Backport,这是 Java 8 新日期/时间类的一个很好的向后移植。对于Android,您还需要ThreeTenABP(详细了解如何使用它here)。
下面的代码适用于两者。
唯一的区别是包名(在 Java 8 中是 java.time,在 ThreeTen Backport(或 Android 的 ThreeTenABP)中是 org.threeten.bp),但类和方法 names 是相同的。
最新的 JDBC 驱动程序已经支持新的 Java 8 类型(检查您是否已经支持)。但如果您仍然需要使用java.util.Date,您可以轻松地从/转换到新的 API。
在 Java 8 中,Date 具有新的 from 和 toInstant 方法,用于从/转换为新的 java.time.Instant 类:
Date date = Date.from(instant);
Instant instant = date.toInstant();
在 ThreeTen Backport 中,您可以使用 org.threeten.bp.DateTimeUtils 类从/到 org.threeten.bp.Instant 进行转换:
Date date = DateTimeUtils.toDate(instant);
Instant instant = DateTimeUtils.toInstant(date);
通过Instant,您可以轻松地转换为您想要的任何时区(使用ZoneId 类将其转换为ZonedDateTime),然后使用DateTimeFormatter 更改格式:
// 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
解析String 是类似的。您使用DateTimeFormatter,然后解析为LocalDateTime(因为输入String 中没有时区),然后将其转换为时区(您可以选择将其转换为Date如果需要):
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());
如果要转换为 UTC,请使用常量 ZoneOffset.UTC 而不是 ZoneId。此外,check the javadoc 了解有关格式的更多详细信息(SimpleDateFormat 使用的格式并不总是相同)。