Java8系列 (六) 新的日期和时间API

概述

在Java8之前, 我们一般都是使用 SimpleDateFormat 来解析和格式化日期时间, 但它是线程不安全的。

    @Test
    public void test() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    Date date = sdf.parse("20191103091515");
                    System.out.println(date.toString());
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }

多次运行上面这段程序, 会报不同的异常, 下面是其中的一种

Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-3" java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.java8.action.Demo.lambda$test$0(Demo.java:25)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

原因也很简单, 查看一下源码, 发现 SimpleDateFormat 类继承了父类 DateFormat 的成员变量  protected Calendar calendar; , 而Calendar 类没有被 final 修饰, 是可以被修改的。

回到上面这个问题, 看一下 SimpleDateFormat 的解析日期时间的API

Java8系列  (六)  新的日期和时间API

进入 establish() 方法里面看一下

Java8系列  (六)  新的日期和时间API

到此, 已经基本明了, 因为每次 SimpleDateFormat 解析日期时间都会清空一下它的成员变量 calendar 的值, 所以当多个线程并发访问同一个 SimpleDateformat 时, 就会有线程不安全问题。

解决方式也很简单, 你可以使用 ThreadLocal 类存放 SimpleDateFormat 对象, 让每个线程拥有自己的SimpleDateFormat对象。

    /**Map键对应不同的解析规则字符串, 比如yyyyMMdd*/
    private static Map<String, ThreadLocal<SimpleDateFormat>> tl = new HashMap<>();

回到我们今天的主题, 在Java8中引入了新的日期和时间API, 这也是下面要介绍的内容。

新的日期时间类都被 final 修饰, 不存在想上面介绍的老版本API的线程不安全问题。

LocalDate、LocalTime和LocalDateTime

LocalDate和LocalTime, LocalDateTime 提供了许多静态工厂方法来创建它们的实例对象, 并且这三者之间可以很方便的互相进行类型转换。

    @Test
    public void test() {
        //静态方法创建对象
        LocalDate ld = LocalDate.of(2019, 10, 3);
        System.out.println(ld.getYear() + "	" + ld.getMonth() + "	" + ld.getDayOfMonth() + "	" + ld.getDayOfWeek() + "	" + ld.lengthOfMonth() + "	" + ld.isLeapYear());//result: 2019    OCTOBER    3    THURSDAY    31    false
        LocalDate now = LocalDate.now();
        System.out.println(now.get(ChronoField.YEAR) + "	" + now.get(ChronoField.MONTH_OF_YEAR) + "	" + now.get(ChronoField.DAY_OF_MONTH));//result: 2019    11    3

        LocalTime lt = LocalTime.of(20, 44, 12);
        System.out.println(lt.getHour() + "	" + lt.getMinute() + "	" + lt.getSecond());//result: 20    44    12
        //解析字符串
        LocalDate ld2 = LocalDate.parse("2019-10-05");//默认格式: yyyy-MM-dd
        System.out.println(ld2.toString());//result: 2019-10-05
        LocalTime lt2 = LocalTime.parse("20:42:12.828");//默认格式: HH:mm:ss.SSS
        System.out.println(lt2.toString());//result: 20:42:12.828
        //互相进行类型转换
        LocalDateTime ldt = LocalDateTime.of(2019, 10, 5, 21, 12, 10, 888).atZone(ZoneId.of("Asia/Shanghai")).toLocalDateTime();
        LocalDateTime ldt2 = LocalDateTime.of(ld2, lt2);// 2019-10-05T20:42:12.828
        LocalDateTime ldt3 = ld2.atTime(10, 10, 10);// 2019-10-05T10:10:10
        LocalDateTime ldt4 = ld2.atTime(lt2);
        LocalDateTime ldt5 = lt2.atDate(ld2);
        LocalDateTime ldt6 = LocalDateTime.parse("2019/10/05 20:20:20.888", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS"));
        LocalDate ld6 = ldt6.toLocalDate();
        LocalTime lt6 = ldt6.toLocalTime();
    }

Instant

Instant对时间的建模方式是以UTC时区的1970年1月1日午夜时分开始所经历的秒数进行计算,它不包含时区信息。Instant类是为了方便计算机处理日期和时间而设计的。

 Instant.now().toEpochMilli()  可以获取当前时间的时间戳, 另外, Instant 提供了类似 ofEpochMilli() 的方法根据某个时间戳获取 Instant 实例, isBefore()和isAfter() 则用来比较两个 Instant 的大小

    @Test
    public void test2() {
        long milli = Instant.now().toEpochMilli();//获取当前时间戳
        Instant instant = Instant.ofEpochMilli(1572749169937L);//根据某个时间戳获取Instant实例
        Instant instant2 = instant.minusSeconds(1000L);
        System.out.println(instant.isAfter(instant2));//true
    }

Duration和Period

Duration 对象用秒和纳秒来衡量时间的长短,如果想要对多个时间对象进行日期运算,可以用 Period 类

    @Test
    public void test3() {
        Duration d1 = Duration.between(LocalDateTime.of(2019, 10, 7, 15, 55, 55, 888), LocalDateTime.now());
        Duration d2 = Duration.between(LocalTime.of(17, 55, 10), LocalTime.now());
        Duration d3 = Duration.between(Instant.ofEpochMilli(1570544602000L), Instant.now());
        System.out.println(d3.toHours());// 612
        //Duration对象用秒和纳秒来衡量时间的长短,所以入参不能使用LocalDate类型, 否则抛UnsupportedTemporalTypeException: Unsupported unit: Seconds
        //Duration.between(LocalDate.of(2019, 10, 7), LocalDate.now());

        //如果想要对多个时间对象进行日期运算,可以用Period
        Period p1 = Period.between(LocalDate.of(2018, 8, 30), LocalDate.now());
        System.out.println(p1.getYears() + "	" + p1.getMonths() + "	" + p1.getDays());// 1    2    4
        //工厂方法介绍
        Duration threeMinutes = Duration.ofMinutes(3);
        threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
        Period tenDays = Period.ofDays(10);
        Period threeWeeks = Period.ofWeeks(3);
        Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
    }

Temporal接口

LocalDate、LocalTime、LocalDateTime、Instant类都实现了 Temporal 接口,有很多通用的处理日期和时间的方法,比如plus(), minus(), with()

    @Test
    public void test4() {
        LocalDate ld = LocalDate.of(2019, 10, 7);
        //修改时间对象的某个属性值,返回一个新的对象
        LocalDate ld2 = ld.withDayOfYear(365);//2019-12-31
        LocalDate ld3 = ld.withDayOfMonth(18);//2019-10-18
        LocalDate ld4 = ld.with(ChronoField.MONTH_OF_YEAR, 8);//2019-08-07
        //对时间对象进行加减运算
        LocalDate ld5 = ld.plusWeeks(2L);//2019-10-21
        LocalDate ld6 = ld.minusYears(9L);//2010-10-07
        LocalDate ld7 = ld.plus(Period.ofMonths(2));//2019-12-07
        LocalDate ld8 = ld.plus(2L, ChronoUnit.MONTHS);//2019-12-07

        LocalTime lt = LocalTime.parse("10:10:10.888");
        LocalTime lt1 = lt.plus(Duration.ofHours(2L));//12:10:10.888
        LocalTime lt2 = lt.plus(120L, ChronoUnit.MINUTES);//12:10:10.888
    }

TemporalAdjuster接口

TemporalAdjuster 类提供了更多对日期定制化操作的功能, 诸如将日期调整到下个工作日、本月的最后的一天、今年的第一天等等。

    @Test
    public void test5() {
        LocalDate ld = LocalDate.of(2019, 10, 7);
        LocalDate ld1 = ld.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));//2019-10-11
        LocalDate ld2 = ld.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));//2019-10-07
        LocalDate ld3 = ld.with(TemporalAdjusters.firstDayOfNextMonth());//2019-11-01

        //自定义TemporalAdjuster, 来计算下一个工作日所在的日期
        LocalDate ld4 = LocalDate.of(2019, 10, 11).with(temporal -> {
            DayOfWeek now = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
            long dayToAdd = now.equals(DayOfWeek.FRIDAY) ? 3L : now.equals(DayOfWeek.SATURDAY) ? 2L : 1L;
            return temporal.plus(dayToAdd, ChronoUnit.DAYS);
        });//2019-10-14
        //对于经常复用的相同操作,可以将逻辑封装一个类中
        TemporalAdjuster temporalAdjuster = TemporalAdjusters.ofDateAdjuster(temporal -> {
            DayOfWeek now = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
            long dayToAdd = now.equals(DayOfWeek.FRIDAY) ? 3L : now.equals(DayOfWeek.SATURDAY) ? 2L : 1L;
            return temporal.plus(dayToAdd, ChronoUnit.DAYS);
        });
    }

