Java 8 新的日期和时间API

本篇内容

  • 为什么在java 8 中需要引入新的日期和时间库。
  • 同时为人和机器表示日期和时间。
  • 定义时间的度。
  • 操纵、格式化以及解析日期。
  • 处理不同的时区和历法。

1. 为什么在java 8 中需要引入新的日期和时间库

  • java.util.Date 只能以毫秒的精度表示时间。
  • java.util.Date 年份的起始选择是1900年,月份的起始从0开始。
  • Date类的toString方法返回的字符串 Tue Mar 18 00:00:00 CET 2014。
  • Calendar 月份依旧是从0开始计算。
  • DateFormat方法就只在Date类里有。
  • DateFormat 方法不是线程安全的。
  • Date和Calendar类都是可以变的。

2. LocalDate、LocalTime、Instant、Duration 以及 Period

2.1.1 使用 LocalDate 和 LocalTime

    开始使用新的日期和时间API时,你最先碰到的可能是LocalDate类。该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。
    你可以通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等,如下所示。

LocalDate date = LocalDate.of(2020, 12, 1);
		int year = date.getYear();//2020
		Month month = date.getMonth(); //December
		int dayOfMonth = date.getDayOfMonth();  //一个月的第几天 1
		DayOfWeek dayOfWeek = date.getDayOfWeek();//一周的第几天   TUESDAY 这个格式
		int len = date.lengthOfMonth();//这个月有多少天
		boolean leapYear = date.isLeapYear(); //是否是闰年

    你还可以使用工厂方法从系统时钟中获取当前的日期:

LocalDate today = LocalDate.now();
2.1.2 使用TemporalField读取LocalDate的值
		int year1 = date.get(ChronoField.YEAR);
		int month1 = date.get(ChronoField.DAY_OF_MONTH);
		int day1 = date.get(ChronoField.DAY_OF_WEEK);
2.1.3 创建LocalTime并读取其值
		LocalTime time = LocalTime.of(12, 45, 20);
		int hour = time.getHour();//12
		int minute = time.getMinute();//45
		int second = time.getSecond();//20
2.1.4 parse

    LocalDate和LocalTime都可以通过解析代表它们的字符串创建。使用静态方法parse,你可以实现这一目的:

		LocalDate date = LocalDate.parse("2020-03-18");
		LocalTime time1 = LocalTime.parse("12:45:20");
2.2 合并日期和时间
2.2.1 直接创建LocalDateTime对象,或者通过合并日期和时间的方式创建
		LocalDateTime localDateTime = LocalDateTime.of(2020, Month.MARCH, 18, 12, 45, 20);
		LocalDate date = LocalDate.of(2020, 12, 1);
		LocalTime time = LocalTime.of(12, 45, 20);

		LocalDateTime localDateTime1 = LocalDateTime.of(date, time);
		LocalDateTime localDateTime2 = date.atTime(12, 45, 20);
		LocalDateTime localDateTime3 = date.atTime(time);
		LocalDateTime localDateTime4 = time.atDate(date);
		//你也可以使用toLocalDate或者toLocalTime方法,从LocalDateTime中提取LocalDate或者LocalTime
		LocalDate localDate = localDateTime.toLocalDate();
		LocalTime localTime = localDateTime.toLocalTime();
2.3 机器的日期和时间格式

    Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。

		Instant.ofEpochSecond(3);
		Instant.ofEpochSecond(3,0);
		//2秒之后再加上100万纳秒
		Instant.ofEpochSecond(2,1_000_000_000);
		//4秒之前的100万纳秒
		Instant.ofEpochSecond(4,-1_000_000_000);
2.4 定义 Duration 或 Period

    Duration类主要用于以秒和纳秒衡量时间的长短,你不能仅向between方法传递一个LocalDate对象做参数。

		LocalTime time = LocalTime.of(15, 20, 0);
		LocalTime time1 = LocalTime.of(14, 30, 0);
		Duration d1 = Duration.between(time1, time);

		Instant instant1 = Instant.ofEpochSecond(3);
		Instant instant2 = Instant.ofEpochSecond(5);
		Duration d3 = Duration.between(instant1, instant2);

    如果你需要以年、月或者日的方式对多个时间单位建模,可以使用Period类。使用该类的工厂方法between,你可以使用得到两个LocalDate之间的时长,如下所示:

