Java 中的日期和时间(下)

目录

  1. 枚举类 Month 和 DayOfWeek
    1. Month
    2. DayOfWeek
  2. 日期和时间
    1. LocalDate 和 LocalTime
    2. LocalDateTime
  3. 解析和格式化
  4. 调节器
    1. TemporalAdjusters
    2. 自定义调节器

上集简单介绍了 Java 中的 Date 类,Calendar 类以及用于格式化的 SimpleDateFormater 类。使用这些类的时候我们会明显地感受到其中的不便之处,比如 Calendar 类的月份是从 0 开始计数的;日期格式输出不够友好,都需要使用 SimpleDateFormater 类来格式化;一些简单的日期计算也比较麻烦等等。所以就有了 joda-time 这种第三方库来简化 Java 对于日期和时间的操作。为了改变这种情况,jdk 8 中对日期和时间对处理就吸收了 joda-time 库的特性。

枚举类 Month 和 DayOfWeek

Month

Calendar 类的月份是从 0 开始计数的,因此月份的表示和计算比较复杂。JDK 8 中为了改变这一现状,增加了枚举类 Month 来表示月份,甚至可以直接使用这个枚举类来进行月份的加减运算。

public static Month of(int month) {
    if (month < 1 || month > 12) {
        throw new DateTimeException("Invalid value for MonthOfYear: " + month);
    }
    return ENUMS[month - 1];
}

of(int month) 方法用于创建一个 Month 对象。传入的参数范围为 [1,12],当传入的参数超出范围就会抛出异常。

public int getValue() {
    return ordinal() + 1;
}

getValue() 方法返回该 Month 对象当前的值。一月份返回1,二月份返回2,依次类推。

public Month plus(long months) {
    int amount = (int) (months % 12);
    return ENUMS[(ordinal() + (amount + 12)) % 12];
}

plus(long months) 方法用来计算月份的加法,传入的参数表示在该 Month 对象的基础上增加的月份,例如 12 月加上 2 个月,返回 2 月。

public Month minus(long months) {
    return plus(-(months % 12));
}

minus(long months) 方法和 plus(long months) 方法是类似的,例如是 1 月减去 2 个月,返回 11 月。

public int length(boolean leapYear) {
    switch (this) {
        case FEBRUARY:
            return (leapYear ? 29 : 28);
        case APRIL:
        case JUNE:
        case SEPTEMBER:
        case NOVEMBER:
            return 30;
        default:
            return 31;
    }
}

length(boolean leapYear) 方法和 maxLength() / minLength() 两个方法都是用来获取 Month 对象表示的该月的日期数。其中参数 leapYear 表示是否为闰年。这三个方法返回的结果在很多情况下都是一样的,返回的都是当月的日数,30或者31。只有二月份除外,当 Month 对象表示二月份时,maxLength()length(true) 返回29,minLength()length(false) 返回28。

System.out.println(Month.DECEMBER);         // DECEMBER
System.out.println(Month.of(2));            // FEBRUARY
Month month = Month.FEBRUARY;        
System.out.println(month.getValue());       // 2
System.out.println(month.minus(3));         // NOVEMBER
System.out.println(month.plus(2));          // APRIL
System.out.println(month.length(false));    // 28
System.out.println(month.length(true));     // 29

有时候我们希望返回月份是中文,这时候需要调用 getDisplayName(TextStyle style, Locale locale) 方法,第一个参数是文本类型,表示希望显示完整的名称还是缩写,第二个参数表示地区。

Month month = Month.APRIL;
System.out.println(month.getDisplayName(TextStyle.FULL, Locale.getDefault()));      // 二月
System.out.println(month.getDisplayName(TextStyle.SHORT, Locale.getDefault()));     // 二月
System.out.println(month.getDisplayName(TextStyle.NARROW, Locale.getDefault()));    // 2
System.out.println(month.getDisplayName(TextStyle.FULL, Locale.ENGLISH));           // February
System.out.println(month.getDisplayName(TextStyle.SHORT, Locale.ENGLISH));          // Feb
System.out.println(month.getDisplayName(TextStyle.NARROW, Locale.ENGLISH));         // F
}

DayOfWeek

枚举类 DayOfWeek 用来表示一周中的七天。常用的方法和枚举类 Month 几乎一致,包括静态方法 of(int dayOfWeek) 用于创建 DayOfWeek 对象; getValue() 方法用来获取该对象的值; plus(long days)minus(long days) 方法用来进行日期计算。同样也可以使用 getDisplayName(TextStyle style, Locale locale) 来格式化输出。

