leftso 2693 0 2018-04-01 12:25:58

文章位置:左搜> 编程技术> 正文

引言

如今,很多应用程序仍然使用API java.util.Datejava.util.CalendarAPI,包括库来使我们的生活更轻松地处理这些类型,例如JodaTime。然而,Java 8引入了新的API来处理日期和时间,这使得我们可以对日期和时间表示进行更细致的控制,向我们提供不可改变的日期时间对象,更流畅的API,并且在大多数情况下提高性能,而无需使用额外的库。我们来看看基础知识。

LocalDate/LocalTime/LocalDateTime

 

    让我们从以下方面最相关的新API开始 java.util.DateLocalDate日期API,代表没有时间的日期; LocalTime,没有日期的时间表示; 并且LocalDateTime,这是前两者的组合。所有这些类型都表示某个地区的本地日期和/或时间,但就像java.util.Date它们包含有关它所代表的区域的信息,仅表示当前时区中的日期和时间。

首先,下面代码这些是API支持一个简单的实例:
 

LocalDate date = LocalDate.of(2018,2,13);
// Uses DateTimeformatter.ISO_LOCAL_DATE for which the format is: yyyy-MM-dd
LocalDate date = LocalDate.parse("2018-02-13");

LocalTime time = LocalTime.of(6,30);
// Uses DateTimeFormatter.ISO_LOCAL_TIME for which the format is: HH:mm[:ss[.SSSSSSSSS]]
// this means that both seconds and nanoseconds may optionally be present.
LocalTime time = LocalTime.parse("06:30");

LocalDateTime dateTime = LocalDateTime.of(2018,2,13,6,30);
// Uses DateTimeFormatter.ISO_LOCAL_DATE_TIME for which the format is the
// combination of the ISO date and time format, joined by 'T': yyyy-MM-dd'T'HH:mm[:ss[.SSSSSSSSS]]
LocalDateTime dateTime = LocalDateTime.parse("2018-02-13T06:30");
在它们之间转换很容易:
// LocalDate to LocalDateTime
LocalDateTime dateTime = LocalDate.parse("2018-02-13").atTime(LocalTime.parse("06:30"));

// LocalTime to LocalDateTime
LocalDateTime dateTime = LocalTime.parse("06:30").atDate(LocalDate.parse("2018-02-13"));

// LocalDateTime to LocalDate/LocalTime
LocalDate date = LocalDateTime.parse("2018-02-13T06:30").toLocalDate();
LocalTime time = LocalDateTime.parse("2018-02-13T06:30").toLocalTime();
除此之外,使用“加法”和“减法”方法以及一些实用功能,对我们的日期和时间表示进行操作是非常容易的:
LocalDate date = LocalDate.parse("2018-02-13").plusDays(5);
LocalDate date = LocalDate.parse("2018-02-13").plus(3, ChronoUnit.MONTHS);

LocalTime time = LocalTime.parse("06:30").minusMinutes(30);
LocalTime time = LocalTime.parse("06:30").minus(500, ChronoUnit.MILLIS);

LocalDateTime dateTime = LocalDateTime.parse("2018-02-13T06:30").plus(Duration.ofHours(2));

// using TemporalAdjusters, which implements a few useful cases:
LocalDate date = LocalDate.parse("2018-02-13").with(TemporalAdjusters.lastDayOfMonth());
现在我们将如何java.util.Date转向LocalDateTime并且是变体?那很简单:我们可以从日期类型转换为即时类型,这是自1970年1月1日以来的时间表示,然后我们可以LocalDateTime 使用即时和当前区域实例化一个。
LocalDateTime dateTime = LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());
要转换回日期,我们可以简单地使用Java 8时间类型表示的Instant。有一点要注意的,虽然是,虽然LocalDateLocalTime并且LocalDateTime不包含任何区域或偏移信息,它们代表了一个特定区域的本地日期和/或时间,因此它们持有该区域的偏移存在。因此,我们需要提供偏移量以将特定类型正确转换为即时。
 
// represents Wed Feb 28 23:24:43 CET 2018
Date now = new Date();

// represents 2018-02-28T23:24:43.106
LocalDateTime dateTime = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault());

// represent Wed Feb 28 23:24:43 CET 2018
Date date = Date.from(dateTime.toInstant(ZoneOffset.ofHours(1)));
Date date = Date.from(dateTime.toInstant(ZoneId.systemDefault().getRules().getOffset(dateTime)));
 

时间差异 - 持续时间和周期