Period between = Period.between(LocalDate.of(2020, 3, 8), LocalDate.of(2020, 5, 8));
2.4.1 创建Duration和Period对象
		Duration threeMinutes = Duration.ofMinutes(3);
		Duration threeMinutes1 = Duration.of(3, ChronoUnit.MINUTES);

		Period tenDays = Period.ofDays(10);
		Period threeWeeks = Period.ofWeeks(3);
		Period twoYearsSixMonthOneDay = Period.of(2, 6, 1);

    Duration类和Period类共享了很多相似的方法,参见下表所示。

方法名 是否是静态方法 方法描述
between 创建两个时间点之间的 interval
from 由一个临时时间点创建 interval
of 由它的组成部分创建 interval 的实例
parse 由字符串创建 interval 的实例
addTo 创建该 interval 的副本,并将其叠加到某个指定的 temporal 对象
get 读取该 interval 的状态
isNegative 检查该 interval 是否为负值,不包含零
isZero 检查该 interval 的时长是否为零
minus 通过减去一定的时间创建该 interval 的副本
multipliedBy 将 interval 的值乘以某个标量创建该 interval 的副本
negated 以忽略某个时长的方式创建该 interval 的副本
plus 以增加某个指定的时长的方式创建该 interval 的副本
subtractFrom 从指定的 temporal 对象中减去该 interval

    截至目前,我们介绍的这些日期、时间对象都是不可修改的,这是为了更好地支持函数式编程,确保线程安全,保持领域模式一致性而做出的重大设计决定。当然,新的日期和时间API也提供了一些便利的方法来创建这些对象的可变版本。比如,你可能希望在已有的LocalDate实例上增加3天。我们在下一节中会针对这一主题进行介绍。除此之外,我们还会介绍如何依据指定的模式,比如dd/MM/yyyy,创建日期、时间格式器,以及如何使用这种格式器解析和输出日期。

3 操纵、解析和格式化日期

    如果你已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!
    以比较直观的方式操纵LocalDate的属性

		LocalDate localDate = LocalDate.of(2020, 3, 18);
		LocalDate localDate1 = localDate.withYear(2019);
		LocalDate localDate2 = localDate1.withDayOfMonth(25);
		LocalDate localDate3 = localDate1.with(ChronoField.MONTH_OF_YEAR, 9);
		

    以相对方式修改LocalDate对象的属性

		LocalDate date1 = LocalDate.of(2020, 3, 18);
		LocalDate date2 = date1.plusWeeks(1);
		LocalDate date3 = date2.minusWeeks(3);
		LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);

    大概你已经猜到,像LocalDate、LocalTime、LocalDateTime以及Instant这样表示时间点的日期、时间类提供了大量通用的方法,下表对这些通用的方法进行了总结。

方法名 是否是静态方法 方法描述
from 依据传入的 Temporal 对象创建对象实例
now 依据系统时钟创建 Temporal 对象
of Temporal 对象的某个部分创建该对象的实例
parse 由字符串创建 Temporal 对象的实例
atOffset 将 Temporal 对象和某个时区偏移相结合
atZone 将 Temporal 对象和某个时区相结合
format 使用某个指定的格式器将Temporal对象转换为字符串(Instant类不提供该方法)
get 读取 Temporal 对象的某一部分的值
minus 创建 Temporal 对象的一个副本,通过将当前 Temporal 对象的值减去一定的时长创建该副本
plus 创建 Temporal 对象的一个副本,通过将当前 Temporal 对象的值加上一定的时长创建该副本
with 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本

