日期处理第四篇(终)- Java日期时间处理大总结

文章目录

    • 日期时间概念
      • 通用标准
      • 日期字段解析
      • 国际化的日期格式
    • 日期的实战
      • 第一个问题:日期常用时间操作
      • 第二个问题:时区的问题
        • 时区概念
        • 时区的处理
          • ZoneID的使用
        • ZoneOffset的使用
        • 让人恼火的夏令时
      • 第三个问题:MySQL存储时间用什么类型?
        • MySQL中的日期类型
          • DATETIME
          • TIMESTAMEP
          • 数值型时间戳(INT)
        • 结论
      • 第四个问题:项目国际化,日期时间处理方案。
    • 总结

前面三篇分享了在Java中处理日期、时间相关的一些工具类。俗话说得好磨刀不误砍柴工,今天我们来系统的去看一下在程序中的日期是什么样的?我们把他的原理搞清楚,把概念弄明白,希望以后再遇到日期相关的问题,处理起来事半功倍。本篇会先阐述一下时间相关的概念,然后再分享日期的一些实战,比如日期格式化、日期计算、数据库存储日期用什么类型最好等。话不多说,开始吧。

日期处理第四篇(终)- Java日期时间处理大总结_第1张图片

日期时间概念

通用标准

ISO 8601确定四位数年份、两位数日月两位数24小时制时分秒yyyy-MM-dd HH:mm:ss)的表达方式作为国际标准。这一标准方便计算机进行自然排序,且可减少歧义,有利于跨国资讯交换。说个题外话,你知道千禧年问题吗?

日期字段解析

我们经常使用的就是这种格式yyyy-MM-dd HH:mm:ss,还有很多其他格式,我们了解了下面每个字符的含义后,就可以按照自己想要的格式进行输出了。
y: 年,一般使用yyyy表示4位年份如2024,yy表示2位年份如24
M:月,一般使用MM表示月份比如01、12,MMM会根据语言环境显示不用的月份,如中国:一月、十二月;美国:Oct
d: 月份中天数,一般使用dd表示月份,如:20
D:年份中的天数,表示的是一年的第几天,用D表示,如:323
E:星期几。用E表示,比如中国显示星期六,英语环境下会显示 Sat
H:一天中的小时数(0-23),24小时制,一般用HH表示小时数,如:18
h:一天中的小时数(1-12),12小时制,使用hh表示的10点也可能是晚上22点
m:分钟,用mm表示,如:53
s:秒(1-999),用ss表示,如:35
S:毫秒,一般使用SSS表示,如:879
-: 连接符,没有特殊意义,可以是任意字符,汉字也可以
z:时区,通用时区,如:Pacific Standard Time; PST; GMT-08:00
Z:时区,RFC 822时区,如:-0800,+0800
X:时区,ISO 8601时区,如:-08; -0800; -08:00;+08:00
a:表示am/pm,
G:年代,AD(公元)、BC(公元前)

2024-01-21T00:38:55.981+08:00
这个时间的格式用字母来表达式就是:yyyy-MM-ddTHH:mm:ss.SSSX
T是什么含义呢,类似分隔符可以用任何字符代替,但是国际标准用T来表面后半部分是时间,前半部分是日期。X是时区。

我们再来看一些日期格式,下面这些其实并不常用,可以作为参考:
日期处理第四篇(终)- Java日期时间处理大总结_第2张图片

国际化的日期格式

可以简单看下国际化的日期格式都是什么样的:
https://docs.oracle.com/cd/E19683-01/816-3981/overview-46/index.html
日期处理第四篇(终)- Java日期时间处理大总结_第3张图片
以上可以看到只要掌握了日期每个字段的概念和代表符号,就可以自定义你想要的任何格式了。最常见的还是yyyy-MM-dd HH:mm:ss,主要字母的大小写。

日期的实战

知道了日期的概念之后,我们进入实战部分,这里会介绍最常见的一些场景和问题。首先之前介绍了三篇的Java日期处理的工具类:

  1. 日期处理第一篇:优雅好用的Java日期工具类Joda-Time: Java8之前的业务场景推荐使用Joda-Time
  2. 日期处理第二篇:Java8新时间和日期API,看完你就全明白了:Java8以后的版本包括Java8,优先使用内置的sdk,弃用java.date下的工具类,优先使用java.time下的工具类。
  3. 日期处理第三篇:Hutool的日期时间工具-DateUtil使用:使用起来很方便,也兼容了Java8新的日期的API,但是非必要不建议引入额外的jar包了,推荐使用内置的就可以。