System.out.println(DayOfWeek.FRIDAY);       // FRIDAY
System.out.println(DayOfWeek.of(7));        // SUNDAY
DayOfWeek dayOfWeek = DayOfWeek.TUESDAY;
System.out.println(dayOfWeek.getValue());   // 2
System.out.println(dayOfWeek.plus(3));      // FRIDAY
System.out.println(dayOfWeek.minus(2));     // SUNDAY
System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault()));    // 星期二
System.out.println(dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()));   // 星期二
System.out.println(dayOfWeek.getDisplayName(TextStyle.NARROW, Locale.getDefault()));  // 二
System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, Locale.ENGLISH));         // Tuesday
System.out.println(dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.ENGLISH));        // Tue
System.out.println(dayOfWeek.getDisplayName(TextStyle.NARROW, Locale.ENGLISH));       // T
}

由于 MonthDayOfWeek 只是枚举类,它们并不持有当前时间信息,所以不能使用这两个枚举类来解决“今天是星期几”或者“明天是几号”等问题了。

日期时间

  • LocalDate 类可以获取当前日期(不包含时间),并可以进行相应处理。

  • LocalTime 类可以获取当前时间(不包含日期),并可以进行相应处理。

  • LocalDateTime 类可以同时处理日期和时间。

LocalDate 和 LocalTime

根据不同的需求,LocalDate 提供了不同的创建方式,主要包括 now()of() 这两个静态方法。

LocalDatedate1 = LocalDate.now();
LocalDatedate2 = LocalDate.of(2017, 1, 1);
LocalDatedate3 = LocalDate.ofEpochDay(364);
System.out.println(date1);    // 2017-03-13
System.out.println(date2);    // 2017-01-01
System.out.println(date3);    // 1970-12-31
LocalTimetime1 = LocalTime.now();
LocalTimetime2 = LocalTime.now().withNano(0);
LocalTimetime3 = LocalTime.of(23, 59);
LocalTimetime4 = LocalTime.ofSecondOfDay(12 * 60 * 60);
System.out.println(time1);    // 21:52:22.719
System.out.println(time2);    // 21:52:22
System.out.println(time3);    // 23:59
System.out.println(time4);    // 12:00

LocalDate 提供了大量的方法来进行日期信息的获取和计算,主要可以分为四类:获取日期信息,修改日期信息,加减法运算和日期对象间的比较。

方法名 返回值 备注
getYear() int 获取当前日期的年份
getMonth() Month 获取当前日期的月份对象
getMonthValue() int 获取当前日期是第几月
getDayOfWeek() DayOfWeek 表示该对象表示的日期是星期几
getDayOfMonth() int 表示该对象表示的日期是这个月第几天
getDayOfYear() int 表示该对象表示的日期是今年第几天
withYear(int year) LocalDate 修改当前对象的年份
withMonth(int month) LocalDate 修改当前对象的月份
withDayOfMonth(int dayOfMonth) LocalDate 修改当前对象在当月的日期
isLeapYear() boolean 是否是闰年
lengthOfMonth() int 这个月有多少天
lengthOfYear() int 该对象表示的年份有多少天(365或者366)
plusYears(long yearsToAdd) LocalDate 当前对象增加指定的年份数
plusMonths(long monthsToAdd) LocalDate 当前对象增加指定的月份数
plusWeeks(long weeksToAdd) LocalDate 当前对象增加指定的周数
plusDays(long daysToAdd) LocalDate 当前对象增加指定的天数
minusYears(long yearsToSubtract) LocalDate 当前对象减去指定的年数
minusMonths(long monthsToSubtract) LocalDate 当前对象减去注定的月数
minusWeeks(long weeksToSubtract) LocalDate 当前对象减去指定的周数
minusDays(long daysToSubtract) LocalDate 当前对象减去指定的天数
compareTo(ChronoLocalDate other) int 比较当前对象和 other 对象在时间上的大小,返回值如果为正,则当前对象时间较晚,
isBefore(ChronoLocalDate other) boolean 比较当前对象日期是否在 other 对象日期之前
isAfter(ChronoLocalDate other) boolean 比较当前对象日期是否在 other 对象日期之后
isEqual(ChronoLocalDate other) boolean 比较两个日期对象是否相等

列表中的参数 ChronoLocalDate 是一个接口,LocalDate 是它的实现类,所以可以直接传入一个 LocalDate对象。

LocalDate localDate = LocalDate.now();
System.out.println(localDate.getYear());                 // 2017
System.out.println(localDate.getDayOfWeek());            // MONDAY
System.out.println(localDate.getDayOfMonth());           // 13
System.out.println(localDate.withMonth(3));              // 2017-03-13
System.out.println(localDate.minusWeeks(2));             // 2017-02-27
System.out.println(localDate.plusDays(10));              // 2017-03-23
LocalDate firstDayOfYear = LocalDate.of(2017, 1, 1);
System.out.println(localDate.compareTo(firstDayOfYear)); // 2
System.out.println(localDate.isAfter(firstDayOfYear));   // true
System.out.println(localDate.isEqual(firstDayOfYear));   // false

LocalTime 类中的常用方法和 LocalDate 类似,同样可以分为:获取时间信息,修改时间信息,加减法运算和时间对象间的比较,不过 LocalTime 类中没有 isEqual() 方法。

