Java 8 日期/时间 API

在 Java 8 之前,对于日期和时间的处理是能过 Date 和 Calendar 来完成的,因为长时间没接触 Java 了,我对日期的处理也还停留在它们上,最近重新学 Java 才知道,Java 8 新推出了一套日期处理的API,在这就来探讨一下它们跟之前的日期处理类有什么不同,和新的 API 有什么优点,怎么使用。

本文将以下顺序去展开:

  1. 为什么要推出新的日期处理 API,过去的日期处理存在哪些问题?
  2. Java 8 的日期 API 做了哪些优化,有什么新功能?
  3. 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();
        }
    }

可以看到,程序运行中途报错了

Java 8 日期/时间 API

我们点进SimpleDateFormat的源码看看,SimpleDateFormat继承于DateFormat,而DateFormat内部保存着一个全局的calendar对象的,而对日期的格式化或者解析都是要操作这个对象。然后我们再来看看format这个方法的源码。
Java 8 日期/时间 API

可以看到在第943行,对calendar赋值了需要格式化的date对象,如果在多线程环境下,线程1设置了calendar的时间,但是还没完成格式化的逻辑,这里线程2又对calendar设置时间的时候,覆盖了线程1的设置,那么线程1在后面读取calendar对象的时间就会导致报错。为了解决这个问题,只能给每个线程都创建一个SimpleDateFormat,跟其它线程隔离起来。

同理,对于parse方法,每一次把日期字符串解析成对象时,calendar都会把上一次的保存的日期时间信息全部清空,然后保存最新的日期时间信息,在多线程下就会出来,线程1设置了日期时间后,线程2又把它给清空了,最后程序就会报错了。

parse方法最后会调用下面的方法,在第114行对日期信息进行了清空。
Java 8 日期/时间 API

易用性

相信 Date 和 Calendar 的难用大家都清楚。就随便举例几点。

Date的月份开始是从0开始,以11结束的,每次操作月份都要做加1操作跟现在月份匹配上。

想要对日期进行运算,加一天,减一天,加一个月,减一个月等操作,只能通过Calendar来进行,DateCalendar转换来转换去相当的麻烦。

Date把日期、时间揉合在了一起,当面临只需要处理日期部分或者时间部分的场景,Date就显得有点臃肿了,而且Date的输出可读性也不好,不对它进行格式化的话看起来很难受。
Java 8 日期/时间 API

Java 8 新日期 API

基于上述问题,在 Java 8 对日期进行了优化,首先是将日期和时间设计为不可变类型,就像String类型一样,这样就避免了多线程下对日期的修改导致的线程安全问题,每次对日期的操作都会生成一个新的日期对象;另外还把功能进一步细化了,对日期的运算更加便利,输出也更加人性化。

下面来看看 Java 8 主要提供了哪一些常用的日期 API。
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()));
    }

Java 8 日期/时间 API

LocalDateTimeLocalDateLocalTimeInstant都实现了TemporalTemporalAdjuster接口,Temporal提供了一些对日期运算的接口,如对日期的加减,TemporalAdjuster提供只提供了一个接口,用于对日期/时间对象的调整。

另外LocalDateTime内部只是封装了LocalDateLocalTime,当对LocalDateTime进行操作时,都是针对指定的日期或者时间部分去进行操作的。

另外LocalDateTimeLocalDateLocalTimeInstant在运算中都有各自约束的范围,LocalDateTime可以支持日期和时间的运算;LocalDate只支持最小粒度为1天的运算,不能LocalDate上对时间进行运算;LocalTime支持纳秒到小时的运算,不能对日期进行运算;Instant只支持纳秒到秒的运算。如果进行超出规定范围内的运算就抛出不支持的异常。

但是LocalTimeInstantplus有些特殊,支持到天的运算,是因为plus在内部把天转换为在范围内的单位再进行运算。
Java 8 日期/时间 API

Java 8 日期/时间 API

Java 8 新日期 API 的使用

上面讲到了 Java 8 对日期 API 在安全性和易用性上的优化。现在来总结一下日常常用 API 具体怎么用。

这里以LocalDate为例,LocalDateTimeLocalTime的用法差不多。

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 日期/时间 API

从最后月份的输出可以看出,Java 8 把月份优化成了一个枚举类,也把月份区间调整为1~12了。

Duration 和 Period

Java 8 还提供了 2 个计算两个时间/日期差的 API。

Duration里面封装了secondsnanos,前者是秒,后者是纳秒,代表着两个时间间的差值;

Period里面封装了daymonthyears 3 个属性,代表的是两个日期间的差值。

所以,Duration只能计算包含有时间的对象,比如LocalDateTimeLocalTimeInstant,如果计算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());
    }

Java 8 日期/时间 API

Duration也支持plusminus操作,这里就不演示了。

另外Duration还有个功能,可以通过解析字符串来生成对象。字符串的规则是这样:PnDTnHnMn.nSP为固定开头,n为数字,D为天数,T代表后面是时间部分,HMS分别时、分、秒。字母大小写不敏感,可大写可小写。另外还支持+-+为往上加时间,-为往下减时间。

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());
    }

Java 8 日期/时间 API

每个n前面都是隐式的添加了一个+,像-2H30M这种意思就是减去2个小时再加30分钟,那就是1个小时30分钟咯;但是如果在P前面加一个-的话,会对里面所有的数字都会产生影响。其实就是像小学数学一样,用括号把PnDTnHnMn.nS括起来,(PnDTnHnMn.nS),在最外面加一个-,那里面的符号就全部取反了,但是单独在某个数字前加的话只会影响到它自己。

Period

PeriodDuration其实用法差不多,都是表达时间上的差值。只不过一个是表达日期,一个是表达时间,粒度不同。就不演示了。

总结

Java 8 的日期处理 API 常用的功能方法总结得差不多了,基本上日常使用的也就大概这么多了,这一套日期 API 搞清楚它们的区别和背后逻辑后用起来很方便,本文没有讲到的用法,开发使用的时候查一下文档也可以马上用起来了,底层都是这些基础的知识点。

上一篇:玩转Linux网络namespace-单机自环測试与策略路由


下一篇:获取上月日期