应该没有太多项目了还在java8之前的版本,所以本篇会全部采用Java8新的日期API来进行编码做示例。

第一个问题:日期常用时间操作

这个问题应该是我们业务中最常遇见到的了,将日期输出、展示、存储、时间戳转成日期格式等场景,我们下面一一说明。

  1. 获取当前系统时间
// 2024-01-21T12:20:05.394
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
// 2024-01-21T12:20:05.395  推荐使用带有时区的方式获取时间
LocalDateTime localDateTime2 = LocalDateTime.now(ZoneId.systemDefault());
System.out.println(localDateTime2);
// 2024-01-21T04:20:05.395
LocalDateTime localDateTime3 = LocalDateTime.now(ZoneId.of("UTC+0"));
System.out.println(localDateTime3);

虽然我们日常使用第一种LocalDateTime.now()的场景更多,但是这里还是要强调推荐使用LocalDateTime.now(ZoneId.systemDefault());这种带有时区的方式获取时间,这样目的更明确。

  1. 获取当前系统时间戳
		//1705811713124
        long now = System.currentTimeMillis();
        System.out.println(now);
        
        // 1705811713124
        Instant instant = Instant.now();
        System.out.println(instant.toEpochMilli());
  1. 日期时间和字符串之间的转换
		// 时间戳转成字符串
        Instant instant = Instant.now();
        // 2024-01-21T12:35:13.124
        LocalDateTime fromMillsDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        System.out.println(fromMillsDateTime);
		
		// 字符串转成时间戳
		Instant instantFromDateTime = fromMillsDateTime.toInstant(ZoneOffset.UTC);
		// 1705840746886
        System.out.println(instantFromDateTime.toEpochMilli());
  1. 日期格式化-DateTimeFormatter
    记住不要再使用SimpleDateFormat了,SimpleDateFormat是线程不安全的,也说也被淘汰的类了,推荐使用DateTimeFormatter,DateTimeFormatter也是一个不可变的类,所以是线程安全的,比SimpleDateFormat靠谱多了吧。另外它还内置了非常多的格式化模版实例供以使用,形如:
    日期处理第四篇(终)- Java日期时间处理大总结_第4张图片
String strDate6 = "2017-01-05";
String strDate7 = "2017-01-05 12:30:05";

LocalDate date = LocalDate.parse(strDate6, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
LocalDateTime dateTime1 = LocalDateTime.parse(strDate7, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));


LocalDateTime dateTime = LocalDateTime.now();
String strDate1 = dateTime.format(DateTimeFormatter.BASIC_ISO_DATE);    // 20170105
String strDate2 = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE);    // 2017-01-05
String strDate3 = dateTime.format(DateTimeFormatter.ISO_LOCAL_TIME);    // 14:20:16.998
String strDate4 = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));   // 2017-01-05
String strDate5 = dateTime.format(DateTimeFormatter.ofPattern("今天是:YYYY年 MMMM DD日 E", Locale.CHINESE)); // 今天是:2017年 一月 05日 星期四

若想自定义模式pattern,和Date一样它也可以自己指定任意的pattern 日期/时间模式。由于本文在Date部分详细介绍了日期/时间模式,各个字母代表什么意思以及如何使用,这里就不再赘述了哈。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("第Q季度 yyyy-MM-dd HH:mm:ss", Locale.US);

    // 格式化输出
    System.out.println(formatter.format(LocalDateTime.now()));

    // 解析
    String dateTimeStrParam = "第1季度 2021-01-17 22:51:32";
    LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam, formatter);
    // Q/q:季度,如3; 03; Q3; 3rd quarter。
    System.out.println("解析后的结果:" + localDateTime);

还有很多其他操作参考: 日期处理第二篇:Java8新时间和日期API,看完你就全明白了

第二个问题:时区的问题

令人最头痛的应该就是这个时区的问题了,平时虽然我们用不到,但还是会经常遇到时区的问题的,最近也在做国际化相关的项目,所以开始深入的了解一下时区的问题。
查资料的时候发现这篇文章研究的很清楚,可以参考:https://www.cnblogs.com/yourbatman/p/14307194.html
如果展开来讲,又能写一篇博客深入浅出时区了,这里我们还是尽量简单讲基础概念和实战运用,想深入了解的可以参考上面的文章。

时区概念

