如何处理计算机中的日期和时间问题?

如何处理计算机中的日期和时间问题?

最近在家办工接到的一项工作是和时区有关的,据用户反馈,由于美国的 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<Date> {

    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

声明:本文归 “力扣” 版权所有,如需转载请联系。文章封面图和文中部分图片来源于网络,如有侵权联系删除。

发布于 03-05

文章被以下专栏收录