Java的API提供了不少有用的组件,能帮助你构建复杂的应用。不过,Java API也不老是完美的。咱们相信大多数有经验的程序员都会赞同Java 8以前的库对日期和时间的支持就很是不理想。然而,你也不用太担忧:Java 8中引入全新的日期和时间API就是要解决这一问题。java
在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类。正如类名所表达的,这个类没法表示日期,只能以毫秒的精度表示时间。更糟糕的是它的易用性,因为某些缘由未知的设计决策,这个类的易用性被深深地损害了,好比:年份的起始选择是1900年,月份的起始从0开始。这意味着,若是你想要用Date表示Java 8的发布日期,即2014年3月18日,须要建立下面这样的Date实例:git
Date date = new Date(114, 2, 18);
它的打印输出效果为:程序员
Tue Mar 18 00:00:00 CST 2014
看起来不那么直观,不是吗?此外,甚至Date类的toString方法返回的字符串也容易误导人。github
随着Java 1.0退出历史舞台,Date类的种种问题和限制几乎一扫而光,但很明显,这些历史旧帐若是不牺牲前向兼容性是没法解决的。因此,在Java 1.1中,Date类中的不少方法被废弃了,取而代之的是java.util.Calendar类。很不幸,Calendar类也有相似的问题和设计缺陷,致使使用这些方法写出的代码很是容易出错。好比,月份依旧是从0开始计算(不过,至少Calendar类拿掉了由1900年开始计算年份这一设计)。更糟的是,同时存在Date和Calendar这两个类,也增长了程序员的困惑。到底该使用哪个类呢?此外,有的特性只在某一个类有提供,好比用于以语言无关方式格式化和解析日期或时间的DateFormat方法就只在Date类里有。数据库
DateFormat方法也有它本身的问题。好比,它不是线程安全的。这意味着两个线程若是尝试使用同一个formatter解析日期,你可能会获得没法预期的结果。编程
最后,Date和Calendar类都是能够变的。能把2014年3月18日修改为4月18日意味着什么呢?这种设计会将你拖入维护的噩梦,接下来的一章,咱们会讨论函数式编程,你在该章中会了解到更多的细节。安全
这一章中,咱们会一块儿探索新的日期和时间API所提供的新特性。咱们从最基本的用例入手,好比建立同时适合人与机器的日期和时间,逐渐转入到日期和时间API更高级的一些应用,好比操纵、解析、打印输出日期时间对象,使用不一样的时区和年历。app
让咱们从探索如何建立简单的日期和时间间隔入手。java.time包中提供了不少新的类能够帮你解决问题,它们是LocalDate、LocalTime、Instant、Duration和Period。函数式编程
开始使用新的日期和时间API时,你最早碰到的多是LocalDate类。该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。函数
你能够经过静态工厂方法of建立一个LocalDate实例。LocalDate实例提供了多种方法来读取经常使用的值,好比年份、月份、星期几等,以下所示。
LocalDate localDate = LocalDate.of(2014, 3, 18); int year = localDate.getYear(); Month month = localDate.getMonth(); int day = localDate.getDayOfMonth(); DayOfWeek dow = localDate.getDayOfWeek(); int len = localDate.lengthOfMonth(); boolean leap = localDate.isLeapYear(); System.out.println(String.format("year:%s\nmonth:%s\nday:%s\ndow:%s\nlen:%s\nleap:%s", year, month, day, dow, len, leap));
打印结果:
year:2014 month:MARCH day:18 dow:TUESDAY len:31 leap:false
你还可使用工厂方法从系统时钟中获取当前的日期:
LocalDate today = LocalDate.now();
接下来剩余的部分会探讨全部日期-时间类,这些类都提供了相似的工厂方法。你还能够经过传递一个TemporalField参数给get方法拿到一样的信息。TemporalField是一个接口,它定义了如何访问temporal对象某个字段的值。ChronoField枚举实现了这一接口,因此你能够很方便地使用get方法获得枚举元素的值,以下所示。
int year = localDate.get(ChronoField.YEAR); int month = localDate.get(ChronoField.MONTH_OF_YEAR); int day = localDate.get(ChronoField.DAY_OF_MONTH);
相似地,一天中的时间,好比13:45:20,可使用LocalTime类表示。你可使用of重载的两个工厂方法建立LocalTime的实例。第一个重载函数接收小时和分钟,第二个重载函数同时还接收秒。同LocalDate同样,LocalTime类也提供了一些getter方法访问这些变量的值,以下所示。
LocalTime localTime = LocalTime.of(13, 45, 20); int hour = localTime.getHour(); int minute = localTime.getMinute(); int second = localTime.getSecond(); System.out.println(String.format("hour:%s\nminute:%s\nsecond:%s", hour, minute, second));
打印结果:
hour:13 minute:45 second:20
LocalDate和LocalTime均可以经过解析表明它们的字符串建立。使用静态方法parse,你能够实现这一目的:
LocalDate date = LocalDate.parse("2018-11-17"); LocalTime time = LocalTime.parse("21:27:58");
你能够向parse方法传递一个DateTimeFormatter。该类的实例定义了如何格式化一个日期或者时间对象。正如咱们以前所介绍的,它是替换老版java.util.DateFormat的推荐替代品。这个咱们后面将会讨论到。同时,也请注意,一旦传递的字符串参数没法被解析为合法的LocalDate或LocalTime对象,这两个parse方法都会抛出一个继承自RuntimeException的DateTimeParseException异常。
这个复合类名叫LocalDateTime,是LocalDate和LocalTime的合体。它同时表示了日期和时间,但不带有时区信息,你能够直接建立,也能够经过合并日期和时间对象构造,以下所示。
// 2018-11-17T21:31:50 LocalTime time = LocalTime.of(21, 31, 50); LocalDate date = LocalDate.of(2018, 11, 17); LocalDateTime dt1 = LocalDateTime.of(2018, Month.NOVEMBER, 17, 21, 31, 50); LocalDateTime dt2 = LocalDateTime.of(date, time); LocalDateTime dt3 = date.atTime(21, 11, 17); LocalDateTime dt4 = date.atTime(time); LocalDateTime dt5 = time.atDate(date);
注意,经过它们各自的atTime或者atDate方法,向LocalDate传递一个时间对象,或者向LocalTime传递一个日期对象的方式,你能够建立一个LocalDateTime对象。你也可使用toLocalDate或者toLocalTime方法,从LocalDateTime中提取LocalDate或者LocalTime组件:
LocalDate date1 = dt1.toLocalDate(); LocalTime time1 = dt1.toLocalTime();
做为人,咱们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问,这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最天然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的java.time.Instant类对时间建模的方式,基本上它是以Unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行计算。
你能够经过向静态工厂方法ofEpochSecond传递一个表明秒数的值建立一个该类的实例。静态工厂方法ofEpochSecond还有一个加强的重载版本,它接收第二个以纳秒为单位的参数值,对传入做为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在0到999 999999之间。这意味着下面这些对ofEpochSecond工厂方法的调用会返回几乎一样的Instant对象:
Instant.ofEpochSecond(3); Instant.ofEpochSecond(3, 0); // 2 秒以后再加上100万纳秒(1秒) Instant.ofEpochSecond(2, 1_000_000_000); // 4秒以前的100万纳秒(1秒) Instant.ofEpochSecond(4, -1_000_000_000);
正如你已经在LocalDate及其余为便于阅读而设计的日期-时间类中所看到的那样,Instant类也支持静态工厂方法now,它可以帮你获取当前时刻的时间戳。咱们想要特别强调一点,Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。因此,它没法处理那些咱们很是容易理解的时间单位。好比下面这段语句:
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
它会抛出下面这样的异常:
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
可是你能够经过Duration和Period类使用Instant,接下来咱们会对这部份内容进行介绍。
目前为止,你看到的全部类都实现了Temporal接口,Temporal接口定义了如何读取和操纵为时间建模的对象的值。以前的介绍中,咱们已经了解了建立Temporal实例的几种方法。很天然地你会想到,咱们须要建立两个Temporal对象之间的duration。Duration类的静态工厂方法between就是为这个目的而设计的。你能够建立两个LocalTimes对象、两个LocalDateTimes对象,或者两个Instant对象之间的duration,以下所示:
LocalTime time1 = LocalTime.of(21, 50, 10); LocalTime time2 = LocalTime.of(22, 50, 10); LocalDateTime dateTime1 = LocalDateTime.of(2018, 11, 17, 21, 50, 10); LocalDateTime dateTime2 = LocalDateTime.of(2018, 11, 17, 23, 50, 10); Instant instant1 = Instant.ofEpochSecond(1000 * 60 * 2); Instant instant2 = Instant.ofEpochSecond(1000 * 60 * 3); Duration d1 = Duration.between(time1, time2); Duration d2 = Duration.between(dateTime1, dateTime2); Duration d3 = Duration.between(instant1, instant2); // PT1H 相差1小时 System.out.println("d1:" + d1); // PT2H 相差2小时 System.out.println("d2:" + d2); // PT16H40M 相差16小时40分钟 System.out.println("d3:" + d3);
因为LocalDateTime和Instant是为不一样的目的而设计的,一个是为了便于人阅读使用,另外一个是为了便于机器处理,因此你不能将两者混用。若是你试图在这两类对象之间建立duration,会触发一个DateTimeException异常。此外,因为Duration类主要用于以秒和纳秒衡量时间的长短,你不能仅向between方法传递一个LocalDate对象作参数。
若是你须要以年、月或者日的方式对多个时间单位建模,可使用Period类。使用该类的工厂方法between,你可使用获得两个LocalDate之间的时长,以下所示:
Period period = Period.between(LocalDate.of(2018, 11, 7), LocalDate.of(2018, 11, 17)); // P10D 相差10天 System.out.println("Period between:" + period);
最后,Duration和Period类都提供了不少很是方便的工厂类,直接建立对应的实例;换句话说,就像下面这段代码那样,再也不是只能以两个temporal对象的差值的方式来定义它们的对象。
Duration threeMinutes = Duration.ofMinutes(3); Duration fourMinutes = Duration.of(4, ChronoUnit.MINUTES); Period tenDay = Period.ofDays(10); Period threeWeeks = Period.ofWeeks(3); Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
Duration类和Period类共享了不少类似的方法,有兴趣的能够参考官网的文档。
截至目前,咱们介绍的这些日期时间对象都是不可修改的,这是为了更好地支持函数式编程,确保线程安全,保持领域模式一致性而作出的重大设计决定。固然,新的日期和时间API也提供了一些便利的方法来建立这些对象的可变版本。好比,你可能但愿在已有的LocalDate实例上增长3天。除此以外,咱们还会介绍如何依据指定的模式,好比dd/MM/yyyy,建立日期-时间格式器,以及如何使用这种格式器解析和输出日期。
若是你已经有一个LocalDate对象,想要建立它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会建立对象的一个副本,并按照须要修改它的属性。注意,下面的这段代码中全部的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!
// 2018-11-17 LocalDate date1 = LocalDate.of(2018, 11, 17); // 2019-11-17 LocalDate date2 = date1.withYear(2019); // 2019-11-25 LocalDate date3 = date2.withDayOfMonth(25); // 2019-09-25 LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
它们都声明于Temporal接口,全部的日期和时间API类都实现这两个方法,它们定义了单点的时间,好比LocalDate、LocalTime、LocalDateTime以及Instant。更确切地说,使用get和with方法,咱们能够将Temporal对象值的读取和修改区分开。若是Temporal对象不支持请求访问的字段,它会抛出一个UnsupportedTemporalTypeException异常,好比试图访问Instant对象的ChronoField.MONTH_OF_YEAR字段,或者LocalDate对象的ChronoField.NANO_OF_SECOND字段时都会抛出这样的异常。
它甚至能以声明的方式操纵LocalDate对象。好比,你能够像下面这段代码那样加上或者减去一段时间。
// 2018-11-17 LocalDate date1 = LocalDate.of(2018, 11, 17); // 2018-11-24 LocalDate date2 = date1.plusWeeks(1); // 2015-11-24 LocalDate date3 = date2.minusYears(3); // 2016-05-24 LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
与咱们刚才介绍的get和with方法相似最后一行使用的plus方法也是通用方法,它和minus方法都声明于Temporal接口中。经过这些方法,对TemporalUnit对象加上或者减去一个数字,咱们能很是方便地将Temporal对象前溯或者回滚至某个时间段,经过ChronoUnit枚举咱们能够很是方便地实现TemporalUnit接口。
大概你已经猜到,像LocalDate、LocalTime、LocalDateTime以及Instant这样表示时
间点的日期时间类提供了大量通用的方法,咱们目前所使用的只有一小部分,有兴趣的能够去看官网文档。
截至目前,你所看到的全部日期操做都是相对比较直接的。有的时候,你须要进行一些更加复杂的操做,好比,将日期调整到下个周日、下个工做日,或者是本月的最后一天。这时,你可使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。对于最多见的用例, 日期和时间API已经提供了大量预约义的TemporalAdjuster。你能够经过TemporalAdjuster类的静态工厂方法访问它们,以下所示。
// 2018-11-17 LocalDate date1 = LocalDate.of(2018, 11, 17); // 2018-11-19 LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)); // 2018-11-30 LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
正如咱们看到的,使用TemporalAdjuster咱们能够进行更加复杂的日期操做,并且这些方法的名称也很是直观,方法名基本就是问题陈述。此外,即便你没有找到符合你要求的预约义的TemporalAdjuster,建立你本身的TemporalAdjuster也并不是难事。实际上,TemporalAdjuster接口只声明了单一的一个方法(这使得它成为了一个函数式接口),定义以下。
@FunctionalInterface public interface TemporalAdjuster { Temporal adjustInto(Temporal temporal); }
这意味着TemporalAdjuster接口的实现须要定义如何将一个Temporal对象转换为另外一个Temporal对象。你能够把它当作一个UnaryOperator<Temporal>。
你可能但愿对你的日期时间对象进行的另一个通用操做是,依据你的业务领域以不一样的格式打印输出这些日期和时间对象。相似地,你可能也须要将那些格式的字符串转换为实际的日期对象。接下来的一节,咱们会演示新的日期和时间API提供那些机制是如何完成这些任务的。
处理日期和时间对象时,格式化以及解析日期时间对象是另外一个很是重要的功能。新的java.time.format包就是特别为这个目的而设计的。这个包中,最重要的类是DateTimeFormatter。建立格式器最简单的方法是经过它的静态工厂方法以及常量。像BASIC_ISO_DATE和ISO_LOCAL_DATE 这样的常量是DateTimeFormatter 类的预约义实例。全部的DateTimeFormatter实例都能用于以必定的格式建立表明特定日期或时间的字符串。好比,下面的这个例子中,咱们使用了两个不一样的格式器生成了字符串:
LocalDate date1 = LocalDate.of(2018, 11, 17); // 20181117 String s1 = date1.format(DateTimeFormatter.BASIC_ISO_DATE); // 2018-11-17 String s2 = date1.format(DateTimeFormatter.ISO_LOCAL_DATE);
你也能够经过解析表明日期或时间的字符串从新建立该日期对象。全部的日期和时间API都提供了表示时间点或者时间段的工厂方法,你可使用工厂方法parse达到重创该日期对象的目的:
LocalDate date2 = LocalDate.parse("20181117", DateTimeFormatter.BASIC_ISO_DATE); LocalDate date3 = LocalDate.parse("2018-11-17", DateTimeFormatter.ISO_LOCAL_DATE);
和老的java.util.DateFormat相比较,全部的DateTimeFormatter实例都是线程安全的。因此,你可以以单例模式建立格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。DateTimeFormatter类还支持一个静态工厂方法,它能够按照某个特定的模式建立格式器,代码清单以下。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); // 17/11/2018 String formattedDate = date1.format(formatter); LocalDate date4 = LocalDate.parse(formattedDate, formatter);
这段代码中,LocalDate的formate方法使用指定的模式生成了一个表明该日期的字符串。紧接着,静态的parse方法使用一样的格式器解析了刚才生成的字符串,并重建了该日期对象。ofPattern方法也提供了一个重载的版本,使用它你能够建立某个Locale的格式器,代码清单以下所示。
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); LocalDate date5 = LocalDate.of(2018, 11, 16); // 16. novembre 2018 String formattedDate2 = date5.format(italianFormatter); // 2018-11-16 LocalDate date6 = LocalDate.parse(formattedDate2, italianFormatter);
最后,若是你还须要更加细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂的格式器,你能够选择恰当的方法,一步一步地构造本身的格式器。另外,它还提供了很是强大的解析功能,好比区分大小写的解析、柔性解析(容许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充, 以及在格式器中指定可选节。
好比, 你能够经过DateTimeFormatterBuilder本身编程实现咱们在上面代码中使用的italianFormatter,代码清单以下。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder() .appendText(ChronoField.DAY_OF_MONTH) .appendLiteral(". ") .appendText(ChronoField.MONTH_OF_YEAR) .appendLiteral(" ") .appendText(ChronoField.YEAR) .parseCaseInsensitive() .toFormatter(Locale.ITALIAN); LocalDate now = LocalDate.now(); // 17. novembre 2018 String s1 = now.format(italianFormatter);
目前为止,你已经学习了如何建立、操纵、格式化以及解析时间点和时间段,可是你还不了解如何处理日期和时间之间的微妙关系。好比,你可能须要处理不一样的时区,或者因为不一样的历法系统带来的差别。接下来的一节,咱们会探究如何使用新的日期和时间API解决这些问题。
以前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增长的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,好比处理日光时(Daylight Saving Time,DST)这种问题。跟其余日期和时间类同样,ZoneId类也是没法修改的。
时区是按照必定的规则将区域划分红的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你能够简单地经过调用ZoneId的getRules()获得指定时区的规则。每一个特定的ZoneId对象都由一个地区ID标识,好比:
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。你能够经过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦获得一个ZoneId对象,你就能够将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它表明了相对于指定时区的时间点,代码清单以下所示。
LocalDate date = LocalDate.of(2018, 11, 17); ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone); LocalDateTime dateTime = LocalDateTime.of(2018, 11, 27, 18, 13, 15); ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone); Instant instant = Instant.now(); ZonedDateTime zdt3 = instant.atZone(shanghaiZone);
经过ZoneId,你还能够将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 17, 18, 45); Instant instantFromDateTime = dateTime.toInstant(shanghaiZone);
你也能够经过反向的方式获得LocalDateTime对象:
Instant instant = Instant.now(); LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, shanghaiZone);
另外一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定误差。好比,基于这个理论,你能够说“纽约落后于伦敦5小时”。这种状况下,你可使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差别:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
“-05:00”的误差实际上对应的是美国东部标准时间。注意,使用这种方式定义的ZoneOffset并未考虑任何日光时的影响,因此在大多数状况下,不推荐使用。因为ZoneOffset也是ZoneId,因此你能够像上面的代码那样使用它。你甚至还能够建立这样的OffsetDateTime,它使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的误差方式表示日期时间。
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 17, 18, 45); OffsetDateTime offsetDateTime = OffsetDateTime.of(dateTime, newYorkOffset);
能够说《Java8实战》的读书笔记相关的已经写完了,这本书后面还有最后一部分超越Java8,这一部分相关的章节都是跟函数式编程的思考与技巧相关,以及Java之后的将来等等。《Java8实战》这本书真的写的太好了并且这本书彻底能够看成一本关于Java8使用的工具书,随时能够翻开看看,看看关于Java8的特性是如何使用,该如何去避免一些坑,该如何使用Stream和Lambda表达式去简化你的代码。
Gitee:chap12
Github:chap12