前面一篇文章写了《SimpleDateFormat 如何安全的使用?》, 里面介绍了 SimpleDateFormat 如何处理日期/时间,以及如何保证线程安全,及其介绍了在 Java 8 中的处理时间/日期默认就线程安全的 DateTimeFormatter 类。那么 Java 8 中该怎么样处理生活中常见的一些日期/时间呢?好比:计算一周后的日期;计算一年前或一年后的日期;检查闰年等。java
接下来建立了 20 个基于任务的实例来学习 Java 8 的新特性。从最简单建立当天的日期开始,而后建立时间及时区,接着模拟一个日期提醒应用中的任务——计算重要日期的到期天数,例如生日、记念日、帐单日、保费到期日、信用卡过时日等。面试
示例 一、在 Java 8 中获取今天的日期segmentfault
Java 8 中的 LocalDate 用于表示当天日期。和 java.util.Date 不一样,它只有日期,不包含时间。当你仅须要表示日期时就用这个类。安全
1LocalDate now = LocalDate.now(); 2System.out.println(now);
结果是:多线程
12018-06-20
上面的代码建立了当天的日期,不含时间信息。打印出的日期格式很是友好,不像老的 Date 类打印出一堆没有格式化的信息。app
LocalDate 类提供了获取年、月、日的快捷方法,其实例还包含不少其它的日期属性。经过调用这些方法就能够很方便的获得须要的日期信息,不用像之前同样须要依赖 java.util.Calendar 类了框架
1LocalDate now = LocalDate.now(); 2int year = now.getYear(); 3int monthValue = now.getMonthValue(); 4int dayOfMonth = now.getDayOfMonth(); 5System.out.printf("year = %d, month = %d, day = %d", year, monthValue, dayOfMonth);
结果是:工具
1year = 2018, month = 6, day = 20
在第一个例子里,咱们经过静态工厂方法 now() 很是容易地建立了当天日期,你还能够调用另外一个有用的工厂方法LocalDate.of() 建立任意日期, 该方法须要传入年、月、日作参数,返回对应的 LocalDate 实例。这个方法的好处是没再犯老 API 的设计错误,好比年度起始于 1900,月份是从 0 开始等等。日期所见即所得,就像下面这个例子表示了 6 月 20 日,没有任何隐藏机关。单元测试
1LocalDate date = LocalDate.of(2018, 06, 20); 2System.out.println(date);
能够看到建立的日期彻底符合预期,与写入的 2018 年 6 月 20 日彻底一致。学习
现实生活中有一类时间处理就是判断两个日期是否相等。你经常会检查今天是否是个特殊的日子,好比生日、记念日或非交易日。这时就须要把指定的日期与某个特定日期作比较,例如判断这一天是不是假期。下面这个例子会帮助你用 Java 8 的方式去解决,你确定已经想到了,LocalDate 重载了 equal 方法,请看下面的例子:
1LocalDate now = LocalDate.now(); 2LocalDate date = LocalDate.of(2018, 06, 20); 3if (date.equals(now)) { 4 System.out.println("同一天"); 5}
这个例子中咱们比较的两个日期相同。注意,若是比较的日期是字符型的,须要先解析成日期对象再做判断。
Java 中另外一个日期时间的处理就是检查相似每个月帐单、结婚记念日、EMI日或保险缴费日这些周期性事件。若是你在电子商务网站工做,那么必定会有一个模块用来在圣诞节、感恩节这种节日时向客户发送问候邮件。Java 中如何检查这些节日或其它周期性事件呢?答案就是 MonthDay 类。这个类组合了月份和日,去掉了年,这意味着你能够用它判断每一年都会发生事件。和这个类类似的还有一个 YearMonth 类。这些类也都是不可变而且线程安全的值类型。下面咱们经过 MonthDay 来检查周期性事件:
1LocalDate now = LocalDate.now(); 2LocalDate dateOfBirth = LocalDate.of(2018, 06, 20); 3MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth()); 4MonthDay currentMonthDay = MonthDay.from(now); 5if (currentMonthDay.equals(birthday)) { 6 System.out.println("Happy Birthday"); 7} else { 8 System.out.println("Sorry, today is not your birthday"); 9}
结果:(注意:获取当前时间可能与你看的时候不对,因此这个结果可能和你看的时候运行结果不同)
1Happy Birthday
只要当天的日期和生日匹配,不管是哪一年都会打印出祝贺信息。你能够把程序整合进系统时钟,看看生日时是否会受到提醒,或者写一个单元测试来检测代码是否运行正确。
与 Java 8 获取日期的例子很像,获取时间使用的是 LocalTime 类,一个只有时间没有日期的 LocalDate 近亲。能够调用静态工厂方法 now() 来获取当前时间。默认的格式是 hh:mm:ss:nnn。
1LocalTime localTime = LocalTime.now(); 2System.out.println(localTime);
结果:
113:35:56.155
能够看到当前时间就只包含时间信息,没有日期。
经过增长小时、分、秒来计算未来的时间很常见。Java 8 除了不变类型和线程安全的好处以外,还提供了更好的plusHours() 方法替换 add(),而且是兼容的。注意,这些方法返回一个全新的 LocalTime 实例,因为其不可变性,返回后必定要用变量赋值。
1LocalTime localTime = LocalTime.now(); 2System.out.println(localTime); 3LocalTime localTime1 = localTime.plusHours(2);//增长2小时 4System.out.println(localTime1);
结果:
113:41:20.721 215:41:20.721
能够看到,新的时间在当前时间 13:41:20.721 的基础上增长了 2 个小时。
和上个例子计算两小时之后的时间相似,这个例子会计算一周后的日期。LocalDate 日期不包含时间信息,它的 plus()方法用来增长天、周、月,ChronoUnit 类声明了这些时间单位。因为 LocalDate 也是不变类型,返回后必定要用变量赋值。
1LocalDate now = LocalDate.now(); 2LocalDate plusDate = now.plus(1, ChronoUnit.WEEKS); 3System.out.println(now); 4System.out.println(plusDate);
结果:
12018-06-20 22018-06-27
能够看到新日期离当天日期是 7 天,也就是一周。你能够用一样的方法增长 1 个月、1 年、1 小时、1 分钟甚至一个世纪,更多选项能够查看 Java 8 API 中的 ChronoUnit 类。
继续上面的例子,上个例子中咱们经过 LocalDate 的 plus() 方法增长天数、周数或月数,这个例子咱们利用 minus() 方法计算一年前的日期。
1LocalDate now = LocalDate.now(); 2LocalDate minusDate = now.minus(1, ChronoUnit.YEARS); 3LocalDate plusDate1 = now.plus(1, ChronoUnit.YEARS); 4System.out.println(minusDate); 5System.out.println(plusDate1);
结果:
12017-06-20 22019-06-20
Java 8 增长了一个 Clock 时钟类用于获取当时的时间戳,或当前时区下的日期时间信息。之前用到System.currentTimeInMillis() 和 TimeZone.getDefault() 的地方均可用 Clock 替换。
1Clock clock = Clock.systemUTC(); 2Clock clock1 = Clock.systemDefaultZone(); 3System.out.println(clock); 4System.out.println(clock1);
结果:
1SystemClock[Z] 2SystemClock[Asia/Shanghai]
另外一个工做中常见的操做就是如何判断给定的一个日期是大于某天仍是小于某天?在 Java 8 中,LocalDate 类有两类方法 isBefore() 和 isAfter() 用于比较日期。调用 isBefore() 方法时,若是给定日期小于当前日期则返回 true。
1 LocalDate tomorrow = LocalDate.of(2018,6,20); 2 if(tomorrow.isAfter(now)){ 3 System.out.println("Tomorrow comes after today"); 4 } 5 LocalDate yesterday = now.minus(1, ChronoUnit.DAYS); 6 if(yesterday.isBefore(now)){ 7 System.out.println("Yesterday is day before today"); 8 }
在 Java 8 中比较日期很是方便,不须要使用额外的 Calendar 类来作这些基础工做了。
Java 8 不只分离了日期和时间,也把时区分离出来了。如今有一系列单独的类如 ZoneId 来处理特定时区,ZoneDateTime 类来表示某时区下的时间。这在 Java 8 之前都是 GregorianCalendar 类来作的。
1ZoneId america = ZoneId.of("America/New_York"); 2LocalDateTime localtDateAndTime = LocalDateTime.now(); 3ZonedDateTime dateAndTimeInNewYork = ZonedDateTime.of(localtDateAndTime, america ); 4System.out.println(dateAndTimeInNewYork);
与 MonthDay 检查重复事件的例子类似,YearMonth 是另外一个组合类,用于表示信用卡到期日、FD 到期日、期货期权到期日等。还能够用这个类获得 当月共有多少天,YearMonth 实例的 lengthOfMonth() 方法能够返回当月的天数,在判断 2 月有 28 天仍是 29 天时很是有用。
1YearMonth currentYearMonth = YearMonth.now(); 2System.out.printf("Days in month year %s: %d%n", currentYearMonth, currentYearMonth.lengthOfMonth()); 3YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY); 4System.out.printf("Your credit card expires on %s %n", creditCardExpiry);
结果:
1Days in month year 2018-06: 30 2Your credit card expires on 2018-02
LocalDate 类有一个很实用的方法 isLeapYear() 判断该实例是不是一个闰年。
有一个常见日期操做是计算两个日期之间的天数、周数或月数。在 Java 8 中能够用 java.time.Period 类来作计算。下面这个例子中,咱们计算了当天和未来某一天之间的月数。
1LocalDate date = LocalDate.of(2019, Month.MARCH, 20); 2Period period = Period.between(now, date); 3System.out.println("离下个时间还有" + period.getMonths() + " 个月");
在 Java 8 中,ZoneOffset 类用来表示时区,举例来讲印度与 GMT 或 UTC 标准时区相差 +05:30,能够经过ZoneOffset.of() 静态方法来获取对应的时区。一旦获得了时差就能够经过传入 LocalDateTime 和 ZoneOffset 来建立一个 OffSetDateTime 对象。
1LocalDateTime datetime = LocalDateTime.of(2014, Month.JANUARY, 14,19,30); 2ZoneOffset offset = ZoneOffset.of("+05:30"); 3OffsetDateTime date = OffsetDateTime.of(datetime, offset); 4System.out.println("Date and Time with timezone offset in Java : " + date);
若是你还记得 Java 8 之前是如何得到当前时间戳,那么如今你终于解脱了。Instant 类有一个静态工厂方法 now() 会返回当前的时间戳,以下所示:
1Instant timestamp = Instant.now(); 2System.out.println(timestamp);
结果:
12018-06-20T06:35:24.881Z
时间戳信息里同时包含了日期和时间,这和 java.util.Date 很像。实际上 Instant 类确实等同于 Java 8 以前的 Date类,你可使用 Date 类和 Instant 类各自的转换方法互相转换,例如:Date.from(Instant) 将 Instant 转换成java.util.Date,Date.toInstant() 则是将 Date 类转换成 Instant 类。
在 Java 8 之前的世界里,日期和时间的格式化很是诡异,惟一的帮助类 SimpleDateFormat 也是非线程安全的,并且用做局部变量解析和格式化日期时显得很笨重。幸亏线程局部变量能使它在多线程环境中变得可用,不过这都是过去时了。Java 8 引入了全新的日期时间格式工具,线程安全并且使用方便。它自带了一些经常使用的内置格式化工具。
参见我上一篇文章: 《SimpleDateFormat 如何安全的使用?》
尽管内置格式化工具很好用,有时仍是须要定义特定的日期格式。能够调用 DateTimeFormatter 的 ofPattern() 静态方法并传入任意格式返回其实例,格式中的字符和之前表明的同样,M 表明月,m 表明分。若是格式不规范会抛出 DateTimeParseException 异常,不过若是只是把 M 写成 m 这种逻辑错误是不会抛异常的。
参见我上一篇文章: 《SimpleDateFormat 如何安全的使用?》
上两个主要是从字符串解析日期。如今咱们反过来,把 LocalDateTime 日期实例转换成特定格式的字符串。这是迄今为止 Java 日期转字符串最为简单的方式了。下面的例子将返回一个表明日期的格式化字符串。和前面相似,仍是须要建立 DateTimeFormatter 实例并传入格式,但这回调用的是 format() 方法,而非 parse() 方法。这个方法会把传入的日期转化成指定格式的字符串。
1LocalDateTime arrivalDate = LocalDateTime.now(); 2try { 3 DateTimeFormatter format = DateTimeFormatter.ofPattern("MMMdd yyyy hh:mm a"); 4 String landing = arrivalDate.format(format); 5 System.out.printf("Arriving at : %s %n", landing); 6}catch (DateTimeException ex) { 7 System.out.printf("%s can't be formatted!%n", arrivalDate); 8 ex.printStackTrace(); 9}
经过这些例子,你确定已经掌握了 Java 8 日期时间 API 的新知识点。如今来回顾一下这个优雅 API 的使用要点:
1)提供了 javax.time.ZoneId 获取时区。
2)提供了 LocalDate 和 LocalTime 类。
3)Java 8 的全部日期和时间 API 都是不可变类而且线程安全,而现有的 Date 和 Calendar API 中的 java.util.Date 和SimpleDateFormat 是非线程安全的。
4)主包是 java.time, 包含了表示日期、时间、时间间隔的一些类。里面有两个子包 java.time.format 用于格式化, java.time.temporal 用于更底层的操做。
5)时区表明了地球上某个区域内广泛使用的标准时间。每一个时区都有一个代号,格式一般由区域/城市构成(Asia/Tokyo),在加上与格林威治或 UTC 的时差。例如:东京的时差是 +09:00。
6)OffsetDateTime 类实际上组合了 LocalDateTime 类和 ZoneOffset 类。用来表示包含和格林威治或 UTC 时差的完整日期(年、月、日)和时间(时、分、秒、纳秒)信息。
7)DateTimeFormatter 类用来格式化和解析时间。与 SimpleDateFormat 不一样,这个类不可变而且线程安全,须要时能够给静态常量赋值。DateTimeFormatter 类提供了大量的内置格式化工具,同时也容许你自定义。在转换方面也提供了 parse() 将字符串解析成日期,若是解析出错会抛出 DateTimeParseException。DateTimeFormatter 类同时还有format() 用来格式化日期,若是出错会抛出 DateTimeException异常。
8)再补充一点,日期格式“MMM d yyyy”和“MMM dd yyyy”有一些微妙的不一样,第一个格式能够解析“Jan 2 2014”和“Jan 14 2014”,而第二个在解析“Jan 2 2014”就会抛异常,由于第二个格式里要求日必须是两位的。若是想修正,你必须在日期只有个位数时在前面补零,就是说“Jan 2 2014”应该写成 “Jan 02 2014”。
推荐阅读:
为何选择 Spring 做为 Java 框架?
SpringBoot RocketMQ 整合使用和监控
Elasticsearch实战 | 必要的时候,还得空间换时间!
干货 |《从Lucene到Elasticsearch全文检索实战》拆解实践
上篇好文:
JVM面试问题系列:JVM 配置经常使用参数和经常使用 GC 调优策略