正如你已经注意到的,在上面的例子中,我们使用了一个Duration对象。Duration并且Period是两个日期之间的时间表示,前者表示以秒和纳秒表示的时间差,后者表示日,月和年。

你应该什么时候使用这些?Period当你需要知道两个LocalDate表示之间的时间差异时:

Period period = Period.between(LocalDate.parse("2018-01-18"), LocalDate.parse("2018-02-14"));
Duration 当你在查找包含时间信息的表示之间的区别时:
Duration duration = Duration.between(LocalDateTime.parse("2018-01-18T06:30"), LocalDateTime.parse("2018-02-14T22:58"));
输出PeriodDuration使用时toString(),将根据ISO-8601标准使用特殊格式。一个期间使用的模式是PnYnMnD,其中n定义了期间内存在的年数,月数或天数。这意味着P1Y2M3D定义了1年,2个月和3天的时期。。模式中的'P'是周期指示符,它告诉我们下面的格式代表一个周期。使用模式,我们也可以使用该parse() 方法创建基于字符串的句点。
// represents a period of 27 days
Period period = Period.parse("P27D");

使用时Durations,我们稍微离开ISO-8601标准,因为Java 8不使用相同的模式。由ISO-8601定义的模式是PnYnMnDTnHnMn.nS。这基本上Period是以时间表示扩展的模式。在该模式中,T是时间指示符,因此接下来的部分定义了以小时,分钟和秒为单位指定的持续时间。

Java 8使用两种特定模式Duration,即在将字符串解析为a时为PnDTnHnMn.nS Duration,而在调用实例toString()上的方法时使用PTnHnMn.nS Duration

最后但并非最不重要的,我们也可以通过在类型上使用相应的方法来检索一段时间或持续时间的各个部分。但是,了解各种日期时间类型通过使用ChronoUnit枚举类型也支持这一点很重要。我们来看看一些例子:
 

// represents PT664H28M
Duration duration = Duration.between(LocalDateTime.parse("2018-01-18T06:30"), LocalDateTime.parse("2018-02-14T22:58"));

// returns 664
long hours = duration.toHours();

// returns 664
long hours = LocalDateTime.parse("2018-01-18T06:30").until(LocalDateTime.parse("2018-02-14T22:58"), ChronoUnit.HOURS);
 

使用区域和偏移量 - ZonedDateTime和OffsetDateTime

到目前为止,我们已经展示了新日期API如何使一些事情变得更容易一些。但是,真正有所作为的是在时区上下文中轻松使用日期和时间的功能。Java 8为我们提供了ZonedDateTimeOffsetDateTime,第一个是LocalDateTime针对特定区域(例如欧洲/巴黎)的信息,第二个是LocalDateTime具有偏移量的信息。有什么不同?OffsetDateTime使用UTC /格林威治和指定日期之间的固定时差,同时ZonedDateTime指定代表时间的区域,并考虑夏令时。

转换为这两种类型都非常简单:
 

OffsetDateTime offsetDateTime = LocalDateTime.parse("2018-02-14T06:30").atOffset(ZoneOffset.ofHours(2));
// Uses DateTimeFormatter.ISO_OFFSET_DATE_TIME for which the default format is
// ISO_LOCAL_DATE_TIME followed by the offset ("+HH:mm:ss").
OffsetDateTime offsetDateTime = OffsetDateTime.parse("2018-02-14T06:30+06:00");

ZonedDateTime zonedDateTime = LocalDateTime.parse("2018-02-14T06:30").atZone(ZoneId.of("Europe/Paris"));
// Uses DateTimeFormatter.ISO_ZONED_DATE_TIME for which the default format is
// ISO_OFFSET_DATE_TIME followed by the the ZoneId in square brackets.
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2018-02-14T06:30+08:00[Asia/Macau]");
// note that the offset does not matter in this case.
// The following example will also return an offset of +08:00
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2018-02-14T06:30+06:00[Asia/Macau]");

在切换它们时,必须记住,从a转换ZonedDateTimeOffsetDateTime将考虑夏令时,而从另一个方向转换OffsetDateTimeZonedDateTime,意味着您不会获得有关该区域的信息,也不会在那里是适用于夏令时的任何规则。这是因为偏移不定义任何时区规则,也不会绑定到特定区域。
ZonedDateTime winter = LocalDateTime.parse("2018-01-14T06:30").atZone(ZoneId.of("Europe/Paris"));
ZonedDateTime summer = LocalDateTime.parse("2018-08-14T06:30").atZone(ZoneId.of("Europe/Paris"));