我们经常遇到一些名词:时间戳、UTC、GMT、夏令时等,我们来看一看这几个名词分别是什么意思。

  • 时间戳(Timestamp):是指从一个特定的时间起点开始,到另一个时间点的间隔数值。通常来说,这个起点是UNIX时间的开始,即1970年1月1日00:00:00 UTC(协调世界时),到目标日期的秒数(或者毫秒数)。时间戳是一个非常精确的时间表示方法,广泛应用于程序开发和数据库中。

  • UTC(Coordinated Universal Time,协调世界时):是世界标准时间,替代了过去的格林尼治标准时间(GMT)。UTC比GMT更加精确,在偏差控制上采用了原子时钟,因此UTC现在被作为全球统一的时间标准。

  • GMT(Greenwich Mean Time,格林尼治平时):指位于英国伦敦郊区的格林尼治天文台的标准时间,过去常用作国际时间标准。由于GMT不是非常精确(它不涉及原子时钟),现在一般使用更精确的UTC来替代它。

  • 夏令时(Daylight Saving Time,DST):是一种在夏季把标准时间调快1小时的制度,目的是让人们更好地利用日照,晚上减少电灯的使用,以节约能源。不是所有的国家和地区都采用夏令时。在进入冬季时,会把时间调回正常的标准时间。

其实夏令时的英语很有意思,节省日光时间,所以这就很容易记住和明白他的含义了。这里记住UTC就可以了,因为我们使用最多的还是UTC。还有UTC、GMT这些不是时区,是一个标准的统称,UTC+8 这才是一个时区,表示标准时间增加8个小时,就是北京时间了。

时区的处理

Java中引入了ZoneId和ZoneOffset,这两个概念如下:
ZoneId:代表了一个时区的标识符,例如“Europe/Paris”。它是用来识别特定的时区规则的,并且可以用于转换时间点到本地时间。ZoneId可以通过静态方法of来获取,也可以通过时区规则的转换来获得。ZoneId与时区规则相关,这意味着它也包含了关于夏令时(如果该时区有)的信息。如:将一个Instant时间戳转换为本地日期/时间LocalDateTime。

ZoneOffset:是ZoneId的一个具体实现,它表示与UTC/格林尼治时间偏移了多少小时、分钟。ZoneOffset不包含夏令时的信息,它纯粹代表了一个固定的时差,例如“+08:00”代表东八区。ZoneOffset通常用在不需要关心夏令时变化的情况下,如记录日志、事件时间戳等。

ZoneID的使用

时区ZoneId是包含有规则的,实际上描述偏移量何时以及如何变化的实际规则由java.time.zone.ZoneRules定义。ZoneId则只是一个用于获取底层规则的ID。之所以采用这种方法,是因为规则是由政府定义的,并且经常变化,而ID是稳定的。对于API调用者来说只需要使用这个ID(也就是ZoneId)即可,而需无关心更为底层的时区规则ZoneRules,和“政府”同步规则的事是它领域内的事就交给它喽。如:夏令时这条规则是由各国政府制定的,而且不同国家不同年一般都不一样,这个事就交由JDK底层的ZoneRules机制自行sync,使用者无需关心。
ZoneId在系统内是唯一的,它共包含三种类型的ID:

最简单的ID类型:ZoneOffset,它由’Z’和以’+‘或’-‘开头的id组成。如:Z、+18:00、-18:00
另一种类型的ID是带有某种前缀形式的偏移样式ID,例如’GMT+2’或’UTC+01:00’。可识别的(合法的)前缀是’UTC’, ‘GMT’和’UT’
第三种类型是基于区域的ID(推荐使用)。基于区域的ID必须包含两个或多个字符,且不能以’UTC’、‘GMT’、‘UT’ '+‘或’-'开头。基于区域的id由配置定义好的,如Europe/Paris

在Java中使用ZoneId来处理时区。

        // 中国时间:2024-01-21T13:12:57.577
        LocalDateTime chinaTime = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
        LocalDateTime chinaTime2 = LocalDateTime.now(ZoneId.of("UTC+8"));
        System.out.println(chinaTime);
        System.out.println(chinaTime2);

        // 纽约时间:2024-01-21T00:12:57.580
        LocalDateTime usaTime2 = LocalDateTime.now(ZoneId.of("America/New_York"));
        System.out.println(usaTime2);

这些代号不太好记,可以实时查看sdk,也可以根据时区偏移量来计算。
日期处理第四篇(终)- Java日期时间处理大总结_第5张图片

