Java的API提供了很多有用的组件,能帮助你构建复杂的应用。不过,Java API也不总是完美的。我们相信大多数有经验的程序员都会赞同Java 8之前的库对日期和时间的支持就非常不理想。然而,你也不用太担心:Java 8中引入全新的日期和时间API就是要解决这一问题。
让我们从探索如何创建简单的日期和时间间隔入手。 java.time 包中提供了很多新的类可以帮你解决问题,它们是 LocalDate 、 LocalTime 、 Instant 、 Duration 和 Period 。
LocalDate 类,该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。
你可以通过 静态工厂方法 of 创建一个 LocalDate 实例 。 LocalDate 实例提供了多种方法来读取常用的值,比如年份、月份、星期几等。
public static void main(String[] args) { //2014-3-18 LocalDate date = LocalDate.of(2014, 3, 18); //2014 int year = date.getYear(); //march Month month = date.getMonth(); //18 int day = date.getDayOfMonth(); DayOfWeek dow = date.getDayOfWeek(); //这个月的天数 int len = date.lengthOfMonth(); //是否事闰年 boolean leap = date.isLeapYear(); System.out.println("year : " + year + " month : " + month + " day : " + day + " dow : " + dow + " len : " + len + " leap : " + leap); } 复制代码
似地,一天中的时间,比如13:45:20,可以使用 LocalTime 类表示。你可以使用 of 重载的两个工厂方法创建 LocalTime 的实例。第一个重载函数接收小时和分钟,第二个重载函数同时还接收秒。同 LocalDate 一样, LocalTime 类也提供了一些 getter 方法访问这些变量的值。
LocalTime time = LocalTime.of(13, 45, 20); int hour = time.getHour(); int minute = time.getMinute(); int second = time.getSecond(); 复制代码
LocalDate 和 LocalTime 都可以通过解析代表它们的字符串创建。使用静态方法 parse ,你可以实现这一目的:
LocalDate date = LocalDate.parse("2014-03-18"); LocalTime time = LocalTime.parse("13:45:20"); 复制代码
这个复合类名叫 LocalDateTime ,是 LocalDate 和 LocalTime 的合体。它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造。
// 2014-03-18T13:45:20 LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); LocalDateTime dt2 = LocalDateTime.of(date, time); LocalDateTime dt3 = date.atTime(13, 45, 20); 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(); 复制代码
Duration 类的静态工厂方法 between 就是为比较两个时间而设计的。你可以创建两个 LocalTimes 对象、两个 LocalDateTimes对象,或者两个 Instant 对象之间的 duration:
Duration d1 = Duration.between(time1, time2); Duration d1 = Duration.between(dateTime1, dateTime2); 复制代码
如果你需要以年、月或者日的方式对多个时间单位建模,可以使用 Period 类。使用该类的工厂方法 between ,你可以使用得到两个 LocalDate 之间的时长:
Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18)); 复制代码
最后, Duration 和 Period 类都提供了很多非常方便的工厂类,直接创建对应的实例;换句话说,就像下面这段代码那样,不再是只能以两个temporal对象的差值的方式来定义它们的对象。
Duration threeMinutes = Duration.ofMinutes(3); Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES); Period tenDays = Period.ofDays(10); Period threeWeeks = Period.ofWeeks(3); Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1); 复制代码
//2019-3-20 LocalDate of = LocalDate.of(2019, 3, 20); //2018-3-20 LocalDate localDate = of.withYear(2018); //2018-4-20 LocalDate localDate1 = localDate.withMonth(4); //2018-9-20 LocalDate with = localDate1.with(ChronoField.MONTH_OF_YEAR, 9); 复制代码
最后这一行中使用的 with 方法和get方法有些类似,它们都声明于 Temporal 接口,所有的日期和时间API类都实现这两个方法,它们定义了单点的时间,比如 LocalDate 、 LocalTime 、 LocalDateTime 以及 Instant 。更确切 地说,使用 get 和 with 方法,我们可以将 Temporal 对象值的读取和修改区分开。
//2014-3-18 LocalDate date1 = LocalDate.of(2014, 3, 18); //2014-3-25 LocalDate date2 = date1.plusWeeks(1); LocalDate date3 = date2.minusYears(3); LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); 复制代码
最后一行使用的 plus 方法也是通用方法,它和 minus 方法都声明于 Temporal 接口中。通过这些方法,对 TemporalUnit 对象加上或者减去一个数字,我们能非常方便地将 Temporal 对象前溯或者回滚至某个时间段,通过ChronoUnit 枚举我们可以非常方便地实现 TemporalUnit 接口。
有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的 with 方法,向其传递一个提供了更多定制化选择的 TemporalAdjuster 对象,更加灵活地处理日期。对于最常见的用例,日期和时间API已经提供了大量预定义的TemporalAdjuster 。你可以通过 TemporalAdjuster 类的静态工厂方法访问它们。
//2014-03-18 LocalDate date1 = LocalDate.of(2014, 3, 18); //2014-03-23 LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); //2014-03-31 LocalDate date3 = date2.with(lastDayOfMonth()); 复制代码
正如我们看到的,使用 TemporalAdjuster 我们可以进行更加复杂的日期操作,而且这些方 法的名称也非常直观,方法名基本就是问题陈述。此外,即使你没有找到符合你要求的预定义的 TemporalAdjuster ,创建你自己的 TemporalAdjuster 也并非难事。实际上, Temporal- Adjuster 接口只声明了单一的一个方法(这使得它成为了一个函数式接口)。
@FunctionalInterface public interface TemporalAdjuster { Temporal adjustInto(Temporal temporal); } 复制代码
这意味着 TemporalAdjuster 接口的实现需要定义如何将一个 Temporal 对象转换为另一个 Temporal 对象。你可以把它看成一个 UnaryOperator
设计一个 NextWorkingDay 类,该类实现了 TemporalAdjuster 接口,能够计算明天的日期,同时过滤掉周六和周日这些节假日。格式如下所示:
public class MyTemporalAdjuster { public static void main(String[] args) { LocalDate of = LocalDate.of(2019, 3, 20); LocalDate with = of.with(new NewTemporalAdjuster()); System.out.println("with = " + with); } } //如果当天的星期介于周一至周五之间,日期向后移动一天;如果当天是周六或者周日,则返回下一个周一 class NewTemporalAdjuster implements TemporalAdjuster { @Override public Temporal adjustInto(Temporal temporal) { DayOfWeek of = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); int dayToAdd = 1; if (of == DayOfWeek.FRIDAY){ dayToAdd = 3; }else if (of == DayOfWeek.SATURDAY){ dayToAdd = 2; } return temporal.plus(dayToAdd, ChronoUnit.DAYS); } } 复制代码
处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的java.time.format 包就是特别为这个目的而设计的。这个包中,最重要的类是 DateTimeFormatter 。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像 BASIC_ISO_DATE和 ISO_LOCAL_DATE 这 样 的 常 量 是 DateTimeFormatter 类 的 预 定 义 实 例 。 所 有 的DateTimeFormatter 实例都能用于以一定的格式创建代表特定日期或时间的字符串。
LocalDate date = LocalDate.of(2014, 3, 18); //20140318 String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); //2014-03-18 String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); 复制代码
你也可以通过解析代表日期或时间的字符串重新创建该日期对象。所有的日期和时间API都提供了表示时间点或者时间段的工厂方法,你可以使用工厂方法 parse 达到重创该日期对象的目的:
LocalDate date1 = LocalDate.parse("20140318",DateTimeFormatter.BASIC_ISO_DATE); LocalDate date2 = LocalDate.parse("2014-03-18",DateTimeFormatter.ISO_LOCAL_DATE); 复制代码
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); LocalDate date1 = LocalDate.of(2014, 3, 18); String formattedDate = date1.format(formatter); LocalDate date2 = LocalDate.parse(formattedDate, formatter); 复制代码
LocalDate 的 formate 方法使用指定的模式生成了一个代表该日期的字符串。