Joda-time中文文档
架构概述
下面将介绍Joda-time的主要组成部分,这里将介绍instant、interval、duration、period、chronology和timezones的概念,这些接口在库的设计中与普通的有所不同。最后我们将会介绍一下包的结构。Instant的用法示例将推迟到本指南的以下部分。有关interval、duration和period的示例可在文档的“关键概念”部分的相应部分中找到。
Instants(瞬间)
在Joda-Time中使用最频繁的概念是瞬间,瞬间被定义为datetime连续体中的时刻,该时刻被指定为从1970-01-01T00:00Z开始的毫秒数。这种毫秒的定义与date或Calender中的JDK一致。因此,两个API之间的互操作非常简单。
在joda-time中 ReadableInstant 接口代表了 instants的概念。该接口的主要实现是DateTime,这也是普通API用户需要最熟悉的类。DateTime是不可变的--一旦创建,值就不会改变。因此,此类可以安全地在多线程中传递和使用,而无需同步。
可以使用_Chronology(年表)_将毫秒瞬间转换为任何日期时间字段。为了帮助实现这一点,在DateTime上提供了一些方法,作为最常见的日期和时间字段的getter。
在这篇概述中,我们将进一步讨论_Chronology(年表)_的概念。
DateTime的一个配套可变类是MuableDateTime。此类的对象可以修改,并且不是线程安全的。
ReadableInstant的其他实现包括Instant和不推荐使用的DateMidnight。
Fields(时间字段)
DateTime的主API一直很简单,仅限于为每个日历字段获取方法。因此,例如,可以通过调用getDayOfYear()方法来检索“日期”日历字段。有关字段及其说明的完整列表,请参阅字段参考。
Properties(属性)
同多使用属性,可以获得更多的用法。每个日历字段都与这样的属性相关联。因此,‘day-of-Year’(其值由方法getDayOfYear()直接返回)也与DayOfYear()方法返回的属性相关联。与DateTime关联的属性类是DateTime.Property。
了解属性上的方法是充分利用API的秘诀。在本文档的后面部分,我们将更多地介绍属性的用法。
Intervals(间隔时间)
Joda-Time中的间隔表示从一个时刻到另一个时刻的时间间隔。这两个时刻都是日期时间连续体中完全指定的时刻,带有时区。
间隔被实现为半开区间,也就是说包括开始时刻的,但结束时刻是不包括的。结束总是大于或等于开始。两个端点仅限于具有相同的时间顺序和相同的时区。
提供了两个实现:Interval和MuableInterval,它们都是ReadableInterval的实现。
Durations(持续时间)
Joda-Time中的_duration_表示以毫秒为单位的持续时间。_duration_通常是从interval中获得的。
持续时间是一个非常简单的概念,实现起来也很简单。它们没有年表或时区,仅由毫秒持续时间组成。
可以将持续时间添加到瞬间,也可以添加到间隔的任何末尾以更改这些对象。在日期时间计算中,你可以认为:
instant + duration = instant
目前,ReadableDuration接口只有一种实现: Duration。
Periods(周期)
Joda-Time中的时间段表示根据字段定义的时间段,例如3年5个月2天7小时。这与持续时间的不同之处在于,它在毫秒方面是不精确的。一个周期只能通过指定它相对于的瞬间 (包括时间顺序和时区) 来解析为精确的毫秒数。
例如,假设一个周期为1个月。如果你把这段时间加到2月1日(ISO),你就会得到3月1日。如果你把同样的时间加到3月1日,你会得到4月1日。但在这两种情况下添加的持续时间(以毫秒为单位)非常不同。
作为第二个示例,考虑在夏令时边界处增加1天。如果您使用一段时间进行添加,则将根据需要添加23或25个小时。如果您创建的持续时间等于24小时,那么您将得到错误的结果。
周期被实现为一组int字段。周期中的标准字段集是年,月,周,日,小时,分钟,秒和毫秒。PeriodType类允许限制这组字段,例如消除周。当将持续时间或间隔转换为周期时,这一点很重要,因为计算需要知道应该填充哪些周期字段。
周期上存在获取每个字段值的方法。期间既不与年表关联,也不与时区关联。
可以将周期添加到瞬间,也可以添加到间隔的任何末尾以更改这些对象。在日期时间计算中,你可以说:
instant + period = instant
ReadablePeriod接口有两个实现:Period和MuablePeriod。
Chronology(年表)
Joda-Time的设计以年表为基础。它是一个计算引擎,支持日历系统的复杂规则。它封装了字段对象,这些对象用于按需将绝对时间瞬间划分为可识别的日历字段,如“星期几”。它实际上是一个可插拔的日历系统。
年表的实际计算在年表类本身和字段类-DateTimeField和DurationField之间进行了拆分。这三个类的子类共同构成了库中代码的大部分。大多数用户将永远不需要使用或直接引用子类。相反,他们将简单地获取年表并将其用作单例,如下所示:
Chronology coptic = CopticChronology.getInstance();
在内部,所有的年表,字段等类都保持为单例。因此,使用Joda-Time时存在初始设置成本,但此后只有主要的API实例类 (DateTime,Interval,period等) 具有创建和垃圾收集器成本。
尽管Chronology是设计的关键,但它不是使用API的关键!!
对于大多数应用程序,可以忽略年表,因为它将默认为ISOChronology。这适用于大多数用途。如果您需要1582年10月15日之前的准确日期,或者只要儒略历在您感兴趣的地区停止,您可以更改它。如果您需要一个特定的日历(如前面所示的科普特日历),也可以更改它。
TimeZones(时区)
年表类还支持时区功能。通过装饰器设计模式将其应用于基础年表。DateTimeZone类主要通过一种工厂方法提供对区域的访问,如下所示:
DateTimeZone zone = DateTimeZone.forID("Europe/London");
除了命名时区外,Joda-time还支持固定时区。其中最简单的是UTC,定义为一个常数:
DateTimeZone zoneUTC = DateTimeZone.UTC;
其他固定偏移时间可以通过专门的工厂方法获得:
DateTimeZone zoneUTC = DateTimeZone.forOffsetHours(hours);
时区实施基于GLOBAL-TZ提供的数据。可在此处找到时区ID的完整列表
Joda-Time提供了默认时区,当未指定时区时,该时区在许多操作中使用。这在概念上类似于java.util.TimeZone类的默认时区。可以通过静态方法访问和更新值:
DateTimeZone defaultZone = DateTimeZone.getDefault();
DateTimeZone.setDefault(myZone);
接口使用
正如您所看到的,Joda-Time定义了许多新的接口,这些接口在整个javadoc中都可见。最重要的是ReadableInstant,它目前有4个实现。其他重要的接口包括ReadableInterval和ReadablePeriod。它们目前分别用作仅限值类和可变类的泛化。
这里要提到的重要一点是,Joda接口的使用方式与JDK集合框架接口的使用方式不同。在使用集合接口(如List或Map)时,您通常会将变量作为List或Map的类型保存,仅在创建对象时引用具体类。
List list = new ArrayList();
Map map = new HashMap();
在Joda-Time中,接口的存在允许类似日期实现之间的互操作,例如类的可变和不可变版本。因此,它们只提供具体类的方法的子集。对于大多数工作,您将引用具体的类,而不是接口。这使您可以访问该库的全部功能。
DateTime dt = new DateTime();
但是,为了获得最大的灵活性,您可以选择使用Joda-Time接口声明方法参数。接口上的方法可以获取具体的类,以在该方法中使用。
public void process(ReadableDateTime dateTime) {
DateTime dt = dateTime.toDateTime();
}
包结构
包结构旨在将公共API中的方法与私有API中的方法分开。公共包是root包(在org.joda.time下)和format包。私有包包括base、chrono、Convert、field和tz包。大多数应用程序应该不需要从私有包中导入类。
DateTime的使用
新建
DateTime对象是使用DateTime构造函数创建的。默认构造函数的用法如下
DateTime dt = new DateTime();
创建一个DateTime对象表示由系统时钟确定的以毫秒为单位的当前日期和时间。它是使用默认时区的ISO日历构建的。
要创建代表特定日期和时间的datetime对象,您可以使用初始化字符串:
DateTime dt = new DateTime("2004-12-13T21:39:45.618-08:00");
初始化字符串的格式必须与ISO8601标准兼容。DateTime还提供了其他构造函数来使用各种标准字段创建特定的日期和时间。这也允许使用任何日历和时区。
与JDK的互相操作
DateTime类有一个构造函数,它将一个对象作为输入。特别是这个构造函数可以传递一个JDK date,JDK calander或JDK GregorianCalendar (它也接受一个ISO8601格式化的字符串,或代表毫秒的长对象)。这是与JDK的互操作性的一半。与JDK的互操作性的另一半是由返回JDK对象的DateTime方法提供的。
因此,Joda DateTime和JDK date之间的相互转换可以如下执行
// from Joda to JDK
DateTime dt = new DateTime();
Date jdkDate = dt.toDate();
// from JDK to Joda
dt = new DateTime(jdkDate);
JDK Calendar:
// from Joda to JDK
DateTime dt = new DateTime();
Calendar jdkCal = dt.toCalendar(Locale.CHINESE);
// from JDK to Joda
dt = new DateTime(jdkCal);
JDK GregorianCalendar:
// from Joda to JDK
DateTime dt = new DateTime();
GregorianCalendar jdkGCal = dt.toGregorianCalendar();
// from JDK to Joda
dt = new DateTime(jdkGCal);
查询DateTimes
将日历字段的计算(DateTimefield)与日历即时的表示(DateTime)分开,从而形成了一个强大而灵活的API。两者之间的连接由属性(DateTime.Property)维护,该属性提供对字段的访问。
例如,获取特定DateTime的星期几的直接方法涉及调用该方法
int iDoW = dt.getDayOfWeek();
其中Idow可以获取值(来自类DateTimeConstants)。
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
public static final int SUNDAY = 7;
访问字段
直接方法使用简单,但通过属性/字段机制可以实现更大的灵活性。星期几属性通过以下方式获取
DateTime.Property pDoW = dt.dayOfWeek();
可用于获取有关该字段的更丰富的信息,例如
String strST = pDoW.getAsShortText(); // returns "Mon", "Tue", etc.
String strT = pDoW.getAsText(); // returns "Monday", "Tuesday", etc.
它返回星期几的短名称字符串和长名称字符串(基于当前区域设置)。这些方法的本地化版本也可用,
String strTF = pDoW.getAsText(Locale.FRENCH); // returns "Lundi", etc.
可以用来返回法语中的day-of-Of-week name字符串。当然,该字段的原始整数值仍然可以访问为
iDoW = pDoW.get();
该属性还提供对与该字段关联的其他值的访问,例如关于最小和最大文本大小、跳跃状态、相关持续时间等的元数据。有关完整参考,请参阅基类AbstractReadableInstantFieldProperty的文档。
在实践中,人们实际上不会创建中间的pDoW变量。如果在匿名中间对象上调用这些方法,则代码更易于阅读。因此,例如,
strT = dt.dayOfWeek().getAsText();
iDoW = dt.dayOfWeek().get();
将被编写替换前面介绍的更间接的代码。
注意:对于获取字段数值的单一情况,我们建议在主DateTime对象上使用Get方法,因为这样效率更高。
iDoW = dt.getDayOfWeek();
日期字段
DateTime实现提供了标准日历字段的完整列表:
dt.getEra();
dt.getYear();
dt.getWeekyear();
dt.getCenturyOfEra();
dt.getYearOfEra();
dt.getYearOfCentury();
dt.getMonthOfYear();
dt.getWeekOfWeekyear();
dt.getDayOfYear();
dt.getDayOfMonth();
dt.getDayOfWeek();
它们中的每一个都有相应的属性方法,该方法将DateTime.Property绑定到适当的字段,如Year()或MonthOfYear()。这些属性所表示的字段的行为与它们的名称所暗示的非常相似。在现场参考中提供了准确的定义。
正如您所期望的那样,我们在上面的星期几示例中展示的所有方法都可以应用于这些属性中的任何一个。例如,要从日期时间提取标准的月、日和年字段,我们可以编写
String month = dt.monthOfYear().getAsText();
int maxDay = dt.dayOfMonth().getMaximumValue();
boolean leapYear = dt.yearOfEra().isLeap();
时间字段
另一组属性访问字段表示用于时间计算的日内持续时间。因此,为了计算由DateTime表示的瞬间的小时、分钟和秒,我们可以这样写:
int hour = dt.getHourOfDay();
int min = dt.getMinuteOfHour();
int sec = dt.getSecondOfMinute();
同样,它们中的每一个都有对应的属性方法,用于更复杂的操作。时间字段的完整列表可在字段参考中找到。
操纵DateTimes
DateTime对象具有值语义,并且在构造之后不能修改(它们是不可变的)。因此,对DateTime对象的最简单操作涉及将新的DateTime构造为原始DateTime的修改副本。
警告: 使用不可变类的常见错误是忘记将结果分配给变量。请记住,在immtable对象上调用add或set方法对该对象没有影响-仅更新结果。
修改字段
要做到这一点,一种方法是对属性使用方法。返回到前面的示例,如果我们希望通过将DT对象的星期几字段更改为星期一来修改它,我们可以使用该属性的setCopy方法来实现:
DateTime result = dt.dayOfWeek().setCopy(DateTimeConstants.MONDAY);
注意:如果DateTime对象已设置为星期一,则将返回相同的对象。要添加到日期,可以使用addToCopy方法。
DateTime result = dt.dayOfWeek().addToCopy(3);
DateTime方法
完成类似计算的另一种方法是在DateTime对象本身上使用方法。因此,我们可以直接将3天添加到dt,如下所示:
DateTime result = dt.plusDays(3);
使用MuableDateTime
上面概述的方法适用于涉及一个或两个字段的简单计算。在需要修改多个字段的情况下,创建datetime的可变副本,修改副本并最终创建新值datetime的效率更高。
MutableDateTime mdt = dt.toMutableDateTime();
// perform various calculations on mdt
...
DateTime result = mdt.toDateTime();
MuableDateTime有许多方法,包括标准的setter,用于直接修改日期时间。
切换时区
DateTime支持几个常见的时区计算。例如,如果您想在此时此刻获得伦敦的当地时间,则可以执行以下操作
// get current moment in default time zone
DateTime dt = new DateTime();
// translate to London local time
DateTime dtLondon = dt.withZone(DateTimeZone.forID("Europe/London"));
其中DateTimeZone.forID(“Europe/London”)返回伦敦的时区值。结果值dtLondon具有相同的绝对毫秒时间,但有一组不同的字段值。还支持反向操作,即获取与伦敦具有与当前默认时区相同的本地时间的时刻对应的日期时间(绝对毫秒)。这是按如下方式完成的
// get current moment in default time zone
DateTime dt = new DateTime();
// find the moment when London will have / had the same time
dtLondonSameTime = dt.withZoneRetainFields(DateTimeZone.forID("Europe/London"));
可以通过调用DateTimeZone.getAvailableIDs()获取一组所有时区ID字符串(“Europe/London”)。此处提供了可用时区的完整列表。
切换年表
DateTime类还有一个用于更改日历的方法。这使您可以及时更改给定时刻的日历。因此,如果您想要获取当前时间的日期时间,但在佛教日历中,您可以这样做
// get current moment in default time zone
DateTime dt = new DateTime();
dt.getYear(); // returns 2004
// change to Buddhist chronology
DateTime dtBuddhist = dt.withChronology(BuddhistChronology.getInstance());
dtBuddhist.getYear(); // returns 2547
其中,BudresChronology.getInstance是用于获取佛教年表的工厂方法。
输入输出
对于具有日期时间计算的应用程序来说,从具有其自己的定制格式的外部源读取日期时间信息是一种常见的要求。写入自定义格式也是一种常见要求。
许多自定义格式可以由日期格式字符串表示,这些字符串指定日历字段序列以及表示形式 (数字,名称字符串等) 和字段长度。例如,模式 “yyyy” 将代表4位数的年份。其他格式不太容易表示。例如,两位数年的模式 “yy” 并不能唯一地标识其所属的世纪。在输出上,这不会引起问题,但是在输入上存在解释的问题。
此外,目前有几个常用的日期/时间序列化标准,特别是ISO8601。大多数DateTime应用程序也必须支持这些功能。
Joda-Time通过灵活的架构支持这些不同的需求。我们现在将描述该体系结构的各种元素。
Formatters(格式化器)
使用DateTimeFormatter对象执行所有打印和解析。给定这样的对象fmt,解析如下
String strInputDateTime;
// string is populated with a date time string in some fashion
...
DateTime dt = fmt.parseDateTime(strInputDateTime);
因此,从格式化程序的解析方法返回一个DateTime对象。同样,输出按如下方式执行
String strOutputDateTime = fmt.print(dt);
Standard Formatters(标准格式化器)
ISODateTimeFormat类提供了对基于ISO8601的标准格式的支持。这提供了许多工厂方法。
例如,如果要使用日期时间的ISO标准格式yyyy-MM-dd‘T’HH:mm:ss.SSSZZ,则应将FMT初始化为
DateTimeFormatter fmt = ISODateTimeFormat.dateTime();
然后,您可以如上所述地使用FMT来读取或写入这种格式的DateTime对象。
Custom Formatters(自定义格式化器)
如果您需要一个可以用格式模式描述的自定义格式化程序,您可以使用DateTimeFormat类提供的工厂方法。因此,要获得4位年份、2位月份和2位月份日的格式化程序,即格式yyyyMMdd,您可以这样做
DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");
模式字符串与JDK日期模式兼容。
您可能需要在特定区域中打印或解析。这是通过在格式化程序上调用withLocale方法来实现的,该方法基于原始格式返回另一个格式化程序。
DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");
DateTimeFormatter frenchFmt = fmt.withLocale(Locale.FRENCH);
DateTimeFormatter germanFmt = fmt.withLocale(Locale.GERMAN);
Formatters是不可变的,所以原来的内容不会被with Locale方法更改。
怪异的格式化器
最后,如果您有一种不容易用模式字符串表示的格式,Joda-Time体系结构公开了一个构建器类,可用于构建以编程方式定义的定制格式化程序。因此,如果您希望格式化程序打印和解析格式为“22-Jan-65”的日期,您可以执行以下操作:
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.appendDayOfMonth(2)
.appendLiteral('-')
.appendMonthOfYearShortText()
.appendLiteral('-')
.appendTwoDigitYear(1956) // pivot = 1956
.toFormatter();
每个append方法都将一个要解析/打印的新字段附加到调用构建器,并返回一个新构建器。最后一个toForMatter方法创建将用于打印/解析的实际格式化程序。
这种格式特别有趣的是两位数的年份。由于对两位数年份的解释不明确,appendTwoDigitYear接受一个额外的参数,该参数通过指定范围的中点来定义两位数的100年范围。在本例中,范围是(1956-50)=1906,到(1956+49)=2005。因此,04将是2004,而07将是1907。这种转换对于普通的格式字符串是不可能的,这突出了Joda-Time格式化体系结构的强大功能。
直接访问
为了简化对格式化程序体系结构的访问,在DateTime类(如DateTime)上提供了方法。
DateTime dt = new DateTime();
String a = dt.toString();
String b = dt.toString("dd:MM:yy");
String c = dt.toString("EEE", Locale.FRENCH);
DateTimeFormatter fmt = ...;
String d = dt.toString(fmt);
四个结果中的每一个都演示了使用格式化程序的不同方式。结果a是日期时间的标准ISO8601字符串。结果b将使用模式‘dd:mm:yy’输出(请注意,模式在内部缓存)。结果c将使用法语中的模式‘eee’输出。结果d将使用指定的格式化程序输出,因此与fmt.print(Dt)相同。
高级特性
更改当前时间
Joda-Time允许您更改当前时间。所有获取当前时间的方法都是通过DateTimeUtils间接定向的。这允许更改当前时间,这对测试非常有用。
// always return the same time when querying current time
DateTimeUtils.setCurrentMillisFixed(millis);
// offset the real time
DateTimeUtils.setCurrentMillisOffset(millis);
请注意,以这种方式更改当前时间不会影响系统时钟。
转换器
API中每个主要具体类的构造函数都将一个对象作为参数。将其传递给转换器子系统,该子系统负责将对象转换为Joda-Time可接受的对象。例如,转换器可以将JDK日期对象转换为DateTime。如果需要,您可以将自己的转换器添加到Joda-Time中提供的转换器中。
安全
Joda-Time包含对敏感更改的标准JDK安全方案的挂钩。这些操作包括更改时区处理程序、更改当前时间和更改转换器。有关详细信息,请参见JodaTimePermission。