3.1 使用 TemporalAdjuster

    截至目前,你所看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。对于最常见的用例,日期和时间API已经提供了大量预定义的TemporalAdjuster。你可以通过TemporalAdjuster类的静态工厂方法访问它们,如下所示。

    使用预定义的TemporalAdjuster

		LocalDate localDate = LocalDate.of(2020, 3, 18);
		LocalDate localDate1 = localDate.with(nextOrSame(DayOfWeek.SUNDAY));//下一个周六
		LocalDate localDate2 = localDate1.with(lastDayOfMonth());//本月最后一天

     下表提供了TemporalAdjuster中包含的工厂方法列表。

方法名 描 述
dayOfWeekInMonth 创建一个新的日期,它的值为同一个月中每一周的第几天
firstDayOfMonth 创建一个新的日期,它的值为当月的第一天
firstDayOfNextMonth 创建一个新的日期,它的值为下月的第一天
firstDayOfNextYear 创建一个新的日期,它的值为明年的第一天
firstDayOfYear 创建一个新的日期,它的值为当年的第一天
firstInMonth 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值
lastDayOfMonth 创建一个新的日期,它的值为当月的最后一天
lastDayOfNextMonth 创建一个新的日期,它的值为下月的最后一天
lastDayOfNextYear 创建一个新的日期,它的值为明年的最后一天
lastDayOfYear 创建一个新的日期,它的值为今年的最后一天
lastInMonth 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值
next/previous 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期
nextOrSame/previousOrSame 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象

     TemporalAdjuster接口

@FunctionalInterface 
public interface TemporalAdjuster { 
 Temporal adjustInto(Temporal temporal); 
}

     这意味着TemporalAdjuster接口的实现需要定义如何将一个Temporal对象转换为另一个Temporal对象。你可以把它看成一个UnaryOperator

     实现一个定制的TemporalAdjuster
     请设计一个NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天的日期,同时过滤掉周六和周日这些节假日。格式如下所示:

    date = date.with(new NextWorkingDay());
    //如果当天的星期介于周一至周五之间,日期向后移动一天;如果当天是周六或者周日,则返回下一个周一。
public class NextWorkingDay implements TemporalAdjuster {
	@Override
	public Temporal adjustInto(Temporal temporal) {

		DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
		int addDay = 1;
		if (dayOfWeek == DayOfWeek.FRIDAY) {
			addDay = 3;
		} else if (dayOfWeek == DayOfWeek.SATURDAY) {
			addDay = 2;
		}

		return temporal.plus(addDay, ChronoUnit.DAYS);
	}
}

3.2 打印输出及解析日期-时间对象

     处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的java.time.format包就是特别为这个目的而设计的。这个包中,最重要的类是DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像BASIC_ISO_DATE和 ISO_LOCAL_DATE 这样的常量是 DateTimeFormatter 类的预定义实例。所有的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。比如,下面的这个例子中,我们使用了两个不同的格式器生成了字符串:

LocalDate date = LocalDate.of(2020, 3, 18);
		String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);//20200318
		String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);//2020-03-18
		
		LocalDate date1 = LocalDate.parse("20200318", DateTimeFormatter.BASIC_ISO_DATE);
		LocalDate date2 = LocalDate.parse("2020-03-18", DateTimeFormatter.ISO_LOCAL_DATE);

     和老的java.util.DateFormat相比较,所有的DateTimeFormatter实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。

     按照某个模式创建DateTimeFormatter

		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
		LocalDate localDate = LocalDate.of(2020, 03, 18);
		String format = localDate.format(formatter);
		LocalDate date2 = LocalDate.parse(format, formatter);

     创建一个本地化的DateTimeFormatter

        DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); 
        LocalDate date1 = LocalDate.of(2020, 3, 18); 
        String formattedDate = date.format(italianFormatter); // 18. marzo 2020 
        LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

    最后,如果你还需要更加细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精 确 地 匹 配 指 定 的 模 式 )、 填 充 , 以 及 在 格 式 器 中 指 定 可 选 节 。 比 如 , 你 可 以 通 过DateTimeFormatterBuilder自己编程实现(d. MMMM yyyy)上面代码使用的italianFormatter,代码清单如下。

		DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
				.appendText(ChronoField.DAY_OF_MONTH)
				.appendLiteral(". ")
				.appendText(ChronoField.MONTH_OF_YEAR)
				.appendLiteral(" ")
				.appendText(ChronoField.YEAR)
				.parseCaseInsensitive()
				.toFormatter(Locale.ITALIAN);

