最近在家办工接到的一项工作是和时区有关的,据用户反馈,由于美国的 Puerto Rico 州不使用夏令时,在其他州施行夏令时时,这个州的用户选不到适合自己的时区,导致时间无法正确显示。最终笔者为软件添加了太平洋标准时区解决了这个问题。
时区、夏令时、标准时间...日期和时间是计算机处理的重要数据,在绝大多数软件程序中,我们都要和日期和时间打交道。本篇文章我们将系统地学习 Java 对日期和时间的处理。(本文参考了 廖雪峰 Java 教程-日期和时间 文章中的资料,事实上,笔者并不认为本文比廖大佬的文章更好,有时间的读者可以直接阅读原教程。)
一、时区
地球人都知道,我们地球是自西向东自转的,所以东边会比西边早看到太阳,东边的时间也总比西边的快。如果全球采用统一的时间,比如都用北京时间,会产生什么问题呢?
当正午十二点的太阳照射到北京时,身处地球另一面的纽约还是漆黑一片。对于纽约来说,日常作息时间就成了晚上九点开始上班,因为那时太阳刚刚升起;所有纽约人都上班到第二天早上六点下班,因为那时太阳刚刚落下。
虽然对于长期居住在一个地方的人来说,他可以适应自己本地的作息时间,但当他去其他地方旅游或是与其他地方的人交流时,就必须查询当地的作息时间,这会带来很大的不便。
于是,在 1879 年,加拿大铁路工程师弗莱明首次提出全世界按统一标准划分“时区”。1884 年华盛顿子午线国际会议正式通过采纳这种时区划分,称为世界标准时制度。
时区划分的初衷是 尽量使中午贴近太阳上中天的时间,从此以后,各地的时间经过换算,都能统一地早上六点起床,中午十二点午餐,晚上六点下班。
全球共分为 24 个时区,所以每个时区占 15˚ 经度。理论时区 以能被 15 整除的经线为中心,向东西两侧延伸 7.5˚。国际规定经过英国格林威治天文台的那一条经线为 0˚ 经线,这条经线也被称作 本初子午线。选择格林威治既是因为当初“日不落帝国”的强大,也是由于格林威治常年提供准确的航海观测数据,19 世纪晚期,72% 的世界贸易都依靠以格林威治作为本初子午线的航海图表。
为了避开国界线,有的时区的形状并不规则,而是比较大的国家以国家内部行政分界线为时区界线,这是 实际时区,也称为 法定时区。
身处地球的不同地区,时间可能是不同的,所以光靠时间我们无法确定一个时刻,要确定一个时刻必须要带上时区。
表示时区有两种常见的写法,最常见的是 GMT,它的全称是 Greenwich Mean Time,意思是格林威治标准时间,世界各地根据东西偏移量计算时区。比如,北京位于东八区,记做 GMT+8,纽约位于西五区,记做 GMT-5。
还有一种写法是 UTC,它的全称是 Coordinated Universal Time,意思是协调世界时,如果时间以 UTC 表示,则在时间后面直接加上一个“Z”(不加空格),“Z”是协调世界时中 0 时区的标志。比如,“09:30 UTC” 写作 “09:30Z” 或是 “0930Z”。“14:45:15 UTC” 写作 “14:45:15Z” 或 “144515Z”。因为在北约音标字母中用 “Zulu” 表示 “Z”,所以 UTC 时间也被称做祖鲁时间。
GMT 和 UTC 基本一样,只不过 UTC 使用更加精确的原子钟计时,每隔几年会有一个闰秒。但我们无需关注两者的差别,计算机在联网时会自动与时间服务器同步时间。
计算不同时区的时间差很简单,我们平时常用的北京时间位于东八区,即:GMT+8,它的值是在 GMT 的基础上增加了 8 小时,纽约位于西五区,即:GMT-5,它的值是在 GMT 的基础上减少了 5 小时。所以北京时间通常比纽约时间快 13 个小时。
我们现在知道,每往西越过一个时区,时间便提前一小时。据此我们来思考一个有趣的问题:如果我们一直往西,以每小时一个时区的速度前进,时间是否会静止呢?
1.比如我们从北京出发,此时时间是 2020-2-11 8:00 GMT+8
2.当我们花费一个小时,走到东七区时,时间是 2020-2-11 8:00 GMT+7
3.当我们走到本初子午线时,时间是 2020-2-11 8:00 GMT
4.当我们走到西五区时,时间是 2020-2-11 8:00 GMT-5
......
我们都知道地球是个球体,当我们绕地球一圈回到北京时,如果时间还是 2020-2-11 8:00 GMT+8,岂不是时间真的静止了?进一步思考,如果我们以半小时一个时区的速度向西前进,岂不是时间还会倒流?
常识告诉我们,时间是不可能静止也不可能倒流的。那么这里的问题出在哪里呢?问题就出在东西时区的交界处。上文说到,地球分为 24 个时区,包括标准时区、东一区~东十二区、西一区~西十二区。实际上,东十二区和西十二区是同一时区。
从 0˚ 经线开始,每往西跨一个时区时间便减少 1 小时,每往东跨一个时区便增加 1 小时。如此一来,到了另一端 180˚ 经线时,就会有 24 小时的落差,为了平衡这一落差,人们规定由西向东越过此线日期需减少一天,由东向西越过此线时日期需增加一天。故而这一条线被称之为国际日期变更线,也叫换日线,它位于本初子午线的另一面。和时区界限类似,为了避开国界线,换日线并不与 180˚ 经线重合,换日线实际上是不规则的。
如果我们接着走下去:
5.当我们走到东 / 西十二区时,时间是 2020-2-11 8:00 GMT±12
6.我们越过国际换日线,日期增加一天,时间是 2020-2-12 8:00 GMT±12
7.当我们走到东十一区时,时间是 2020-2-12 8:00 GMT+11
8.当我们回到北京时,时间是 2020-2-12 8:00 GMT+8
此时,我们的环球之旅刚好用了 24 小时。
再来看一下如果我们以每半小时一个时区的速度向西行走,时间为什么不会逆流:
1.我们还是从北京出发,此时时间是 2020-2-11 8:00 GMT+8
2.当我们花费半小时,走到东七区时,时间是 2020-2-11 7:30 GMT+7
3.当我们走到本初子午线时,时间是 2020-2-11 4:00 GMT
4.当我们走到西五区时,时间是 2020-2-11 1:30 GMT-5
5.当我们走到东 / 西十二区时,时间是 2020-2-10 22:00 GMT±12
6.我们越过国际换日线,日期增加一天,时间是 2020-2-11 22:00 GMT±12
7.当我们走到东十一区时,时间是 2020-2-11 21:30 GMT+11
8.当我们回到北京时,时间是 2020-2-11 20:00 GMT+8
此时,我们的环球之旅刚好用了 12 小时。
二、夏令时
由于夏季和冬季白昼时间不一致,部分国家施行了夏令时制度,目的是让人们根据白昼时间来调整作息。夏令时:在夏天开始的时候,把时间往后拨 1 小时,夏天结束的时候,再把时间往前拨 1 小时。
施行夏令时使得人们可以尽量在白天工作,从而减少照明,节省电能。但夏令时也带来了很多的不便,如夏令时开始和结束时,人们不得不调整睡眠时间;夏令时也使得时间计算变得复杂,在夏令时结束的当天,某些时间会出现两次,容易造成交通、生产、会议安排等时间的混乱。中国曾经施行过一段时间夏令时,在 1992 年就被废除了,而美国大部分地区现在还在使用夏令时。
美国使用夏令时时,纽约时间按照西四区计算,即:GMT-4。这段时间北京时间比纽约时间快 12 个小时,夏令时结束后,纽约时间又恢复到西五区 GMT-5。
由于各国规定有所差异,所以夏令时计算非常复杂。当我们需要计算夏令时时,应尽量使用 Java 库提供的类,避免自己计算夏令时。
三、旧 API
Java 标准库提供了两套关于时间和日期的 API:旧 API:位于 java.util 包中,里面主要有 Date、Calendar、TimeZone 类
新 API:位于 java.time 包中,里面主要有 LocalDateTime、ZonedDateTime、ZoneId 类
有两套 API 的原因是旧 API 在设计时没有考虑好时区问题,常量设计也有些不合理,导致使用起来不够方便。新 API 很好地解决了这些问题。我们在开发时,除非维护老代码,其他时候都应该尽量使用新 API。
3.1. Date
Date 类用于存储日期和时间,查看其源码可以发现,它保存了一个 long 类型的时间戳。时间戳是指格林威治时间从 1970 年 1 月 1 日零点到此刻经历的秒数或毫秒数。
public class Date implements Serializable, Cloneable, Comparable {
private transient long fastTime;
...
}
Date 的基本用法如下:
import java.util.*;
public class Main {
public static void main(String[] args) {
// 获取当前时间 Date date = new Date();
// 年份,需要加上 1900 System.out.println(date.getYear() + 1900);
// 月份,取值范围是 0~11,代表 1~12 月,所以需要加上 1 System.out.println(date.getMonth() + 1);
// 日期,取值范围是 1~31 System.out.println(date.getDate());
// 转换为 String,如:Tue Feb 11 17:24:10 CST 2020 System.out.println(date.toString());
// 转换为 GMT 时间,如:11 Feb 2020 09:24:10 GMT System.out.println(date.toGMTString());
// 转换为本地化字符串,如:2020年2月11日 下午5:24:10 System.out.println(date.toLocaleString());
}
}
Date 在使用时有几个缺点:每次获取年份、月份都需要转换
只能获取当前时区的时间,无法设置时区
无法加减日期和时间
无法计算某个月的第几个星期几
3.2. SimpleDateFormat
默认输出的时间字符串的格式通常不能满足我们的要求,所以我们需要用 SimpleDateFormat 来格式化输出,它使用一些预定义的字符串表示格式化,较常用的字符串有:y:年
M:月
d:日
H:小时
m:分钟
s:秒
S:毫秒
a:上午 / 下午
E:星期
z:时区
附:Java 官网文档中给出的预定义字符串表格
SimpleDateFormat 的使用:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
var formatter = new SimpleDateFormat("y-M-d a H:m:s.S E z");
// 输出:2020-2-11 下午 17:26:13.776 周二 CST System.out.println(formatter.format(date));
}
}
这里的时区信息输出为 CST,表示 China Standard Time,也就是中国标准时间。
SimpleDateFormat 会根据预定义字符的长度打印不同长度的信息。以 M 为例:M:输出 2
MM:输出 02
MMM:输出 2月
MMMM:输出 二月
如果预定义字符串的长度短于需要输出的信息,这时 Java 会输出 能包含全部信息的最短字符串,也就是说 Java 不会丢弃任何信息,如上例中只用了一个 y,仍然输出了 2020,并不会只输出一个 2。
我们来发挥一下极客精神,探索一下预定义字符串过长 Java 会怎么处理:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
var formatter = new SimpleDateFormat("yyyyyyyyyy-MMMMMMMMMM-dddddddddd aaaaaaaaaa HHHHHHHHHH:mmmmmmmmmm:ssssssssss.SSSSSSSSSS EEEEEEEEEE zzzzzzzzzz");
// 输出:0000002020-二月-0000000011 下午 0000000017:0000000027:0000000008.0000000504 星期二 中国标准时间 System.out.println(formatter.format(date));
}
}
本例中,每个预定义字符的长度都为 10,可以看到,系统对年、日、时、分、秒、毫秒的处理是用前置 0 补足位数,对月份、上午 / 下午、星期、时区的处理是输出全中文。
SimpleDateFormat 可以设置时区,我们可以用 SimpleDateFormat 把 Date 获取的时间转换为其他时区显示出来:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
var formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzzzz");
// 输出:2020-02-11 17:27:33 中国标准时间 System.out.println(formatter.format(date));
formatter.setTimeZone(TimeZone.getTimeZone("America/New_York"));
// 输出:2020-02-11 04:27:33 北美东部标准时间 System.out.println(formatter.format(date));
}
}
3.3. Calendar
旧 API 中,为了加减日期和时间,Java 提供了 Calendar 类。
Calendar 的基本使用:
import java.util.*;
public class Main {
public static void main(String[] args) {
// 获取当前时间 Calendar c = Calendar.getInstance();
// 年份,不必加 1900 int y = c.get(Calendar.YEAR);
// 月份,取值范围是 0~11,代表 1~12 月,所以需要加上 1 int M = c.get(Calendar.MONTH) + 1;
// 日期,取值范围是 1~31 int d = c.get(Calendar.DAY_OF_MONTH);
int H = c.get(Calendar.HOUR_OF_DAY);
int m = c.get(Calendar.MINUTE);
int s = c.get(Calendar.SECOND);
int S = c.get(Calendar.MILLISECOND);
// 星期,取值范围 1~7,代表周日~周六 int E = c.get(Calendar.DAY_OF_WEEK);
// 输出:2020-2-11 17:28:19.364 3 System.out.println(y + "-" + M + "-" + d + " " + H + ":" + m + ":" + s + "." + S + " " + E);
}
}
Calendar 修复了 Date 获取年份时必须 + 1900 的问题,但月份仍然使用 0~11 表示 1~12 月,星期采用 1~7 表示周日~周六。虽然咱们程序员都从 0 开始计数,但日期和时间一般都是要展示给用户看的,每次显示时都要转换实在是太不方便了,这也是需要新 API 的原因之一。
Calendar 提供的日期和时间的加减功能使用如下:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 当前时间:2020-02-11 17:28:56 System.out.println(sdf.format(c.getTime()));
// 加 2 天 c.add(Calendar.DAY_OF_MONTH, 2);
// 减 5 小时 c.add(Calendar.HOUR_OF_DAY, -5);
// 计算后的时间:2020-02-13 12:28:56 System.out.println(sdf.format(c.getTime()));
}
}
使用日期加减时有一点需要特别注意,我们来看一个例子:
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
c.set(2020, 11, 31);
// 输出:Thu Dec 31 17:09:11 CST 2020 System.out.println(c.getTime());
c.add(Calendar.MONTH, -1);
c.add(Calendar.MONTH, 1);
// 输出:Wed Dec 30 17:09:11 CST 2020 System.out.println(c.getTime());
}
}
我们将 12 月 31 日减去 1 个月,再加上 1 个月,日期变成了 12 月 30 日!这是因为 11 月 没有 31 日,所以 12 月 31 日减去 1 个月时, Calendar 会自动将日期调整到 11 月 30 日,再加 1 个月,就变成了 12 月 30 日。也就是说 Calendar 加减时,会根据月份自动调整日期。
上文介绍 Date 时我们说到,单靠 Date 和 SimpleDateFormat 只能把本地时区的时间用其他时区显示出来,无法自由的实现时区的转换,比如我们身在中国,无法把纽约时间 GMT-5 转换为东京时间 GMT+9。但 Calendar 是可以设置时区的,所以我们现在有了一种间接转换任意时区的方法:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
// 清空 Calendar 获取到的本地时间 c.clear();
// 将时间重设为:2020-2-11 13:00:00 c.set(2020, 1, 11, 13, 0, 0);
// 设定为纽约时区 c.setTimeZone(TimeZone.getTimeZone("America/New_York"));
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzzzz");
// c.getTime() 转换成 Date 时,时间会转换成本地时区 Date d = c.getTime();
// 输出:2020-02-12 02:00:00 中国标准时间 System.out.println(sdf.format(d));
// 设置 SimpleDateFormat 的时区为东京时区 sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
// 输出纽约时间转换成的东京时间 // 输出:2020-02-12 03:00:00 日本标准时间 System.out.println(sdf.format(d));
}
}
实际转换过程为:Calendar 保存的 纽约时间 先转换成 Date 保存的 北京时间,再用 SimpleDateFormat 将 Date 转换成 东京时间 展示出来。
上例中还可以看到,Calendar 使用 set 方法设置指定时间,除了此例中的一次性全部指定的方式外,也可以单个指定:
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
// 输出:Tue Feb 11 17:30:20 CST 2020 System.out.println(c.getTime());
c.set(Calendar.DAY_OF_MONTH, 9);
// 输出:Sun Feb 09 17:30:20 CST 2020 System.out.println(c.getTime());
}
}
四、新 API
由于旧 API 存在的诸多不便,从 Java 8 开始,jaca.time 包提供了一套新的日期和时间的 API。主要有 LocalDateTime、ZonedDateTime、Instant、ZoneId、Duration、DateTimeFormatter。
新 API 不仅使用更方便,而且修正了 旧 API 中不合理的常量设计:新 API 中,Month 取值范围变成:1~12,表示 1~12月
新 API 中,Week 取值范围变成:1~7,表示周一~周日
4.1. LocalDateTime
LocalDateTime 用来代替 Date 和 Calendar,LocalDateTime 的基本用法如下:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 当前日期和时间 LocalDateTime dt = LocalDateTime.now();
// 严格按照 ISO 8601 格式打印,输出:2020-02-11T17:31:27.027983 System.out.println(dt);
}
}
LocalDateTime 使用 now() 函数获取当前日期和时间,输出时严格按照 ISO 8601 格式打印。ISO 8601 是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》,ISO 8601 规定使用 T 分隔日期和时间。标准格式如下:日期:yyyy-MM-dd
时间:HH:mm:ss
带毫秒的时间:HH:mm:ss.SSS
日期和时间:yyyy-MM-dd'T'HH:mm:ss
带毫秒的日期和时间:yyyy-MM-dd'T'HH:mm:ss.SSS
我们可以通过 parse() 函数解析一个符合 ISO 8601 格式的字符串,创建出 LocalDateTime:
LocalDateTime dt = LocalDateTime.parse("2020-02-11T13:00:00");
除此之外,我们还可以通过 of() 函数指定日期和时间创建 LocalDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 当前日期和时间,最后的 1 表示纳秒,可不传 LocalDateTime dt = LocalDateTime.of(2020, 2, 11, 13, 20, 30, 1);
// 输出:2020-02-11T13:20:30.000000001 System.out.println(dt);
}
}
LocalDateTime 存储了当前的日期信息和时间信息,如果我们只需要当前日期或当前时间,可以使用 LocalDate 和 LocalTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 当前日期和时间 LocalDateTime dt = LocalDateTime.now();
LocalDate d = dt.toLocalDate();
LocalTime t = dt.toLocalTime();
// 输出:2020-02-11 System.out.println(d);
// 输出:17:32:09.742114 System.out.println(t);
}
}
同 LocalDateTime 类一样,LocalDate 和 LocalTime 类也可以通过 now()、parse()、of() 方法创建。
LocalDateTime 在加减日期时,可以采用简洁的链式调用:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.of(2020, 2, 11, 13, 0, 0);
// 加 2 天,减 5 小时 LocalDateTime dt2 = dt.plusDays(2).minusHours(5);
// 输出: 2020-02-13T08:00 System.out.println(dt2);
}
}
和 Calendar 一样,LocalDateTime 在加减时,仍然会自动调整日期:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.of(2020, 12, 31, 1, 0, 0);
// 输出:2020-12-31T01:00 System.out.println(dt);
LocalDateTime dt2 = dt.minusMonths(1).plusMonths(1);
// 输出:2020-12-30T01:00 System.out.println(dt2);
}
}
与 Calendar 不同的是,LocalDateTime 是不可变类,如此例中调用 minusMonths(1) 和 plusMonths(1) 方法后,dt 的值并没有改变,这个函数返回的是一个调整后的新值,我们将这个新值赋值给了 dt2。
对应 Calendar 的 set() 方法,LocalDateTime 调整时间使用 withXxx() 方法:调整年:withYear()
调整月:withMonth()
调整日:withDayOfMonth()
调整时:withHour()
调整分:withMinute()
调整秒:withSecond()
import java.time.LocalDateTime;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.now();
// 输出:2020-02-11T17:34:17.570764 System.out.println(dt);
LocalDateTime dt2 = dt.withDayOfMonth(9);
// 输出:2020-02-09T17:34:17.570764 System.out.println(dt2);
}
}
LocalDateTime 还有一个 with() 方法允许我们做更复杂的运算:
import java.time.*;
import java.time.temporal.*;
public class Main {
public static void main(String[] args) {
// 今天 0:00 LocalDateTime startOfToday = LocalDate.now().atStartOfDay();
// 输出:2020-02-11T00:00 System.out.println(startOfToday);
// 本月最后一天 LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
// 输出:2020-02-29 System.out.println(lastDay);
// 下个月第一天 LocalDate firstDayOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
// 输出:2020-03-01 System.out.println(firstDayOfNextMonth);
// 本月第1个周一 LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
// 输出:2020-02-03 System.out.println(firstWeekday);
}
}
要比较两个日期的先后,可以使用 LocalDateTime 的 isBefore()、isAfter() 方法:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.now();
LocalDateTime dt2 = dt.plusDays(2);
// 输出: true, true System.out.println(dt.isBefore(dt2) + ", " + dt2.isAfter(dt));
}
}
4.2. ZonedDateTime
LocalDateTime 和 Date 类一样,总是表示本地时区的时间,如果要表示带时区的时间,需要使用 ZonedDateTime,它相当于 LocalDateTime + ZoneId。LocalDateTime 提供的方法,如 now()、of()、plusDays() 等,ZonedDateTime 也都提供。
ZonedDateTime 的使用:
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime date = ZonedDateTime.now();
ZonedDateTime dateEST = ZonedDateTime.now(ZoneId.of("America/New_York"));
// 输出: 2020-02-11T17:35:27.191452+08:00[Asia/Shanghai] System.out.println(date);
// 输出: 2020-02-11T04:35:27.193329-05:00[America/New_York] System.out.println(dateEST);
}
}
ZonedDateTime 通过 now() 函数获取当前时区的时间,通过 now(ZoneId zone) 函数获取指定时区的时间。这样获取到的两个时间虽然时区不同,但表示的都是同一时刻(毫秒数不同是由于执行代码会花费一点时间)。
通过给 LocalDateTime 设置 ZoneId,也可以创建出 ZonedDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.now();
ZonedDateTime zdt = dt.atZone(ZoneId.systemDefault());
ZonedDateTime zdt2 = dt.atZone(ZoneId.of("America/New_York"));
// 输出: 2020-02-11T17:35:50.246180+08:00[Asia/Shanghai] System.out.println(zdt);
// 输出: 2020-02-11T17:35:50.246180-05:00[America/New_York] System.out.println(zdt2);
}
}
通过这种方式创建的 ZonedDateTime 日期和时间一样,但时区不同,所以表示的是两个不同时刻。
ZonedDateTime 可以通过 toLocalDateTime() 函数转换成 LocalDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));
LocalDateTime ldt = zdt.toLocalDateTime();
// 输出: 2020-02-11T04:36:16.839591-05:00[America/New_York] System.out.println(zdt);
// 输出: 2020-02-11T04:36:16.839591 System.out.println(ldt);
}
}
我们看到,ZonedDateTime 转换成 LocalDateTime 时,不会自动切换成本地时区的时间,而是直接丢弃时区信息。
由于 ZonedDateTime 自带时区信息,所以在涉及时区转换时使用 ZonedDateTime 非常方便。如上文中提到的将纽约时间转换成的东京时间,使用 ZonedDateTime 实现如下:
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.of(2020, 2, 11, 13, 0, 0, 0, ZoneId.of("America/New_York"));
// 使用 withZoneSameInstant() 方法切换时区 ZonedDateTime zdt2 = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
// 输出: 2020-02-11T13:00-05:00[America/New_York] System.out.println(zdt);
// 输出: 2020-02-12T03:00+09:00[Asia/Tokyo] System.out.println(zdt2);
}
}
4.3. DateTimeFormatter
上文已经说到,DateTimeFormatter 是用来代替 SimpleDateFormat 的。与 SimpleDateFormat 相比,DateTimeFormatter 的一个明显优势在于它是线程安全的。SimpleDateFormat 由于不是线程安全的,使用时只能在方法内部创建新的局部变量,而 DateTimeFormatter 可以只创建一个实例。
DataTimeFormat 预定义的字符串和 SimpleDateFormat 一模一样,来看下 DateTimeFormatter 的基本使用:
import java.time.*;
import java.time.format.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime date = ZonedDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
// 输出: 2020-02-11 17:37 System.out.println(formatter.format(date));
}
}
还记得 LocalDateTime 的 parse() 方法吗?我们查看一下它的源码:
public static LocalDateTime parse(CharSequence text) {
return parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
public static LocalDateTime parse(CharSequence text, DateTimeFormatter
formatter) {
Objects.requireNonNull(formatter, "formatter");
return formatter.parse(text, LocalDateTime::from);
}
从源码中我们看到,parse() 方法可以传入两个参数,第二个参数就是一个 DateTimeFormatter,也就是说不仅 ISO 8601 标准格式的字符串可以被解析,我们完全可以自定义被解析的字符串格式。
import java.time.*;
import java.time.format.*;
public class Main {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
// 自定义格式化:2020/02/11 17:37:23 System.out.println(dtf.format(LocalDateTime.now()));
LocalDateTime dt2 = LocalDateTime.parse("2020/02/11 13:00:00", dtf);
// 用自定义格式解析:2020-02-11T13:00 System.out.println(dt2);
}
}
DataTimeFormatter 的 ofPattern() 方法还可以传入一个 Locale 参数,这个参数的作用是使用当地的习惯来格式化时间:
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
// 输出:2020-02-11T17:37 GMT+08:00 System.out.println(formatter.format(zdt));
var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
// 输出:2020 2月 11 周二 17:37 System.out.println(zhFormatter.format(zdt));
var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
// 输出:Tue, February/11/2020 17:37 System.out.println(usFormatter.format(zdt));
}
}
4.4. Instant
在新 API 中,使用 Instant 表示时间戳,它类似于System.currentTimeMillis()。Instant 使用如下:
import java.time.*;
public class Main {
public static void main(String[] args) {
Instant now = Instant.now();
// UTC 标准时间,输出:2020-02-11T09:38:13.891708Z System.out.println(now);
// 以秒为单位的时间戳, 输出:1581413893 System.out.println(now.getEpochSecond());
// 以毫秒为单位的时间戳,输出:1581413893891 System.out.println(now.toEpochMilli());
}
}
给 Instant 加上一个时区,就可以创建出 ZonedDateTime:
import java.time.*;
public class Main {
public static void main(String[] args) {
Instant now = Instant.ofEpochSecond(1581413893);
ZonedDateTime zdt = now.atZone(ZoneId.systemDefault());
// GMT 标准时间,输出:2020-02-11T17:38:13+08:00[Asia/Shanghai] System.out.println(zdt);
}
}
五、新旧 API 的转换
旧 API 转新 API 可以通过 toInstant() 方法转换为 Instant,再由 Instant 转换成 ZonedDateTime:
// Date -> Instant:Instant ins1 = new Date().toInstant();
// Calendar -> Instant -> ZonedDateTime:Calendar calendar = Calendar.getInstance();
Instant ins2 = Calendar.getInstance().toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());
新 API 转旧 API 时,需要借助 long 类型时间戳实现:
// ZonedDateTime -> long:ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;
// long -> Date:Date date = new Date(ts);
// long -> Calendar:Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);
以上,就是 Java 日期和时间的全部内容了,有任何收获或疑问欢迎在评论区与大家一起讨论交流。
本文作者:Alpinist Wang
声明:本文归 “力扣” 版权所有,如需转载请联系。文章封面图和文中部分图片来源于网络,如有侵权联系删除。