LocalDateTime

将日期和时间分开处理可能会有些不方便,这时可以使用 LocalDateTime 类。

LocalDateTime 类的创建方式也同 LocalDate 类和 LocalTime 类相似:

LocalDateTime dateTime1 = LocalDateTime.now();
LocalDateTime dateTime2 = LocalDateTime.of(2008, 8, 8, 8, 8);
LocalDateTime dateTime3 = LocalDateTime.of(1993, 10, 26, 11, 10, 30);
System.out.println(dateTime1);    // 2017-03-13T22:20:53.431
System.out.println(dateTime2);    // 2008-08-08T08:08
System.out.println(dateTime3);    // 1993-10-26T11:10:30

通常需要在 of() 方法中传入6个 int 参数,分别表示年月日时分秒。关于月份,既可以传入 Month 对象,也可以传入 int 值。另外也可以传入 5 个参数,将秒省略了,或者也可以增加一个纳秒参数,变为传入 7 个参数。

LocalDateTime 类的其它常用方法同 LocalDate 类和 LocalTime 类也是类似的,不再赘述。

LocalDateTime 类可以方便地转换为 LocalDate 类或 LocalTime 类。

LocalDateTime dateTime = LocalDateTime.now();
LocalDate date = dateTime.toLocalDate();
LocalTime time = dateTime.toLocalTime();
System.out.println(dateTime);    // 2017-03-13T22:28:33.816
System.out.println(date);        // 2017-03-13
System.out.println(time);        // 22:18:33.816

解析和格式化

在 JDK 7 的时代,想要格式化一个日期,只能用 Date 类,并且 SimpleDateFormat 类还存在线程安全隐患,而在 JDK 8 中,这些问题都不复存在了。

LocalDate date = LocalDate.parse("2017-03-13");
LocalTime time = LocalTime.parse("22:34:45");
LocalDateTime dateTime = LocalDateTime.parse("2017-03-13T22:34:45");
System.out.println(date);         // 2017-03-13
System.out.println(time);         // 22:34:45
System.out.println(dateTime);     // 2017-03-13T22:34:45

也可以按照自定义的格式进行解析:

String text = "2017/03/13 22:37:39";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(LocalDateTime.parse(text, formatter));    // 2017-03-13T22:37:39

日期和时间的格式化同样需要用到 DateTimeFormatter 类:

LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dateTime.format(formatter));    // 2017/03/13 22:40:52

调节器

TemporalAdjusters

新版日期和时间的处理方法,与以前使用的 DateCalendar 类最明显的区别就是调节器了。

LocalDate date = LocalDate.now();
System.out.println("今天是 " + date + " " + date.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault()));    // 今天是 2017-03-14 星期二
System.out.println("下周二是 " + date.with(TemporalAdjusters.next(DayOfWeek.TUESDAY)));    // 下周二是 2017-03-21
System.out.println("本月的第一个周二是 " + date.with(TemporalAdjusters.firstInMonth(DayOfWeek.TUESDAY)));    // 本月的第一个周二是 2017-03-07

得到 LocalDate 对象后,调用 with() 方法,传入一个 TemporalAdjusters 对象即可。TemporalAdjusters 类有许多静态方法来创建该对象:

  • firstDayOfMonth()

  • lastDayOfMonth()

  • firstDayOfNextMonth()

  • firstDayOfYear()

  • lastDayOfYear()

  • firstDayOfNextYear()

  • firstInMonth(DayOfWeek dayOfWeek)

  • lastInMonth(DayOfWeek dayOfWeek)

  • dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)

  • next(DayOfWeek dayOfWeek)

  • nextOrSame(DayOfWeek dayOfWeek)

  • previous(DayOfWeek dayOfWeek)

  • previousOrSame(DayOfWeek dayOfWeek)

如果这些方法不能满足需求,就需要自定义调节器了。

自定义调节器

自定义一个调节器很简单,创建一个类,实现 TemporalAdjuster 接口,重写 adjustInto(Temporal temporal) 方法。

假设一个场景,一个公司每个月清账两次,分别是本月 15 号和最后一天。如果恰逢周末,则提前到周五清帐。如何自定义一个调节器,计算下一次清帐时间呢?

LocalDate date = LocalDate.of(2016, Month.DECEMBER, 20).with(new TemporalAdjuster() {
    @Override
    public Temporal adjustInto(Temporal temporal) {
        LocalDate date = LocalDate.from(temporal);
        int day = date.getDayOfMonth() < 15 ? 15 : date.lengthOfMonth();
        date = date.withDayOfMonth(day);
        if (DayOfWeek.SATURDAY.equals(date.getDayOfWeek()) || DayOfWeek.SUNDAY.equals(date.getDayOfWeek())) {
            date = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
        }
        return temporal.with(date);
    }
});
System.out.println(date);    // 2016-12-30

2016年12月31日是周六,所以清账日期提前至周五。