文章目录

  • 一、时区
  • 二、夏令时
  • 三、旧 API
    • 3.1. Date
    • 3.2. SimpleDateFormat
    • 3.3. Calendar
  • 四、新 API
    • 4.1. LocalDateTime
    • 4.2. ZonedDateTime
    • 4.3. DateTimeFormatter
    • 4.4. Instant
  • 五、新旧 API 的转换

日期和时间是计算机处理的重要数据,在绝大多数软件程序中,我们都要和日期和时间打交道。本篇文章我们将系统地学习 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<Date> {private transient long fastTime;...
}

Date 的基本用法如下:

import java.util.*;public class Main {public static void main(String[] args) {// 获取当前时间Date date = new Date();// 年份,需要加上 1900System.out.println(date.getYear() + 1900);// 月份,取值范围是 0~11,代表 1~12 月,所以需要加上 1System.out.println(date.getMonth() + 1);// 日期,取值范围是 1~31System.out.println(date.getDate());// 转换为 String,如:Tue Feb 11 17:24:10 CST 2020System.out.println(date.toString());// 转换为 GMT 时间,如:11 Feb 2020 09:24:10 GMTSystem.out.println(date.toGMTString());// 转换为本地化字符串,如:2020年2月11日 下午5:24:10System.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 周二 CSTSystem.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();// 年份,不必加 1900int y = c.get(Calendar.YEAR);// 月份,取值范围是 0~11,代表 1~12 月,所以需要加上 1int M = c.get(Calendar.MONTH) + 1;// 日期,取值范围是 1~31int 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 3System.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:56System.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:56System.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 2020System.out.println(c.getTime());c.add(Calendar.MONTH, -1);c.add(Calendar.MONTH, 1);// 输出:Wed Dec 30 17:09:11 CST 2020System.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:00c.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 2020System.out.println(c.getTime());c.set(Calendar.DAY_OF_MONTH, 9);// 输出:Sun Feb 09 17:30:20 CST 2020System.out.println(c.getTime());}
}

四、新 API

由于旧 API 存在的诸多不便,从 Java 8 开始,java.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.027983System.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.000000001System.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-11System.out.println(d);// 输出:17:32:09.742114System.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:00System.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:00System.out.println(dt);LocalDateTime dt2 = dt.minusMonths(1).plusMonths(1);// 输出:2020-12-30T01:00System.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.570764System.out.println(dt);LocalDateTime dt2 = dt.withDayOfMonth(9);// 输出:2020-02-09T17:34:17.570764System.out.println(dt2);}
}

LocalDateTime 还有一个 with() 方法允许我们做更复杂的运算:

