Java的日期和时间API使用备忘

不做软件项目可能永远也不会考虑日期和时间的使用问题。上学时做的最多的是数学模型和算法的仿真验证,根本不用操心软件里如何表示日期和时间,或者如何计算两个时间或日期间的差(小学数学的水平吧),但实际工作中,却会有大量的需要考虑日期和时间的表示和计算问题,稍有不慎就会犯下错误,如果再是个“国际项目”,带上时区再做处理,那可就不是随便找个小学生都能做的问题了。

据说,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的日期和时间

java.time包

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进行使用。


    DateTimeFormmatter提供的标准日期时间格式

稍微花点时间熟悉该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);
默认解析“2017-1-1”报出异常

那用DateTimeFormatter的ofPattern方法自定义一个应该就可以了吧

LocalDate newYear = LocalDate.parse("2017-1-1", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
System.out.println(newYear);

其中,字母“y”表示年,“M”表示月,“d”表示日。
不好意思,还是报出异常:

使用“yyyy-MM-dd”解析“2017-1-1”报出异常

其实上述做法在解析“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

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

推荐阅读更多精彩内容