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

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

run.png

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

format.png

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

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

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

parse.png

易用性

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

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

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

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

date.png

Java 8 新日期 API

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

下面来看看 Java 8 主要提供了哪一些常用的日期 API。


new.png

从上图可以看到,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()));
    }
datestr.png

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

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

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

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

time.png

instant.png

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

从最后月份的输出可以看出,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());
    }
duration.png

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

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

Period

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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容