// offset will be +01:00
OffsetDateTime offsetDateTime = winter.toOffsetDateTime();
// offset will be +02:00
OffsetDateTime offsetDateTime = summer.toOffsetDateTime();

OffsetDateTime offsetDateTime = zonedDateTime.toOffsetDateTime();

OffsetDateTime offsetDateTime = LocalDateTime.parse("2018-02-14T06:30").atOffset(ZoneOffset.ofHours(5));
ZonedDateTime zonedDateTime = offsetDateTime.toZonedDateTime();
 

如果我们不得不在这些类型之间手动转换以获得我们需要的类型,那将是一件麻烦事。这是Spring框架为我们提供帮助的地方。Spring为我们提供了很多开箱即用的日期时间转换器,这些日期转换器已ConversionRegistry在本org.springframework.format.datetime.standard.DateTimeConverters课程中注册并可在课堂上找到。

使用这些转换器时,知道它不会在区域或偏移之间转换时间非常重要。的ZonedDateTimeToLocalDateTimeConverter,例如,将返回LocalDateTime为它指定的区域,而不是LocalDateTime它会代表应用程序的区域。
 

ZonedDateTime zonedDateTime = LocalDateTime.parse("2018-01-14T06:30").atZone(ZoneId.of("Asia/Macau"));
// will represent 2018-01-14T06:30, regardless of the region your application has specified
LocalDateTime localDateTime = conversionService.convert(zonedDateTime, LocalDateTime.class);

最后但并非最不重要的是,您可以咨询ZoneId.getAvailableZoneIds()以查找所有可用时区,或使用ZoneId.SHORT_IDS包含几个时区(如EST,CST等)的缩略版本的地图。

格式化 - 使用 DateTimeFormatter

当然,世界各地使用不同的格式来指定时间。一个应用程序可能使用MM-dd-yyyy,而另一个应用程序使用dd / MM / yyyy。一些应用程序想要删除所有的混淆,并通过yyyy-MM-dd表示它们的日期。使用时java.util.Date,我们会很快转向使用多个格式化器。DateTimeFormatter然而,这个类为我们提供了可选的模式,因此我们可以使用单个格式化器来处理多种格式!我们来看看使用一些例子。
 

// Let’s say we want to convert all of patterns mentioned above.
// 09-23-2018, 23/09/2018 and 2018-09-23 should all convert to the same LocalDate.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd][dd/MM/yyyy][MM-dd-yyyy]");
LocalDate.parse("09-23-2018", formatter);
LocalDate.parse("23/09/2018", formatter);
LocalDate.parse("2018-09-23", formatter);
模式中的方括号定义模式中的可选部分。通过使我们的各种格式可选,与字符串匹配的第一个模式将用于转换我们的日期表示。当你使用多个模式时,这可能会很难阅读,所以我们来看看如何DateTimeFormatter使用builder模式创建。
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
	.appendOptional( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ) )
	.optionalStart().appendPattern( "dd/MM/yyyy" ).optionalEnd()
	.optionalStart().appendPattern( "MM-dd-yyyy" ).optionalEnd()
	.toFormatter();

这些是包含多种模式的基础知识,但如果我们的模式仅略有不同,该怎么办?我们来看看yyyy-MM-dd和yyyy-MMM-dd。
 
// 2018-09-23 and 2018-Sep-23 should convert to the same LocalDate.
// Using the ofPattern example we’ve used above will work:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy-MMM-dd]" );
LocalDate.parse( "2018-09-23", formatter );
LocalDate.parse( "2018-Sep-23", formatter );

// Using the ofPattern example where we reuse the common part of the pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyy-[MM-dd][MMM-dd]" );
LocalDate.parse( "2018-09-23", formatter );
LocalDate.parse( "2018-Sep-23", formatter );
但是,在转换为字符串时,不应使用支持多种格式的格式化程序,因为当我们使用格式化程序将日期格式化为字符串表示形式时,它也将使用可选模式。
LocalDate date = LocalDate.parse("2018-09-23");
// will result in 2018-09-232018-Sep-23
date.format(DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy-MMM-dd]" ));
// will result in 2018-09-23Sep-23
date.format(DateTimeFormatter.ofPattern( "yyyy-[MM-dd][MMM-dd]" ));