3.2 处理不同的时区和历法

    之前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,比如处理日光时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一样,ZoneId类也是无法修改的。

    你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:

     ZoneId zoneId = TimeZone.getDefault().toZoneId();

    一旦得到一个ZoneId对象,你就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点,代码清单如下所示。

//为时间点添加时区信息
		ZoneId romeZone = ZoneId.of("Europe/Rome");
		
		LocalDate date = LocalDate.of(2020, Month.MARCH, 18);
		ZonedDateTime zdt1 = date.atStartOfDay(romeZone);

		LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 18, 13, 45);
		ZonedDateTime zdt2 = dateTime.atZone(romeZone);

		Instant now = Instant.now();
		ZonedDateTime zdt3 = now.atZone(romeZone);

    通过ZoneId,你还可以将LocalDateTime转换为Instant:

		LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 18, 13, 45); 
		Instant instantFromDateTime = dateTime.toInstant(romeZone);

    你也可以通过反向的方式得到LocalDateTime对象:

		Instant instant = Instant.now(); 
		LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

4 利用和 UTC/格林尼治时间的固定偏差计算时区

    另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。比如,基于这个理论,你可以说“纽约落后于伦敦5小时”。这种情况下,你可以使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:

ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

    “-05:00”的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的ZoneOffset并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。由于ZoneOffset也是ZoneId,所以你可以像代码清单12-13那样使用它。你甚至还可以创建这样的OffsetDateTime,它使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。

		ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
		LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 18, 13, 45);
		OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);
4.1 使用别的日历系统

    ISO-8601日历系统是世界文明日历系统的事实标准。但是,Java 8中另外还提供了4种其他的日历系统。这些日历系统中的每一个都有一个对应的日志类,分别是ThaiBuddhistDate、MinguoDate 、 JapaneseDate 以 及 HijrahDate 。所有这些类以及 LocalDate 都实现了ChronoLocalDate接口,能够对公历的日期进行建模。利用LocalDate对象,你可以创建这些类的实例。更通用地说,使用它们提供的静态工厂方法,你可以创建任何一个Temporal对象的实例,如下所示:

		LocalDate localDate = LocalDate.of(2020, Month.MARCH, 18);
		JapaneseDate japaneseDate = JapaneseDate.from(localDate);

    或者,你还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例。新的日期和时间API中,Chronology接口建模了一个日历系统,使用它的静态工厂方法ofLocale,可以得到它的一个实例,代码如下:

		LocalDate localDate = LocalDate.of(2020, Month.MARCH, 18);
		JapaneseDate japaneseDate = JapaneseDate.from(localDate);

    或者,你还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例。新的日期和时间API中,Chronology接口建模了一个日历系统,使用它的静态工厂方法ofLocale,可以得到它的一个实例,代码如下:

		Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
		ChronoLocalDate now = japaneseChronology.dateNow();

    伊斯兰教日历

		HijrahDate ramadanDate =
				HijrahDate.now().with(ChronoField.DAY_OF_MONTH, 1)
						.with(ChronoField.MONTH_OF_YEAR, 9);
		System.out.println("Ramadan starts on " +
				IsoChronology.INSTANCE.date(ramadanDate) +
				" and ends on " +
				IsoChronology.INSTANCE.date(
						ramadanDate.with(
								TemporalAdjusters.lastDayOfMonth())));

小结

    本节中需要掌握:

  • Java 8之前老版的java.util.Date类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
  • 新版的日期和时间API中,日期时间对象是不可变的。
  • 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。
  • 你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。
  • TemporalAdjuster让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。
  • 你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期-时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
  • 你可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期时间对象上,对其进行本地化。
  • 你现在可以使用不同于ISO-8601标准系统的其他日历系统了。

你可能感兴趣的:(java8,java)