DateTimeFormatter

DateTimeFormatter 用于进行可定制的日期时间格式化, 功能相当于以前的 SimpleDateFormat 类

    @Test
    public void test6() {
        //日期转字符串
        LocalDate ld = LocalDate.of(2019, 10, 7);
        String s1 = ld.format(DateTimeFormatter.BASIC_ISO_DATE);//20191007
        String s2 = ld.format(DateTimeFormatter.ISO_LOCAL_DATE);//2019-10-07
        //字符串转日期
        LocalDateTime ld1 = LocalDateTime.parse("2019-10-07 22:22:22.555", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
    }

ZoneId时区

ZoneId 是老版本的 TimeZone 的替代品, ZonedDateTime 代表了相对于指定时区的时间点

    @Test
    public void test7() {
        //LocalDate、LocalDateTime、Instant 转 ZonedDateTime
        ZonedDateTime zdt1 = LocalDate.of(2019, 10, 7).atStartOfDay(ZoneId.systemDefault());
        ZonedDateTime zdt2 = LocalDateTime.of(2019, 10, 7, 15, 55, 55, 888).atZone(ZoneId.of("Asia/Shanghai"));
        ZonedDateTime zdt3 = Instant.now().atZone(ZoneId.of("Asia/Yerevan"));

        //Instant转LocalDateTime
        LocalDateTime ldt1 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
        //下面的两个栗子介绍了ZoneOffset,他是利用和 UTC/格林尼治时间的固定偏差计算时区,但不推荐使用,因为ZoneOffset并未考虑任何夏令时的影响
        LocalDateTime ldt2 = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.of("+8"));
        //LocalDateTime转Instant
        Instant instant = LocalDateTime.of(2019, 10, 7, 15, 55, 55).toInstant(ZoneOffset.of("+4"));
    }

参考资料

Java8 实战

SimpleDateFormat线程不安全及解决办法

Java进阶(七)正确理解Thread Local的原理与适用场景

作者:张小凡
出处:https://www.cnblogs.com/qingshanli/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。