import java.time.*;
import java.time.temporal.*;public class Main {public static void main(String[] args) {// 今天 0:00LocalDateTime startOfToday = LocalDate.now().atStartOfDay();// 输出:2020-02-11T00:00System.out.println(startOfToday);// 本月最后一天LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());// 输出:2020-02-29System.out.println(lastDay);// 下个月第一天LocalDate firstDayOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());// 输出:2020-03-01System.out.println(firstDayOfNextMonth);// 本月第1个周一LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));// 输出:2020-02-03System.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, trueSystem.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.839591System.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:37System.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:23System.out.println(dtf.format(LocalDateTime.now()));LocalDateTime dt2 = LocalDateTime.parse("2020/02/11 13:00:00", dtf);// 用自定义格式解析:2020-02-11T13:00System.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:00System.out.println(formatter.format(zdt));var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);// 输出:2020 2月 11 周二 17:37System.out.println(zhFormatter.format(zdt));var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);// 输出:Tue, February/11/2020 17:37System.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.891708ZSystem.out.println(now);// 以秒为单位的时间戳, 输出:1581413893System.out.println(now.getEpochSecond());// 以毫秒为单位的时间戳,输出:1581413893891System.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 日期和时间的全部内容了,有什么收获或疑问欢迎在留言区一起交流。

详解 Java 日期与时间相关推荐

  1. java日期格式精确到分_详解Java日期格式化及其使用例子

    Java日期格式化及其使用例子 1 SimpleDateFormat担当重任,怎样格式化都行 import java.util.Date; import java.text.SimpleDateFor ...

  2. 8 年经验面试官详解 Java 面试秘诀!

    日前,全球知名 TIOBE 编程语言社区最新发布 11 月的编程语言排行榜,根据最新的榜单显示,相比上个月编程语言 Top 5 并没有太大的变化,其中 Java 依旧稳坐榜首,随后分别是 C.Pyth ...

  3. 详解Java解析XML的四种方法

    http://developer.51cto.com  2009-03-31 13:12  cnlw1985  javaeye  我要评论(8) XML现在已经成为一种通用的数据交换格式,平台的无关性 ...

  4. java lock unlock_详解Java中的ReentrantLock锁

    ReentrantLock锁 ReentrantLock是Java中常用的锁,属于乐观锁类型,多线程并发情况下.能保证共享数据安全性,线程间有序性 ReentrantLock通过原子操作和阻塞实现锁原 ...

  5. java 线程一直运行状态_详解JAVA 线程-线程的状态有哪些?它是如何工作的?

    线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在. 一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源.更加轻量化,也因 ...

  6. 详解Java多线程编程中LockSupport类的线程阻塞用法

    转载自  详解Java多线程编程中LockSupport类的线程阻塞用法 LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语.LockSupport实际 ...

  7. java内部格式_详解java内部类的访问格式和规则

    详解java内部类的访问格式和规则 1.内部类的定义 定义一个类来描述事物,但是这个事物其中可能还有事物,这时候在类中再定义类来描述. 2.内部类访问规则 ①内部类可以直接访问外部类中的成员,包括私有 ...

  8. 【转帖】windows命令行中java和javac、javap使用详解(java编译命令)

    windows命令行中java和javac.javap使用详解(java编译命令) 更新时间:2014年03月23日 11:53:15   作者:    我要评论 http://www.jb51.ne ...

  9. java注解 源码_详解Java注解教程及自定义注解

    详解Java注解教程及自定义注解 更新时间:2016-02-26 11:47:06   作者:佚名   我要评论(0) Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个 ...

最新文章

  1. Hadoop,Yarn,Zookeeper,kafka数据仓库集群命令集合
  2. npm/cnpm install是啥意思
  3. 基于 Ubuntu 16.04 LTS 的 KDE neon 到达维护周期
  4. 下载的c语言程序代码怎么运行,CFree怎么运行程序 编译运行C语言程序代码的方法...
  5. 解决TypeError: string indices must be integers, not str
  6. string input must not be null解决办法
  7. 程序员面试揭秘之求职渠道
  8. 深耕金融行业数字化转型,人大金仓数据库自主可控解决方案综述
  9. linux7磁盘挂载大于16T,CentOS6.5挂载超过16T的大容量存储空间,格式化为xfs
  10. 笔记本自动打开计算机,笔记本电脑自动开机是什么原因
  11. Android开发,GPS获取实时时间并转为北京时间,定位信息,海拔高度,并进行显示
  12. dynamix判定_音乐游戏中判定严格的意义何在?
  13. 12点转成0点(原因时间格式化为十二小时制导致)
  14. css语义化命名_为什么我只在生产中使用语义命名
  15. 苹果软件更新在哪里_苹果再次为已停产的AirPort发布软件更新
  16. True Type 文件格式规范
  17. GPT硬盘分区命令parted
  18. mysql数据字段属性
  19. 戴维·考克斯爵士去世
  20. 【来灌灌水】~~感谢csdn平台给予新手学习的地方

热门文章

  1. 第五人格服务器维护中怎么进游戏,第五人格怎么开始 第五人格怎么进入游戏?[多图]...
  2. 现在开一间网吧还能挣钱么?
  3. Python画美队盾牌
  4. 抖音的服务器到底啥配置?
  5. SVAC1.0逆扫描反变换反量化分析
  6. CSS制作的32种图形效果 梯形 | 三角 | 椭圆 | 平行四边形 | 菱形 | 四分之一圆 | 旗帜
  7. Markdown中图片左对齐
  8. ChatGPT专业应用:撰写节日营销活动方案
  9. LT3759 宽输入电压范围升压 / 负输出 / SEPIC 控制器 可在输入电压低至 1.6V 的情况下工作...
  10. CINTA作业四:群、子群