时间对生活来说非常重要,Java也为我们提供了时间的API,多数程序员都在吐槽Java8之前的日期和时间,在Java8中引入全新的日期和时间API,目前我们项目中都在使用这一新的API。之前的API到底不好在哪里?Java8的时间API到底是在吹还是真的不错?在这篇文章中都有答案!
接下来会先介绍之前API的弊病,然后转而介绍新API的升级和具体应用,老样子文章写的非常用心,都喜欢长的,建议收藏反复观看,对于日期和时间的操作都在这了!
Java老API的缺点主要为以下几点:
- 日期和时间的计算方式不合理
- 日期和时间的输出格式不合理
- 时间格式化工具:DateFormat线程不安全
- 类库包设计杂乱
第一代时间类库
java.util.Date
在Java1.0中,对日期和时间的支持只能依赖java.util.Date类。这个类无法表示日期,只能以毫秒的精度表示时间。而且设计不合理,比如:年份的起始选择是1900年,月份的起始从0开始。如果你想表示女朋友的生日,即2022年12月10日,需要通过以下方式:
输出结果为:
其中,Sat:是 Saturday,周六的缩写,Dec:是 December,12月的缩写,手把手解释,就是这么细,10就不解释了吧,都知道是10号的意思!
总之,看起来非常别扭,它的返回值中的 CST 其实是JVM的默认时区,即美国、澳大利亚、古巴或中国的标准时间,具体是哪一个我也不清楚,毕竟这几个时区都可以简写为 CST,如果能识别出来当前程序在中国运行的,那么可以理解为代表了中国标准时间
- 美国中部时间 Central Standard Time (USA)
- 中国标准时间 China Standard Time UT+8:00,中国在东八区时间要快8小时,需要 + 8:00
- 古巴标准时间 Cuba Standard Time UT-4:00
java.util.Date源码:
通过源码就会明白几个为什么:
- 为什么年份要减去1900
- 为什么月份使用0开始的
- 为什么输出的时间会有 00:00:00 的时间
java.sql.Date
在java.sql包中也有一个Date类,通过源码可以看出,其实java.sql.Date是继承了java.util.Date
类上边的注解这么写道:
翻译:
一个围绕毫秒值的精简包装器,允许JDBC将其标识为SQL DATE值。毫秒值表示自1970年1月1日00:00:00.000 GMT以来经过的毫秒数。
为了符合SQL DATE的定义,毫秒值由java.sql.Date包装。必须通过在与实例关联的特定时区中将小时、分钟、秒和毫秒设置为零来“规范化”日期实例
通过上边的翻译可以看出,Java想用java.sql.Date类来映射SQL中的时间戳,非常理想化,我们一般都需要时分秒,这个类直接将时分秒设置为0啦,基本用不了
总结:
- 使用Date类时需要对年份 减1900,月份 减1 来初始化一个Date对象,不合常理
- 时间的输出格式对于人类来说难以阅读,竟然包含时区,而且顺序也比较混乱
- 无论是使用还是阅读都不合理
第二代时间类库
Java 1.1中,Date类中的很多方法被废弃了,取而代之的是java.util.Calendar类,Calendar类也有类似的问题和设计缺陷,导致编码时非常容易出错。比如,月份依旧是从0开始计算(不过,至少Calendar类拿掉了由1900年开始计算年份这一设计)。同时存在Date和Calendar这两个类,出现了选择困难症。有的特性只在某一个类有提供,比如格式化和解析日期或时间的DateFormat方法就只在Date类里有,那我用 Calendar 干什么,这就出现了 Calendar 使用很少很少的现象,我觉得是一个失败的升级,小伙伴们在编程或者阅读别人代码的时候,见过 Calendar 吗?
最后,Date和Calendar类设计了拥有set方法都是可变的,能把原本的时间改为任意一个时间意味着什么呢?意味着你程序中从数据库读取到的时间可以被修改掉,比如交易日期本来是2022年12月8日,一个set方法就可以改成其他时间,在维护时绝对是灾难,交易时间等应该是不可变的才对,并没有内鬼!
运行结果:
时间格式化
DateFormat是时间格式化的抽象类,我们经常使用它的子类SimpleDateFormat来格式化和解析日期与时间,但是也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果尝试使用同一个formatter解析日期,你可能会得到无法预期的结果。而且格式化和解析时间的类在 java.text 包下,这是想害死强迫症,劝退处女座。
运行结果:
解决线程安全:
解决线程安全问题,首先想到的就是加锁
或者通过 JUC 中,也就是Java5新增的Lock对象加锁,这里锁不是重点
运行结果:
总结:
- 早期的日期时间类库设计不统一,分布在不同的包中
- 日期设计不合理,Date中的年从1900年算起,包括 Calendar 中的月份从0开始
- Date和Calendar 的时间可变,会造成很大的安全隐患
- DateFormat线程不安全,多线程场景需要手动加锁解决
第三代时间类库
所有这些缺陷和不一致导致用户转投第三方的日期和时间库,比如Joda-Time。Oracle觉得面子挂不住,决定在原生的Java API中提供高质量的日期和时间支持。所以,在Java 8在java.time包中整合了很多Joda-Time的特性。这一章中,我们会一起探索新的日期和时间API所提供的新特性。首先看一下java.time包中都包含哪些东西:
开发中常用的看下边脑图:
将从以下几点系统学习新日期API:
- 基本的用例,比如创建简单的日期和时间
- 高级操作,日期和时间的操纵、解析、打印输出
- 使用不同的时区和年历
小贴士:这些API中的方法比较多,就不贴源码了,演示最常用的方法,感兴趣的可以自己打开IDE练习方法和翻阅源码
LocalDate
作用:该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。它也不附带任何与时区相关的信息
创建:通过静态工厂方法of创建一个LocalDate实例,提供了多种方法来读取常用的值,比如年份、月份、星期几等
代码实现:
运行结果:
直接获取当前日期:
使用工厂方法从系统时钟中获取当前的日期:
TemporalField读取值:
LocalDate提供了get方法通过传入TemporalField接口获取值,它是接口无法直接使用,可以使用实现该接口的ChronoField枚举类实现
这样获取月份和星期就不需要转换了
LocalTime
LocalTime可以用来表示一天内的时间,比如 13:14:52 ,可以通过工厂方法of实现LocalTime的创建,可以指定,时,分,秒,纳秒
取值范围合理:
- 小时:0-23
- 分钟:0-59
- 秒钟:0-59
- 纳秒:0-999999999
代码实现:
字符串创建:
可以调用LocalDate 和LocalTime 的parse方法根据字符串解析为日期和时间对象
小贴士:如果你传递的日期和时间是错误的运行时则会报错,比如2022年不是闰年所以2月只有28号,请观察下方动图,其中鼠标选中的leap year是闰年的意思,报错信息提示2022年不是闰年
抛出的是 DateTimeParseException 继承自 RuntimeException
LocalDateTime
是对LocalDate和LocalTime的合并,同时表示了日期和时间,但不带有时区信息,可以直接创建,也可以通过合并日期和时间对象构造
LocalDate的atTime方法:
支持传入LocalTime对象,一起合并返回一个LocalDateTime对象
LocalTime的atDate方法:
LocalTime就只有一个 atDate方法接收LocalDate对象,不支持传入年月日,一起返回LocalDateTime对象
LocalDateTime的toLocalDate和toLocalTime:
可以将日期时间对象拆解出 LocalDate和LocalTime对象
Instant
Instant 类 是Java8 中补充的一个 时间戳类。相较于 System.currentTimeMillis()获取到【毫秒】,Instant 可以更为精确的获取到【纳秒】。该类是基于计算机角度描述时间,以Unix元年时间,也就是1970年1月1日0点开始所经历的秒数
运行结果:
Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。比如我们想要和LocalDate那样获取获取当前星期的话:
就会出现UnsupportedTemporalTypeException 异常
Period 和 Duration
作用:
- Period:计算两个“日期”间隔的类
- Duration:计算两个“时间”间隔的类
Period 类与 Duration 类都是一段持续时间的概念,如果需要对比时间,它们就需要一个固定的时间值,所以就需要 LocalDate 、LocalDateTime、LocalTime、Instant 、类来配合它们使用
Period 对应使用 LocalDate ,它们的作用范围域都是日期(年/月/日),Duration 对应使用 Instant、LocalTime、LocalDateTime,它们的作用范围域都是时间(天/时/分/秒/毫秒/纳秒)
Period:
Period可以用于计算两个日期之间的间隔,但是得到的是年月日,如两个日期相差1年2月3天,但是没办法知道1年2月3天具体是多少天,可以通过ChronoUnit.between()方法计算两个单元相差的天数、月数、年数…这个后便会说到
Period默认输出格式为PnYnMnD,如P2022Y12M31D 以P前缀开头,Y:年 M:月份 D:天 当Period为0时,默认为:P0D
常用API:
方法 |
说明 |
static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) |
计算两个日期之间的间隔 |
boolean isNegative() |
检查此时间段的三个单位中是否有一个为负数。这将检查年,月或天的单位是否小于零。如果此期间的任何单位为负,则为true |
int getYears() |
获取年 |
int getMonths() |
获取月 |
int getDays() |
获取日 |
构建Period:
运行结果:
获取年月日:
- getYears
- getMonths
- getDays
获取两个时间差:
运行结果:
计算相差具体天数:
方法一:通过ChronoUnit也可以计算两个日期之间的天数、月数或年数
方法二:调用LocalDate类的toEpochDay方法,返回距离1970年1月1日的long值,此方法只能计算两个LocalDate日期间的天数,不能计算月份、年数
Duration:
Duration 表示一个时间段,Duration 包含:seconds 表示秒,nanos 表示纳秒, 不包含毫秒,它们的组合表达了时间长度。因为 Duration 表示时间段,并不存在now()这样的静态方法。
Duration只能处理时间,例如LocalTime, LocalDateTime, ZonedDateTime; 如果传入的是LocalDate这种描述日期的类,将会抛出异常
Duration默认输出格式为PTnHnMnS,如PT8H6M12.345S 以PT前缀开头,H:小时 M:分钟 S:秒 当Duration为0时,默认为:PT0S
常用API:
方法 |
说明 |
static Duration between(Temporal startInclusive, Temporal endExclusive) |
计算两个时间的间隔,默认是秒 |
boolean isNegative() |
检查Duration实例是否小于0,若小于0返回true, 若大于等于0返回false |
long toDays() |
将时间转换为以天为单位的long值 |
long toHours() |
将时间转换为以时为单位的long值 |
long toMinutes() |
将时间转换为以分钟为单位的long值 |
long toSeconds() |
将时间转换为以秒为单位的long值 |
long toMillis() |
将时间转换为以毫秒为单位的long值 |
long toNanos() |
将时间转换为以纳秒为单位的long值 |
简单API调用:
API和Period基本相似,两者区别在于Period计算日期差,Duration计算时间差
计算时间间隔:
方法一:通过Duration计算两个LocalTime相差的时间
方法二:与Period相似,通过ChronoUnit类的between() 方法来执行相同的操作
方法三:通过LocalTime类的toSecondOfDay()方法,返回时间对应的秒数,然后计算出两个时间相差的间隔
计算两个时间戳的间隔:
总结:
- Period用来计算两个日期之间的间隔,Duration用来计算两个时间之间的间隔
- 两者的结果默认有自己的特殊格式,可以通过getXX或者toXX方法获取相应的间隔单位
- 可以配合 ChronoUnit 来单独计算指定单位的日期差和时间差
至此,已经为大家讲解了基础的日期、时间、时间戳、两个日期、时间的间隔,累的话可以喝口茶休息一下,接下来讲解操作、格式化、解析日期
操作日期
注意:LocalDate是不可变的,计算后都会返回一个新的LocalDate对象
增加日期
增加日期API:
方法 |
作用 |
plus(TemporalAmount amountToAdd) |
通过添加指定的TemporalAmount返回LocalDate实例 |
plus(long amountToAdd, TemporalUnit unit) |
通过增加给定的数量返回LocalDate实例。 |
plusYears(long yearsToAdd) |
通过添加指定的年数返回LocalDate实例。 |
plusMonths(long monthsToAdd) |
通过添加指定的月数返回LocalDate实例。 |
plusWeeks(long weeksToAdd) |
通过添加指定的星期数返回LocalDate实例。 |
plusDays(long daysToAdd) |
通过添加指定的天数返回LocalDate实例。 |
代码实现:
减去日期
用法和plus基本相同,区别在于方法切换为minus
减去日期API:
方法 |
作用 |
minus(TemporalAmount amountToSubtract) |
通过减去指定的TemporalAmount返回LocalDate实例 |
minus(long amountToSubtract, TemporalUnit unit) |
通过减去给定的数量返回LocalDate实例。 |
minusYears(long yearsToSubtract) |
通过减去指定的年数返回LocalDate实例。 |
minusMonths(long monthsToSubtract) |
通过减去指定的月数返回LocalDate实例。 |
minusWeeks(long weeksToSubtract) |
通过减去指定的星期数返回LocalDate实例。 |
minusDays(long daysToSubtract) |
通过减去指定的天数返回LocalDate实例。 |
代码实现:
调整日期
修改原LocalDate对象的年、月、日、周等数据,并返回一个新的LocalDate对象,这写方法都是with开头的
调整日期API:
方法 |
作用 |
with(TemporalAdjuster adjuster) |
返回用给定的TemporalAdjuster调整的LocalDate实例 |
with(TemporalField field, long newValue) |
将指定字段的LocalDate实例返回到一个新值 |
withYear(int year) |
通过用给定值改变年份来返回LocalDate实例 |
withMonth(int month) |
通过用给定的值改变年的月份来返回LocalDate实例。有效值是到12 |
withDayOfMonth(int dayOfMonth) |
通过用给定的值改变月份中的号数来返回LocalDate实例 |
withDayOfYear(int dayOfYear) |
通过使用给定值更改一年中的某一天来返回 LocalDate 实例。一年中的第几天的有效值为1到 365,闰年的有效值为 1到 366 |
代码实现:
比较日期
方法 |
作用 |
isAfter(ChronoLocalDate other) |
检查此日期是否在给定日期之后 |
isBefore(ChronoLocalDate other) |
检查此日期是否在给定日期之前 |
isEqual(ChronoLocalDate other) |
检查此日期是否等于给定日期 |
compareTo(ChronoLocalDate other) |
将此日期与指定日期进行比较 |
equals(Object obj) |
检查此日期是否等于指定日期 |
isLeapYear() |
判断是否为闰年 |
isSupported(TemporalField field) |
检查是否支持给定字段,在从日期获取任何字段之前,可以检查该字段是否受支持,否则可能会出错 |
isSupported(TemporalUnit unit) |
检查是否支持给定的单位。在使用加减之前,可以检查是否支持给定的单位,否则可能会出错 |
lengthOfMonth() |
给出月份的最大天数,例如 28、29、30、31 |
lengthOfYear() |
给出年份的最大天数 365 或 366(闰年) |
代码实现:
小贴士:LocalTime和LocalDateTime都有这些操作日期的API,大家可以放心食用
格式化时间
Java 8 的 java.time.format 包中提供了 DateTimeFormatter 和 DateTimeFormatterBuilder 来以不同的方式格式化日期、时间或两者
DateTimeFormatter:具有可直接用于解析字符序列的内置格式。
DateTimeFormatterBuilder:提供自定义方式来创建格式化器
格式化LocalDate:
也可以通过工厂方法解析字符串来创建 LocalDate
老API中的解析时间多线程不安全还记得吗?DateTimeFormatter 实例都是线程安全的
DateTimeFormatterBuilder: 提供了更复杂的格式器,可以自定义格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,所有的格式化器都是用DateTimeFormatterBuilder创建的,可以通过appendValue、appendLiteral和appendText等,用于生成一个格式化器
操作时区
之前的日期和时间都不包含时区信息。时区的处理是新版日期和时间API新增的重要功能,使用新版日期和时间API时区的处理被极大地简化。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心。跟其他日期和时间类一样,ZoneId类也是无法修改的。时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识
地区ID都为{区域}/{城市}的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId
一旦得到ZoneId对象,就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点
重要:LocalDate、LocalTime、ZoneId、LocalDateTime、ZonedDateTime五者关系如下:
相信这五张图对您理解个对象之间的关系和差异有非常有力的帮助
总结
- Java 8之前老版的java.util.Date类以及其他用于日期时间的类在设计上有缺陷,比如可变性,尴尬的起始时间,默认值和包名
- 新版的日期和时间API中,日期-时间对象是不可变的
- 新的API提供了两种不同的时间表示方式,有效地区分了运行时人【LocalDate】和机器【Instant】的不同需求
- 操纵时间时钟返回一个全新的日期和时间,不会改变原本的日期和时间
- 使用TemporalAdjuster可以更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器
- 格式化时间也变得线程安全,而且可以根据自己的意愿自定义格式化风格
文章出自: 石添的编程哲学 ,如有转载本文请联系【石添的编程哲学】今日头条号。