文章目录
前面三篇分享了在Java中处理日期、时间相关的一些工具类。俗话说得好磨刀不误砍柴工,今天我们来系统的去看一下在程序中的日期是什么样的?我们把他的原理搞清楚,把概念弄明白,希望以后再遇到日期相关的问题,处理起来事半功倍。本篇会先阐述一下时间相关的概念,然后再分享日期的一些实战,比如日期格式化、日期计算、数据库存储日期用什么类型最好等。话不多说,开始吧。
日期时间概念
通用标准
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是时区。
我们再来看一些日期格式,下面这些其实并不常用,可以作为参考:
国际化的日期格式
可以简单看下国际化的日期格式都是什么样的:
https://docs.oracle.com/cd/E19683-01/816-3981/overview-46/index.html
以上可以看到只要掌握了日期每个字段的概念和代表符号,就可以自定义你想要的任何格式了。最常见的还是yyyy-MM-dd HH:mm:ss,主要字母的大小写。
日期的实战
知道了日期的概念之后,我们进入实战部分,这里会介绍最常见的一些场景和问题。首先之前介绍了三篇的Java日期处理的工具类:
- 日期处理第一篇:优雅好用的Java日期工具类Joda-Time: Java8之前的业务场景推荐使用Joda-Time
- 日期处理第二篇:Java8新时间和日期API,看完你就全明白了:Java8以后的版本包括Java8,优先使用内置的sdk,弃用java.date下的工具类,优先使用java.time下的工具类。
- 日期处理第三篇:Hutool的日期时间工具-DateUtil使用:使用起来很方便,也兼容了Java8新的日期的API,但是非必要不建议引入额外的jar包了,推荐使用内置的就可以。
应该没有太多项目了还在java8之前的版本,所以本篇会全部采用Java8新的日期API来进行编码做示例。
第一个问题:日期常用时间操作
这个问题应该是我们业务中最常遇见到的了,将日期输出、展示、存储、时间戳转成日期格式等场景,我们下面一一说明。
- 获取当前系统时间
// 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());这种带有时区的方式获取时间,这样目的更明确。
- 获取当前系统时间戳
//1705811713124
long now = System.currentTimeMillis();
System.out.println(now);
// 1705811713124
Instant instant = Instant.now();
System.out.println(instant.toEpochMilli());
- 日期时间和字符串之间的转换
// 时间戳转成字符串
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());
- 日期格式化-DateTimeFormatter
记住不要再使用SimpleDateFormat了,SimpleDateFormat是线程不安全的,也说也被淘汰的类了,推荐使用DateTimeFormatter,DateTimeFormatter也是一个不可变的类,所以是线程安全的,比SimpleDateFormat靠谱多了吧。另外它还内置了非常多的格式化模版实例供以使用,形如:
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,也可以根据时区偏移量来计算。
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。因为一般都需要将日期精确到秒,其中比较合适的有DATETIME,TIMESTAMEP。
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 》中的作者是如何说的:
结论
当你不知道在数据库中使用哪个时间类型时,就选择TIMESTAMP。
第四个问题:项目国际化,日期时间处理方案。
内容过多,准备专门写一个博客来阐述国际化方案中的时间该怎么处理,从数据库到前端展示等,保持关注。
总结
本文在前三篇的基础上,着重介绍日期时间的原理和概念以及经常用到的一些名词解释。最后把项目中经常遇到的一些问题记录下来,后续如果再遇到其他的问题,也会补充到这里,记得收藏关注。