不做软件项目可能永远也不会考虑日期和时间的使用问题。上学时做的最多的是数学模型和算法的仿真验证,根本不用操心软件里如何表示日期和时间,或者如何计算两个时间或日期间的差(小学数学的水平吧),但实际工作中,却会有大量的需要考虑日期和时间的表示和计算问题,稍有不慎就会犯下错误,如果再是个“国际项目”,带上时区再做处理,那可就不是随便找个小学生都能做的问题了。
据说,Java 8以前的API对日期和时间的支持就不是很好(java.util.Date以及java.util.Calendar),广遭诟病,其中就有用0 - 11表示1月至12月的奇葩设计,带来了很多后续处理的问题,以致很多码农表示日期时间要么用数组,要么自建,要么转投第三方库。
Java 8 在日期和时间处理上利用“后发优势”,不继续在java.util包内“修修补补”,推出java.time包“另起炉灶”。这里就简单介绍Java 8的日期和时间API,推荐一本Java 8教程,并给出两个特殊的应用示例。
1. Java 8的日期和时间
LocalDate、LocalTime、Instant、Duration及Period是Java 8提供的日期和时间API。关于它的使用,网上很多博客在讲(应该有讲的好的),比较推荐的是《Java 8 实战》(《Java 8 in Action》),书上讲的比较系统,有了全面掌握后,如果日后工作中再有问题就能针对性地百度下,提高效率。
《Java 8 in Action》英文版微盘下载
嫌看着费劲,就跟我一样去天猫买个中文版的吧-
2. 应用场景
- LocalDate、LocalTime、LocalDateTime
从关键词即可推测出,LocalDate用于日期,如:“2017-07-14”;
LocalDate d1 = LocalDate.of(2017, 7, 14);
LocalDate d2 = LocalDate.parse("2017-07-14");
LocalDate d3 = LocalDate.parse("2017-07-14", DateTimeFormatter.ofPattern("yyyy-M-d"));
- LocalTime用于时间,如:“12:01:56”;
LocalTime t1 = LocalTime.of(12, 01, 56);
LocalTime t2 = LocalTime.parse("12:01:56");
LocalTime t3 = LocalTime.parse("12:1:56", DateTimeFormatter.ofPattern("H:m:s"));
- LocalDateTime用于既有日期又有时间,如:“2017-07-14T12:01:56”,还可以表示到精度为纳秒级,带时区的格式。
LocalDateTime dt1 = LocalDateTime.of(2017, 7, 14, 12, 01, 56);
LocalDateTime dt2 = LocalDateTime.parse("2017-07-14T12:01:56");
LocalDateTime dt3 = LocalDateTime.parse("2017-07-14 12:01:56",
DateTimeFormatter.ofPattern("yyyy-M-d H:m:s"));
字符“T”在后面介绍的几种DateTimeFormatter提供的ISO标准中都用,具体含义没去查证,猜测可能是单词“Time”的缩写。如果不希望在输入字符串组中包含字母T,那就按照上述示例,利用ofPattern定制一个日期时间格式器即可。
- Period、Duration及Instant
有了日期和时间,那么比较两个日期或时间之间的差异, 或者计算两者间的时间差就是再自然不过的需求了。
Period用于比较两个具体日期的年、月、日的差值,如:"1997-07-01"至"2017-08-11"的日期差为“P20Y1M10D”(20 years, 1 month and 10days);
LocalDate d1 = LocalDate.of(1997, 7, 1);
LocalDate d2 = LocalDate.parse("2017-08-11");
System.out.println(Period.between(d1, d2));
Duration用于比较两个时间的秒或纳秒级的差值,如:“12:01:00”与“12:01:30”的时间为“PT30S”(30 seconds);
LocalTime t1 = LocalTime.of(12, 01, 00);
LocalTime t2 = LocalTime.parse("12:01:30");
System.out.println(Duration.between(t1, t2));
Instant用于测试时比较程序中两个时间标签之间的差值,比如某个耗时操作前后的时间差值。
Instant start = Instant.now();
while (true) {
try {
Thread.sleep(new Random().nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis()); // output: 0 - 2000
注意:Instant.now()获取的时间主要是为了设定时间标签,便于日后测试两个时间标签间的时长,并不能看做准确的当下时间。
-
DateTimeFormatter
DateTimeFormatter不仅可以通过ofPattern自定义日期时间格式,还有一些标准化的格式可供使用。可根据需要参照javadoc进行使用。
稍微花点时间熟悉该API的使用方法和异常处理,并根据应用场景选用Java 8日期时间API,可以提高编码效率,减少不必要的错误。
3. 典型应用
- 获取两个日期间的天数
假如需要求解两个日期节点间的天数,如果按照javadoc上文档说明,很可能第一反应使用Period去计算两个日期的差,然后再通过调用getDays获取总天数。这种做法在同年同月的处理时没有问题的,比如香港回归纪念日和今天的日期差:
LocalDate start = LocalDate.parse("2017-07-01");
LocalDate end = LocalDate.parse("2017-07-14");
System.out.println(Period.between(start, end).getDays()); // output: 13
但如果不同月不同年就会出现问题,比如香港回归纪念日和建军90周年的日期差:
LocalDate start = LocalDate.parse("2017-07-01");
LocalDate end = LocalDate.parse("2017-08-01");
System.out.println(Period.between(start, end).getDays()); // output: 0
原因很简单,因为静态方法Period.between(LocalDatestart DateInclusive, LocalDate endDateExclusive)返回值是Period,而刚才也讲到,Period的返回值是“P{年数}Y{月数}M{日数}D”,调用的getDays方法实际返回的就是这个{日数},所以输出的上述结果就不足为奇了(p.s:承认还是英文太差,读不懂javadoc,对于嘲讽只能呵呵了)。
当然,Period的这种表示在很多情况下还是有用的,但实际中还是有时需要具体知道两个日期间差了多少天,有没有官方支持啊,总不至于对相差的每个月判断大小月、如果里面刚好还有个2月,还要再判断个闰年后再相加吧。
显然不会的。使用LocalDate的until(Temporal endExclusive, TemporalUnit unit) : long,演示一下:
LocalDate start = LocalDate.parse("2016-01-01");
LocalDate end = LocalDate.parse("2017-01-01");
System.out.println(start.until(end, ChronoUnit.DAYS)); // output: 366
start = LocalDate.parse("2017-07-01");
end = LocalDate.parse("2017-08-01");
System.out.println(start.until(end, ChronoUnit.DAYS)); // output: 31
ChronoUnit还有其它的计算单元,可以逐一试试,但要注意,如果调用until方法的实例不支持该计算单元就会报出UnsupportedTemporalTypeException异常,比如在上面的例子中将“ChronoUnit.DAYS”改为“ChronoUnit.HOURS”。
- 解析日期
习惯上对日期的解析都是通过字符串进行的,那么这时候如果选用的parse方法或者DateTimeFormatter的格式设置有误也是会报错的。比如有时我们为了简便,会用“2017-1-1”表示今年元旦,但如果用默认parse方法解析会成功吗?
LocalDate newYear = LocalDate.parse("2017-1-1");
System.out.println(newYear);
那用DateTimeFormatter的ofPattern方法自定义一个应该就可以了吧
LocalDate newYear = LocalDate.parse("2017-1-1", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
System.out.println(newYear);
其中,字母“y”表示年,“M”表示月,“d”表示日。
不好意思,还是报出异常:
其实上述做法在解析“2017-01-01”或者其它两位表示的“月”和“日”的时候是完全正确的,但一旦图省事或者不巧遇到粗心汉子对单数日就不想在前面补“0”,那么错误就发生了。
为了避免这种情况,正确的做法是:
LocalDate newYear = LocalDate.parse("2017-1-1", DateTimeFormatter.ofPattern("yyyy-M-d"));
System.out.println(newYear); // output: 2017-01-01
LocalDate christmas = LocalDate.parse("2017-12-25", DateTimeFormatter.ofPattern("yyyy-M-d"));
System.out.println(christmas); // output: 2017-12-25
插句题外话,前一段时间就是这样的错误让我在启动的异步线程中发生了错误,而由于采用了回调机制,回调方法认为此时执行失败直接返回failed方法,加之自己也没有在此做异常捕获,没有任何异常提示,不过还好日志做的比较完善,基本能够很快定位,但由于压根没想到这里会出问题,足足花了一下午时间逐行排查才找到问题原因。
目前遇到的就这么多了,以后碰到其它再补充。
2017-07-19 补充:
日期格式设置为"y-M-d"和"yy-M-d"的使用. 当使用前者时, 会按照输入的实际年份进行解析; 当使用后者时, 会自动为年份增加2000, 即: 千禧年之后的年份.
String d1= "1-1-1";
System.out.println(LocalDate.parse(d1, DateTimeFormatter.ofPattern("y-M-d"))); // output: 0001-01-01
String d2= "11-1-1";
System.out.println(LocalDate.parse(d2, DateTimeFormatter.ofPattern("y-M-d"))); // ouput: 0011-01-01
String d3 = "2011-1-1";
System.out.println(LocalDate.parse(d3, DateTimeFormatter.ofPattern("y-M-d"))); // output: 2011-01-01
String d3= "11-1-1";
System.out.println(LocalDate.parse(d3, DateTimeFormatter.ofPattern("yy-M-d"))); // output: 2011-01-01
以上都是正常的输出. 但也有些无法解析并抛出异常, 典型的有:
String de1 = "1-1-1";
System.out.println(LocalDate.parse(de1, DateTimeFormatter.ofPattern("yy-M-d"))); // 若业务场景中遇见两位表示的年份值且需要处理2000 - 2009年的对象, 则需要考虑为年份值补0.
END