在 Java 8 之前,对于日期和时间的处理是能过 Date 和 Calendar 来完成的,因为长时间没接触 Java 了,我对日期的处理也还停留在它们上,最近重新学 Java 才知道,Java 8 新推出了一套日期处理的API,在这就来探讨一下它们跟之前的日期处理类有什么不同,和新的 API 有什么优点,怎么使用。
本文将以下顺序去展开:
- 为什么要推出新的日期处理 API,过去的日期处理存在哪些问题?
- Java 8 的日期 API 做了哪些优化,有什么新功能?
- Java 8 的日期 API 的使用。
Date 存在的问题
先来看看 Date 存在一些什么问题,我在网上查了一些资料,都是说 Date 存在线程安全和易用性上的问题。先来看看具体是怎样的问题。
线程安全问题
写段程序在多线程下跑一下日期格式化
public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
for(int i=0; i < 5; i++) {
new Thread(() -> {
for(;;) {
try{
System.out.println(Thread.currentThread() + ":" + simpleDateFormat.format(new Date(Math.abs(new Random().nextLong()))));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}).start();
}
}
可以看到,程序运行中途报错了
我们点进SimpleDateFormat
的源码看看,SimpleDateFormat
继承于DateFormat
,而DateFormat
内部保存着一个全局的calendar
对象的,而对日期的格式化或者解析都是要操作这个对象。然后我们再来看看format
这个方法的源码。
可以看到在第943行,对calendar
赋值了需要格式化的date
对象,如果在多线程环境下,线程1设置了calendar
的时间,但是还没完成格式化的逻辑,这里线程2又对calendar
设置时间的时候,覆盖了线程1的设置,那么线程1在后面读取calendar
对象的时间就会导致报错。为了解决这个问题,只能给每个线程都创建一个SimpleDateFormat
,跟其它线程隔离起来。
同理,对于parse
方法,每一次把日期字符串解析成对象时,calendar
都会把上一次的保存的日期时间信息全部清空,然后保存最新的日期时间信息,在多线程下就会出来,线程1设置了日期时间后,线程2又把它给清空了,最后程序就会报错了。
parse
方法最后会调用下面的方法,在第114
行对日期信息进行了清空。
易用性
相信 Date 和 Calendar 的难用大家都清楚。就随便举例几点。
Date
的月份开始是从0
开始,以11
结束的,每次操作月份都要做加1操作跟现在月份匹配上。
想要对日期进行运算,加一天,减一天,加一个月,减一个月等操作,只能通过Calendar
来进行,Date
和Calendar
转换来转换去相当的麻烦。
Date
把日期、时间揉合在了一起,当面临只需要处理日期部分或者时间部分的场景,Date
就显得有点臃肿了,而且Date
的输出可读性也不好,不对它进行格式化的话看起来很难受。
Java 8 新日期 API
基于上述问题,在 Java 8 对日期进行了优化,首先是将日期和时间设计为不可变类型,就像String类型一样,这样就避免了多线程下对日期的修改导致的线程安全问题,每次对日期的操作都会生成一个新的日期对象;另外还把功能进一步细化了,对日期的运算更加便利,输出也更加人性化。
下面来看看 Java 8 主要提供了哪一些常用的日期 API。
从上图可以看到,Java 8 把日期和时间拆分出来了,LocalDateTime
包含日期和时间,LocalDate
只包含日期部分,LocalTime
只包含时间部分,Instant
代表时间线上的一个瞬时时间点,但默认时区是UTC+0
的。
public static void main(String[] args) {
System.out.println("LocalDateTime: " + LocalDateTime.now());
System.out.println("LocalDate: " + LocalDate.now());
System.out.println("LocalTime: " + LocalTime.now());
System.out.println("Instant: (UTC+0)" + Instant.now());
System.out.println("Instant: (UTC+8)" + Instant.now().atZone(ZoneId.systemDefault()));
}
LocalDateTime
、LocalDate
、LocalTime
、Instant
都实现了Temporal
和TemporalAdjuster
接口,Temporal
提供了一些对日期运算的接口,如对日期的加减,TemporalAdjuster
提供只提供了一个接口,用于对日期/时间对象的调整。
另外LocalDateTime
内部只是封装了LocalDate
和LocalTime
,当对LocalDateTime
进行操作时,都是针对指定的日期或者时间部分去进行操作的。
另外LocalDateTime
、LocalDate
、LocalTime
、Instant
在运算中都有各自约束的范围,LocalDateTime
可以支持日期和时间的运算;LocalDate
只支持最小粒度为1天的运算,不能LocalDate
上对时间进行运算;LocalTime
支持纳秒到小时的运算,不能对日期进行运算;Instant
只支持纳秒到秒的运算。如果进行超出规定范围内的运算就抛出不支持的异常。
但是LocalTime
和Instant
的plus
有些特殊,支持到天的运算,是因为plus
在内部把天转换为在范围内的单位再进行运算。
Java 8 新日期 API 的使用
上面讲到了 Java 8 对日期 API 在安全性和易用性上的优化。现在来总结一下日常常用 API 具体怎么用。
这里以LocalDate
为例,LocalDateTime
和LocalTime
的用法差不多。
public static void main(String[] args) {
// 获取当天日期时间
LocalDate today = LocalDate.now();
print("获取当天日期时间: ", today);
// 加一天
LocalDate tomorrow = today.plusDays(1);
print("加一天: ", tomorrow);
// 加一个月
LocalDate nextMonth = today.plusMonths(1);
print("加一个月: ", nextMonth);
// 减一天
LocalDate yesterday = today.minusDays(1);
print("减一天: ", yesterday);
// 减一个月
LocalDate lastMonth = today.minusMonths(1);
print("减一个月: ", lastMonth);
// 获取今天是本月第几天
int dayOfMonth = today.getDayOfMonth();
print("获取今天是本月第几天: ", dayOfMonth);
// 获取今天是本周第几天
int dayOfWeek = today.getDayOfWeek().getValue();
print("获取今天是本周第几天: ", dayOfWeek);
// 获取今天是本年第几天
int dayOfYear = today.getDayOfYear();
print("获取今天是本年第几天: ", dayOfYear);
// 获取本月天数。
int daysOfMonth = today.lengthOfMonth();
print("获取本月天数: ", daysOfMonth);
// 获取本年天数
int daysOfYear = today.lengthOfYear();
print("获取本年天数: ", daysOfYear);
// 获取本月指定的第n天
LocalDate date1 = today.withDayOfMonth(15);
print("获取本月指定的第n天: ", date1);
// 获取本月的最后一天
LocalDate lastDaysOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
print("获取本月的最后一天: ", lastDaysOfMonth);
// 日期字符串解析。 严格按照ISO yyyy-MM-dd 验证
LocalDate date = LocalDate.parse("2021-01-17");
print("日期字符串解析: ", date);
// 日期字符串解析。 自定义格式
DateTimeFormatter dft = DateTimeFormatter.ofPattern("yyyy-M-dd");
LocalDate date2 = LocalDate.parse("2021-1-17", dft);
print("日期字符串解析(日期字符串解析): ", date2);
// 格式化日期
String dateStr = today.format(dft);
print("格式化日期: ", dateStr);
// 自定义日期
LocalDate cusDate = LocalDate.of(2020, 8, 14);
print("自定义日期: ", cusDate);
// 日期比较
boolean before = today.isBefore(tomorrow);
print("今天是否比明天早: ", before);
boolean before1 = today.isBefore(yesterday);
print("今天是否比昨天早: ", before1);
boolean after = today.isAfter(tomorrow);
print("今天是否比明天晚: ", after);
boolean after1 = today.isAfter(yesterday);
print("今天是否比昨天晚: ", after1);
// 获取两个时间相差多少天/周/月... 根据单位不同返回不同
long until = today.until(nextMonth, ChronoUnit.WEEKS);
print("今天到下个月相差几周: ", until);
Month month = today.getMonth();
print("月份:", month);
print("月份: ", month.getValue());
}
从最后月份的输出可以看出,Java 8 把月份优化成了一个枚举类,也把月份区间调整为1~12了。
Duration 和 Period
Java 8 还提供了 2 个计算两个时间/日期差的 API。
Duration
里面封装了seconds
和nanos
,前者是秒,后者是纳秒,代表着两个时间间的差值;
而Period
里面封装了day
,month
,years
3 个属性,代表的是两个日期间的差值。
所以,Duration
只能计算包含有时间的对象,比如LocalDateTime
,LocalTime
,Instant
,如果计算LocalDate
的话会不支持的异常。
同理,Period
也就只支持LocalDate
的计算。
下面来看看它们有哪些用法。
Duration
public static void main(String[] args) {
LocalDateTime today = LocalDateTime.now();
LocalDateTime tomorrow = today.plusDays(1);
// 根据两个时间获取 Duration
Duration duration = Duration.between(today, tomorrow);
print("获取纳秒数差值:", duration.toNanos());
print("获取毫秒数差值:", duration.toMillis());
print("获取秒数差值: ", duration.getSeconds());
print("获取分钟数差值:", duration.toMinutes());
print("获取小时数差值:", duration.toHours());
print("获取天数差值:", duration.toDays());
// 当第1个时间比第2个时间小时为false, 反之true。可以用来判断2个时间的大小。
boolean negative = duration.isNegative();
print("isNegative: ", negative);
// 以1天的差值创建Duration
Duration duration1 = Duration.ofDays(1);
print("以1天的差值创建Duration: ", duration1.getSeconds());
}
Duration
也支持plus
和minus
操作,这里就不演示了。
另外Duration
还有个功能,可以通过解析字符串来生成对象。字符串的规则是这样:PnDTnHnMn.nS
。P
为固定开头,n
为数字,D
为天数,T
代表后面是时间部分,H
、M
、S
分别时、分、秒。字母大小写不敏感,可大写可小写。另外还支持+
和-
。+
为往上加时间,-
为往下减时间。
public static void main(String[] args) {
Duration duration = Duration.parse("P1DT1H1M1S");
print("当前时间加上1天1小时1分钟1秒的差值: ", duration.getSeconds());
Duration duration1 = Duration.parse("P2D");
print("当前时间加上2天的差值: ", duration1.getSeconds());
Duration duration2 = Duration.parse("PT2H");
print("当前时间加上2小时的差值: ", duration2.getSeconds());
Duration duration3 = Duration.parse("PT-2H");
print("当前时间减去2小时的差值: ", duration3.getSeconds());
Duration duration4 = Duration.parse("PT-2H30M");
print("当前时间减去1小30分的差值: ", duration4.getSeconds());
Duration duration5 = Duration.parse("PT-2H-30M");
print("当前时间减去2小30分的差值: ", duration5.getSeconds());
// 上面的也可以写成这样
Duration duration6 = Duration.parse("-PT2H30M");
print("当前时间减去2小30分的差值: ", duration6.getSeconds());
}
每个n
前面都是隐式的添加了一个+
,像-2H30M
这种意思就是减去2个小时再加30分钟,那就是1个小时30分钟咯;但是如果在P
前面加一个-
的话,会对里面所有的数字都会产生影响。其实就是像小学数学一样,用括号把PnDTnHnMn.nS
括起来,(PnDTnHnMn.nS
),在最外面加一个-
,那里面的符号就全部取反了,但是单独在某个数字前加的话只会影响到它自己。
Period
Period
跟Duration
其实用法差不多,都是表达时间上的差值。只不过一个是表达日期,一个是表达时间,粒度不同。就不演示了。
总结
Java 8 的日期处理 API 常用的功能方法总结得差不多了,基本上日常使用的也就大概这么多了,这一套日期 API 搞清楚它们的区别和背后逻辑后用起来很方便,本文没有讲到的用法,开发使用的时候查一下文档也可以马上用起来了,底层都是这些基础的知识点。