自从我们进入21世纪以来,显然我们必须考虑全球化,我们希望为用户提供本地化的日期。为确保您DateTimeFormatter返回特定的区域设置,您可以简单地执行以下操作:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "EEEE, MMM dd, yyyy" ).withLocale(Locale.UK);


DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("yyyy-MMM-dd" ).toFormatter(Locale.UK);

要查找哪些区域设置可用,您可以使用Locale.getAvailableLocales()

现在可能是您收到的日期格式比您使用的类型拥有更多的信息。DateTimeFormatter只要提供的日期表示不符合模式,A 就会抛出异常。让我们仔细看看这个问题以及如何解决这个问题。

// The issue: this will throw an exception.
LocalDate date = LocalDate.parse("2018-02-15T13:45");
// We provide a DateTimeFormatter that can parse the given date representation.
// The result will be a LocalDate holding 2018-02-15.
LocalDate date = LocalDate.parse("2018-02-15T13:45", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
我们来创建一个可以处理ISO日期,时间和日期时间模式的格式器。
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
	.appendOptional( DateTimeFormatter.ISO_LOCAL_DATE )
	.optionalStart().appendLiteral( "T" ).optionalEnd()
	.appendOptional( DateTimeFormatter.ISO_LOCAL_TIME )
	.toFormatter();

现在我们可以完美地执行以下所有操作:
// results in 2018-03-16
LocalDate date = LocalDate.parse( "2018-03-16T06:30", formatter );
LocalDate date = LocalDate.parse( "2018-03-16", formatter );
// results in 06:30
LocalTime time = LocalTime.parse( "2018-03-16T06:30", formatter );
LocalTime time = LocalTime.parse( "06:30", formatter );
LocalDateTime localDateTime = LocalDateTime.parse( "2018-03-16T06:30", formatter );
现在下一个问题在哪里?如果你试图解析一个日期模式LocalDateTime呢?如果你期望a LocalTime和你得到日期表示,反之亦然?
// will throw an exception
LocalDateTime localDateTime = LocalDateTime.parse("2018-03-16", formatter);
LocalDate localDate = LocalDate.parse("06:30", formatter);

对于最后两种情况,没有单一的正确解决方案,但这取决于您需要什么,或者这些日期和时间代表或可以代表什么。这种魔法可以在使用中找到TemporalQuery,您可以使用它来为模式的一部分创建默认值。

如果我们从一开始LocalDateTime,而你只想要LocalDate或者LocalTime,你会收到相应的部分LocalDateTime。要创建一个LocalDateTime,我们需要它所持有的日期和时间的默认值。假设如果您没有提供关于日期的信息,我们将返回今天的日期,如果您没有提供时间,我们会假设您意味着一天的开始。

由于我们正在返回a LocalDateTime,因此它不会被解析为LocalDateor LocalTime,所以让我们使用它ConversionService来获取正确的类型。

TemporalQuery<TemporalAccessor> myCustomQuery = new MyCustomTemporalQuery();
// results in 2018-03-16
LocalDateTime localDateTime = conversionService.convert( formatter.parse( "2018-03-16", myCustomQuery ), LocalDateTime.class );
// results in 00:00
LocalTime localTime = conversionService.convert( formatter.parse( "2018-03-16", myCustomQuery ), LocalTime.class );

class MyCustomTemporalQuery implements TemporalQuery<TemporalAccessor>
{
	@Override
	public TemporalAccessor queryFrom( TemporalAccessor temporal ) {
		LocalDate date = temporal.isSupported( ChronoField.EPOCH_DAY )
			? LocalDate.ofEpochDay( temporal.getLong( ChronoField.EPOCH_DAY ) ) : LocalDate.now();
		LocalTime time = temporal.isSupported( ChronoField.NANO_OF_DAY )
			? LocalTime.ofNanoOfDay( temporal.getLong( ChronoField.NANO_OF_DAY ) ) : LocalTime.MIN;
		return LocalDateTime.of( date, time );
	}
}

使用TemporalQuery允许我们检查哪些信息存在并为缺失的任何信息提供默认值,使我们能够使用在我们的应用程序中合理的逻辑轻松转换为所需的类型。

要了解如何DateTimeFormatter编写有效的时间模式,请查看文档。
 

总结

大多数新功能都需要一些时间来理解和习惯,Java 8 Date / Time API也不例外。新的API为我们提供了更好的访问所需的正确格式,以及使用日期时间操作的更加标准化和可读的方式。使用这些技巧和窍门,我们几乎可以涵盖我们所有的用例。

编程技术 java JAVA8 date