ZoneOffset的使用

距离格林威治/UTC的时区偏移量,例如+02:00。值得注意的是它继承自ZoneId,所以也可当作一个ZoneId来使用的,当然并不建议你这么去做,请独立使用。

时区偏移量是时区与格林威治/UTC之间的时间差。这通常是固定的小时数和分钟数。世界不同的地区有不同的时区偏移量。在ZoneId类中捕获关于偏移量如何随一年的地点和时间而变化的规则(主要是夏令时规则),所以继承自ZoneId。

偏移量是能精确到秒的,只不过一般来说精确到分钟已经到顶了。

		// 2024-01-21T13:20:14.987
        LocalDateTime zoneOffsetTime = LocalDateTime.now(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(8)));
        System.out.println(zoneOffsetTime);
让人恼火的夏令时

因为有夏令时规则的存在,让操作日期/时间的复杂度大大增加。但还好JDK尽量的屏蔽了这些规则对使用者的影响。因此:**推荐使用时区(ZoneId)**转换日期/时间,一般情况下不建议使用偏移量ZoneOffset去搞,这样就不会有夏令时的烦恼啦。

第三个问题:MySQL存储时间用什么类型?

在数据库中也有很多存储时间格式,有的使用INT,有的使用DATETIME或者使用TIMESTAMP,甚至有人会使用字符串的日期时间如”2024-01-21 13:00:00“。那么下面我们来看下使用哪种方式存储比较好。

首先,不推荐使用字符串。这是初学者很容易犯的错误,容易直接将字段设置为 VARCHAR 类型,存储"2021-01-01 00:00:00"这样的字符串。当然这样做的优点是比较简单,上手快。但是极力不推荐这样做,因为这样做有两个比较大的问题:

  • 字符串占用的空间大
  • 这样存储的字段比较效率太低,只能逐个字符比较,无法使用 MySQL 提供的日期API
MySQL中的日期类型

MySQL 数据库中常见的日期类型有 YEAR、DATE、TIME、DATETIME、TIMESTAMEP。因为一般都需要将日期精确到秒,其中比较合适的有DATETIMETIMESTAMEP

DATETIME

DATETIME 在数据库中存储的形式为:YYYY-MM-DD HH:MM:SS,固定占用 8 个字节。
从 MySQL 5.6 版本开始,DATETIME 类型支持毫秒,DATETIME(N) 中的 N 表示毫秒的精度。例如,DATETIME(6) 表示可以存储 6 位的毫秒值。

TIMESTAMEP

TIMESTAMP 实际存储的内容为‘1970-01-01 00:00:00’到现在的毫秒数。在 MySQL 中,由于类型 TIMESTAMP 占用 4 个字节,因此其存储的时间上限只能到‘2038-01-19 03:14:07’。

从 MySQL 5.6 版本开始,类型 TIMESTAMP 也能支持毫秒。与 DATETIME 不同的是,若带有毫秒时,类型 TIMESTAMP 占用 7 个字节,而 DATETIME 无论是否存储毫秒信息,都占用 8 个字节。

类型 TIMESTAMP 最大的优点是可以带有时区属性,因为它本质上是从毫秒转化而来。如果你的业务需要对应不同的国家时区,那么类型 TIMESTAMP 是一种不错的选择。比如新闻类的业务,通常用户想知道这篇新闻发布时对应的自己国家时间,那么 TIMESTAMP 是一种选择。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。

数值型时间戳(INT)

很多时候,我们也会使用 int 或者 bigint 类型的数值也就是时间戳来表示时间。这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。

综上,通过对这三个类型的比较,推荐使用TIMESTAMP。我们看看《高性能 MySQL 》中的作者是如何说的:
日期处理第四篇(终)- Java日期时间处理大总结_第6张图片

结论

当你不知道在数据库中使用哪个时间类型时,就选择TIMESTAMP。

第四个问题:项目国际化,日期时间处理方案。

内容过多,准备专门写一个博客来阐述国际化方案中的时间该怎么处理,从数据库到前端展示等,保持关注。

总结

本文在前三篇的基础上,着重介绍日期时间的原理和概念以及经常用到的一些名词解释。最后把项目中经常遇到的一些问题记录下来,后续如果再遇到其他的问题,也会补充到这里,记得收藏关注。

你可能感兴趣的:(Java篇,java,LocalDateTime,Java日期时间,日期,时间)