以前总觉得在Java里面处理各种日期转换很麻烦,虽然我也封装过一些工具包,但是总体感觉还是不够完美,也使用过一些开源的apachecommons里面的时间处理工具类感觉还是不够灵活和强大。
第一次与Joda-Time有一面之缘的时候,是在我使用ELK框架给公司做日志收集分析检索系统的时候,我发现使用jruby重写的logstash里面使用的时间处理工具类是joda-time,当时就对这个框架有点好奇,只不过没太注意,至于为什么会发现?相信搞过ELK的都知道logstash的时间处理比较独特,或者说不了解的情况下,可能会有点麻烦,当然logstash的作者,曾经开玩笑的说过一句话,新手遇到ELK里面的困难,第一肯定是栽在logstash里面,各种input,gork,filter,output插件总能有一款让你手无足措,额,有点扯远了,言归正传。如果说在logstash发现Joda-Time,并没引起我的兴趣,但是后来,我在hadoop,solr里面都发现有Joda-Time的踪影,而hadoop和solr恰恰当今最流行的开源框架,所以就抽空看了看这个精美的小工具,发现确实很不错,下面这篇文章是来自一位朋友的笔记:
先抛个问题:
求任何一年中的第 11 月的第一个星期二的日期,而这天必须是在这个月的第一个星期一之后。
大家思考下用你们各自擅长的语言实现,需要多少代码量?
答案在文末文章中,有例子。下面是正文:
任何企业应用程序都需要处理时间问题。应用程序需要知道当前的时间点和下一个时间点,有时它们还必须计算这两个时间点之间的路径。使用 JDK 完成这项任务将非常痛苦和繁琐。现在来看看 Joda Time,一个面向 Java™ 平台的易于使用的开源时间/日期库。正如您在本文中了解的那样,Joda-Time 轻松化解了处理日期和时间的痛苦和繁琐。
在编写企业应用程序时,我常常需要处理日期。并且在我的最新项目中 — 保险行业 — 纠正日期计算尤其重要。使用 java.util.Calendar 让我有些不安。如果您也曾使用这个类处理过日期/时间值,那么您就知道它使用起来有多麻烦。因此当我接触到 Joda-Time — 面向 Java 应用程序的日期/时间库的替代选择 — 我决定研究一下。其结果是:我很庆幸我这么做了。
Joda-Time 令时间和日期值变得易于管理、操作和理解。事实上,易于使用是 Joda 的主要设计目标。其他目标包括可扩展性、完整的特性集以及对多种日历系统的支持。并且 Joda 与 JDK 是百分之百可互操作的,因此您无需替换所有 Java 代码,只需要替换执行日期/时间计算的那部分代码。
Joda 实际上是涵盖众多用于 Java 语言的替代 API 的大型项目,因此从技术上讲,使用 Joda 和 Joda-Time 名称表示相同的意思是一种误称。但在撰写本文之际,Joda-Time API 目前似乎是唯一处于活跃开发状态下的 Joda API。考虑到 Joda 大型项目的当前状态,我想将 Joda-Time 简称为 Joda 应该没什么问题。
本文将介绍并展示如何使用它。我将介绍以下主题:
您可以 下载 演示这些概念的样例应用程序的源代码。
为什么要使用 Joda?考虑创建一个用时间表示的某个随意的时刻 — 比如,2000 年 1 月 1 日 0 时 0 分。我如何创建一个用时间表示这个瞬间的 JDK 对象?使用 java.util.Date?事实上这是行不通的,因为自 JDK 1.1 之后的每个 Java 版本的 Javadoc 都声明应当使用 java.util.Calendar。Date 中不赞成使用的构造函数的数量严重限制了您创建此类对象的途径。
然而,Date 确实有一个构造函数,您可以用来创建用时间表示某个瞬间的对象(除 “现在” 以外)。该方法使用距离 1970 年 1 月 1 日子时格林威治标准时间(也称为 epoch)以来的毫秒数作为一个参数,对时区进行校正。考虑到 Y2K 对软件开发企业的重要性,您可能会认为我已经记住了这个值 — 但是我没有。Date 也不过如此。
那么 Calendar 又如何呢?我将使用下面的方式创建必需的实例:
Calendar calendar = Calendar.getInstance();calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
使用 Joda,代码应该类似如下所示:
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);
这一行简单代码没有太大的区别。但是现在我将使问题稍微复杂化。假设我希望在这个日期上加上 90 天并输出结果。使用 JDK,我需要使用清单 1 中的代码:
Calendar calendar = Calendar.getInstance();calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);SimpleDateFormat sdf = new SimpleDateFormat("E MM/dd/yyyy HH:mm:ss.SSS");calendar.add(Calendar.DAY_OF_MONTH, 90);System.out.println(sdf.format(calendar.getTime()));
使用 Joda,代码如清单 2 所示:
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);System.out.println(dateTime.plusDays(90).toString("E MM/dd/yyyy HH:mm:ss.SSS"); |
---|
两者之间的差距拉大了(Joda 用了两行代码,JDK 则是 5 行代码)。
现在假设我希望输出这样一个日期:距离 Y2K 45 天之后的某天在下一个月的当前周的最后一天的日期。坦白地说,我甚至不想使用 Calendar 处理这个问题。使用 JDK 实在太痛苦了,即使是简单的日期计算,比如上面这个计算。正是多年前的这样一个时刻,我第一次领略到 Joda-Time 的强大。使用 Joda,用于计算的代码如清单 3 所示:
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);System.out.println(dateTime.plusDays(45).plusMonths(1).dayOfWeek().withMaximumValue().toString("E MM/dd/yyyy HH:mm:ss.SSS");
清单 3 的输出为:
Sun 03/19/2000 00:00:00.000
如果您正在寻找一种易于使用的方式替代 JDK 日期处理,那么您真的应该考虑 Joda。如果不是这样的话,那么继续痛苦地使用 Calendar 完成所有日期计算吧。当您做到这一点后,您完全可以做到使用几把剪刀修建草坪并使用一把旧牙刷清洗您的汽车。
JDK Calendar 类缺乏可用性,这一点很快就能体会到,而 Joda 弥补了这一不足。Joda 的设计者还做出了一个决定,我认为这是它取得成功的构建:JDK 互操作性。Joda 的类能够生成(但是,正如您将看到的一样,有时会采用一种比较迂回的方式)java.util.Date 的实例(和 Calendar)。这使您能够保留现有的依赖 JDK 的代码,但是又能够使用 Joda 处理复杂的日期/时间计算。
例如,完成 清单3 中的计算后。我只需要做出如清单 4 所示的更改就可以返回到 JDK 中:
Calendar calendar = Calendar.getInstance();DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);System.out.println(dateTime.plusDays(45).plusMonths(1).dayOfWeek() .withMaximumValue().toString("E MM/dd/yyyy HH:mm:ss.SSS");calendar.setTime(dateTime.toDate());
就是这么简单。我完成了计算,但是可以继续在 JDK 对象中处理结果。这是 Joda 的一个非常棒的特性。
Joda 使用以下概念,它们可以应用到任何日期/时间库:
我将针对 Joda 依次讨论每一个概念。
我在本文讨论的 Joda 类具有不可变性,因此它们的实例无法被修改。(不可变类的一个优点就是它们是线程安全的)。我将向您展示的用于处理日期计算的 API 方法全部返回一个对应 Joda 类的新实例,同时保持原始实例不变。当您通过一个 API 方法操作 Joda 类时,您必须捕捉该方法的返回值,因为您正在处理的实例不能被修改。您可能对这种模式很熟悉;比如,这正是 java.lang.String 的各种操作方法的工作方式。
Instant 表示时间上的某个精确的时刻,使用从 epoch 开始计算的毫秒表示。这一定义与 JDK 相同,这就是为什么任何 Joda Instant 子类都可以与 JDK Date 和 Calendar 类兼容的原因。
更通用一点的定义是:一个瞬间 就是指时间线上只出现一次且唯一的一个时间点,并且这种日期结构只能以一种有意义的方式出现一次。
一个局部时间,正如我将在本文中将其称为局部时间片段一样,它指的是时间的一部分片段。瞬间性指定了与 epoch 相对的时间上的一个精确时刻,与此相反,局部时间片段指的是在时间上可以来回 “移动” 的一个时刻,这样它便可以应用于多个实例。比如,6 月 2 日 可以应用于任意一年的 6 月份(使用 Gregorian 日历)的第二天的任意瞬间。同样,11:06 p.m. 可以应用于任意一年的任意一天,并且每天只能使用一次。即使它们没有指定一个时间上的精确时刻,局部时间片段仍然是有用的。
我喜欢将局部时间片段看作一个重复周期中的一点,这样的话,如果我正在考虑的日期构建可以以一种有意义的方式出现多次(即重复的),那么它就是一个局部时间。
Joda 本质 — 以及其设计核心 — 的关键就是年表(它的含义由一个同名抽象类捕捉)。从根本上讲,年表是一种日历系统 — 一种计算时间的特殊方式 — 并且是一种在其中执行日历算法的框架。受 Joda 支持的年表的例子包括:
Joda-Time 1.6 支持 8 种年表,每一种都可以作为特定日历系统的计算引擎。
时区是值一个相对于英国格林威治的地理位置,用于计算时间。要了解事件发生的精确时间,还必须知道发生此事件的位置。任何严格的时间计算都必须涉及时区(或相对于 GMT),除非在同一个时区内发生了相对时间计算(即时这样时区也很重要,如果事件对于位于另一个时区的各方存在利益关系的话)。
DateTimeZone 是 Joda 库用于封装位置概念的类。许多日期和时间计算都可以在不涉及时区的情况下完成,但是仍然需要了解 DateTimeZone 如何影响 Joda 的操作。默认时间,即从运行代码的机器的系统时钟检索到的时间,在大部分情况下被使用。
现在,我将展示在采用该库时会经常遇到的一些 Joda 类,并展示如何创建这些类的实例。
我并不是可变实用类的粉丝;我只是认为它们的用例并不适合广泛使用。但是如果您认为您的确需要使用可变 Joda 类的话,本节的内容应当会对您的项目有帮助。Readable 和ReadWritable API 之间的唯一区别在于 ReadWritable 类能够改变封装的日期/时间值,因此我在这里将不再介绍这一点。
本节中介绍的所有实现都具有若干构造函数,允许您初始化封装的日期/时间。它们可以分为 4 个类别:
我将在第一个类中介绍这些构造函数: DateTime。当您使用其他 Joda 类的相应构造函数时,也可以使用这里介绍的内容。
如果您创建了一个 DateTime 的实例,并且没有提供 Chronology 或 DateTimeZone,Joda 将使用 ISOChronology(默认)和 DateTimeZone(来自系统设置)。然而,Joda ReadableInstant 子类的所有构造函数都包含一个超载方法,该方法以一个Chronology 或 DateTimeZone 为参数。本文附带的应用程序的的样例代码展示了如何使用这些超载方法。我在这里不会再详细介绍它们,因为这些方法使用起来非常简单。然而,我建议您试着使用一下这个样例应用程序,看看编写您的应用程序代码有多么简单,这样您就可以随意地在 Joda 的 Chronology 和 DateTimeZone 之间切换,同时不会影响到代码的其余部分。
Joda 通过 ReadableInstant 类实现了瞬间性这一概念。表示时间上的不可变瞬间的 Joda 类都属于这个类的子类。(将这个类命名为ReadOnlyInstant 可能更好,我认为这才是设计者需要传达的意思)。换句话说,ReadableInstant 表示时间上的某一个不可修改的瞬间)。其中的两个子类分别为 DateTime 和 DateMidnight:
您将在本文看到的其他类都遵循与 ReadableInstant 类相同的模式(Joda Javadoc 将显示这些内容),因此为了节省篇幅,我将不会在以下小节介绍这些内容。
应用程序所需处理的日期问题并不全部都与时间上的某个完整时刻有关,因此您可以处理一个局部时刻。例如,有时您比较关心年/月/日,或者一天中的时间,甚至是一周中的某天。Joda 设计者使用 ReadablePartial 接口捕捉这种表示局部时间的概念,这是一个不可变的局部时间片段。用于处理这种时间片段的两个有用类分别为 LocalDate 和 LocalTime:
了解特定的时刻或是某个局部时间片段将非常有用,但是如果能够表达一段时间跨度的话,通常也很有用。Joda 提供了三个类来简化这个过程。您可以选择用于表示不同跨度的类:时间跨度
现在,您已经了解了如何创建一些非常有用的 Joda 类,我将向您展示如何使用它们执行日期计算。接着您将了解到 Joda 如何轻松地与 JDK 进行互操作。
如果您只是需要对日期/时间信息使用占位符,那么 JDK 完全可以胜任,但是它在日期/时间计算方面的表现十分糟糕,而这正是 Joda 的长处。我将向您展示一些简单的例子。
假设在当前的系统日期下,我希望计算上一个月的最后一天。对于这个例子,我并不关心一天中的时间,因为我只需要获得年/月/日,如清单 6 所示:
LocalDate now = SystemFactory.getClock().getLocalDate();LocalDate lastDayOfPreviousMonth =\ now.minusMonths(1).dayOfMonth().withMaximumValue();
您可能对清单 6 中的 dayOfMonth() 调用感兴趣。这在 Joda 中被称为属性(property)。它相当于 Java 对象的属性。属性是根据所表示的常见结构命名的,并且它被用于访问这个结构,用于完成计算目的。属性是实现 Joda 计算威力的关键。您目前所见到的所有 4 个 Joda 类都具有这样的属性。一些例子包括:
我将详细介绍清单 6 中的示例,以向您展示整个计算过程。首先,我从当前月份减去一个月,得到 “上一个月”。接着,我要求获得 dayOfMonth 的最大值,它使我得到这个月的最后一天。注意,这些调用被连接到一起(注意 JodaReadableInstant 子类是不可变的),这样您只需要捕捉调用链中最后一个方法的结果,从而获得整个计算的结果。
当计算的中间结果对我不重要时,我经常会使用这种计算模式。(我以相同的方式使用 JDK 的 BigDecimal)。假设您希望获得任何一年中的第 11 月的第一个星期二的日期,而这天必须是在这个月的第一个星期一之后。清单 7 展示了如何完成这个计算:
LocalDate now = SystemFactory.getClock().getLocalDate();LocalDate electionDate = now.monthOfYear() .setCopy(11) // November .dayOfMonth() // Access Day Of Month Property .withMinimumValue() // Get its minimum value .plusDays(6) // Add 6 days .dayOfWeek() // Access Day Of Week Property .setCopy("Monday") // Set to Monday (it will round down) .plusDays(1); // Gives us Tuesday
清单 7 的注释帮助您了解代码如何获得结果。.setCopy("Monday") 是整个计算的关键。不管中间 LocalDate 值是多少,将其 dayOfWeek 属性设置为 Monday 总是能够四舍五入,这样的话,在每月的开始再加上 6 天就能够让您得到第一个星期一。再加上一天就得到第一个星期二。Joda 使得执行此类计算变得非常容易。
下面是其他一些因为使用 Joda 而变得超级简单的计算:
以下代码计算从现在开始经过两个星期之后的日期:
DateTime now = SystemFactory.getClock().getDateTime();DateTime then = now.plusWeeks(2);
您可以以这种方式计算从明天起 90 天以后的日期:
DateTime now = SystemFactory.getClock().getDateTime();DateTime tomorrow = now.plusDays(1);DateTime then = tomorrow.plusDays(90);
(是的,我也可以向 now 加 91 天,那又如何呢?)
下面是计算从现在起 156 秒之后的时间:
DateTime now = SystemFactory.getClock().getDateTime();DateTime then = now.plusSeconds(156);
下面的代码将计算五年后的第二个月的最后一天:1
DateTime now = SystemFactory.getClock().getDateTime();DateTime then = now.minusYears(5) // five years ago .monthOfYear() // get monthOfYear property .setCopy(2) // set it to February .dayOfMonth() // get dayOfMonth property .withMaximumValue();// the last day of the month
这样的例子实在太多了,我向您已经知道了如何计算。尝试操作一下样例应用程序,亲自体验一下使用 Joda 计算任何日期是多么有趣。
JDK 互操作性
我的许多代码都使用了 JDK Date 和 Calendar 类。但是幸亏有 Joda,我可以执行任何必要的日期算法,然后再转换回 JDK 类。这将两者的优点集中到一起。您在本文中看到的所有 Joda 类都可以从 JDK Calendar 或 Date 创建,正如您在 中看到的那样。出于同样的原因,可以从您所见过的任何 Joda 类创建 JDK Calendar 或 Date。
清单 8. 从 Joda DateTime 类创建 JDK 类
DateTime dateTime = SystemFactory.getClock().getDateTime();Calendar calendar = dateTime.toCalendar(Locale.getDefault());Date date = dateTime.toDate();DateMidnight dateMidnight = SystemFactory.getClock() .getDateMidnight();date = dateMidnight.toDate();
对于 ReadablePartial 子类,您还需要经过额外一步,如清单 9 所示:
清单 9. 创建表示 LocalDate 的 Date 对象
LocalDate localDate = SystemFactory.getClock().getLocalDate();Date date = localDate.toDateMidnight().toDate();
要创建 Date 对象,它表示从清单 9 所示的 SystemClock 中获得的 LocalDate,您必须首先将它转换为一个 DateMidnight 对象,然后只需要将 DateMidnight 对象作为 Date。(当然,产生的 Date 对象将把它自己的时间部分设置为午夜时刻)。
JDK 互操作性被内置到 Joda API 中,因此您无需全部替换自己的接口,如果它们被绑定到 JDK 的话。比如,您可以使用 Joda 完成复杂的部分,然后使用 JDK 处理接口。
使用 JDK 格式化日期以实现打印是完全可以的,但是我始终认为它应该更简单一些。这是 Joda 设计者进行了改进的另一个特性。要格式化一个 Joda 对象,调用它的 toString() 方法,并且如果您愿意的话,传递一个标准的 ISO-8601 或一个 JDK 兼容的控制字符串,以告诉 JDK 如何执行格式化。不需要创建单独的 SimpleDateFormat 对象(但是 Joda 的确为那些喜欢自找麻烦的人提供了一个 DateTimeFormatter 类)。调用 Joda 对象的 toString() 方法,仅此而已。我将展示一些例子。
清单 10. 使用 ISO-8601
DateTime dateTime = SystemFactory.getClock().getDateTime();dateTime.toString(ISODateTimeFormat.basicDateTime());dateTime.toString(ISODateTimeFormat.basicDateTimeNoMillis());dateTime.toString(ISODateTimeFormat.basicOrdinalDateTime());dateTime.toString(ISODateTimeFormat.basicWeekDateTime());
清单 10 中的四个 toString() 调用分别创建了以下内容:
20090906T080000.000-050020090906T080000-05002009249T080000.000-05002009W367T080000.000-0500
您也可以传递与 SimpleDateFormat JDK 兼容的格式字符串,如清单 11 所示:
DateTime dateTime = SystemFactory.getClock().getDateTime();dateTime.toString("MM/dd/yyyy hh:mm:ss.SSSa");dateTime.toString("dd-MM-yyyy HH:mm:ss");dateTime.toString("EEEE dd MMMM, yyyy HH:mm:ssa");dateTime.toString("MM/dd/yyyy HH:mm ZZZZ");dateTime.toString("MM/dd/yyyy HH:mm Z");
09/06/2009 02:30:00.000PM06-Sep-2009 14:30:00Sunday 06 September, 2009 14:30:00PM09/06/2009 14:30 America/Chicago09/06/2009 14:30 -0500
查看 Javadoc 中有关 joda.time.format.DateTimeFormat 的内容,获得与 JDK SimpleDateFormat 兼容的格式字符串的更多信息,并且可以将其传递给 Joda 对象的 toString() 方法。
结束语
谈到日期处理,Joda 是一种令人惊奇的高效工具。无论您是计算日期、打印日期,或是解析日期,Joda 都将是工具箱中的便捷工具。在本文中,我首先介绍了 Joda,它可以作为 JDK 日期/时间库的替代选择。然后介绍了一些 Joda 概念,以及如何使用 Joda 执行日期计算和格式化。