Java 日期、时间 API 总结

日期、时间的相关抽象

Java 8 对时间、日期进行了新的抽象,构建了一系列的日期、时间对象用作相关的时间操作。

LocalDate , LocalTime 和 LocalDateTime

LocalDate 是对日期的抽象,即年月日。LocalTime 是对时间的抽象,即时分秒。而 LocalDateTime 即是 LocalDate 以及 LocalTime 的结合,表示日期与时间。
以上三者都是在开发时常用到的三种时间表示方式。三者的实例都是不可变对象,每次时间的改变,都会新创建一个对应的对象,所以它们是线程安全的。
下面将较为详细的介绍三者。

LocalDate

LocalDate 表示日期,获取一个 LocalDate 对象有如下方式:

// 创建表示当前日期的 LocalDate 对象
LocalDate nowDate = LocalDate.now();
// 创建指定日期的 LocalDate 对象
LocalDate specificDate = LocalDate.of(2018, 8, 8);
// 获取最小日期,通常用来表示很久很久以前
LocalDate minDate = LocalDate.MIN;
// 获取获取最大日期,通常用来表示很远很远的未来
LocalDate maxDate = LocalDate.MAX;

除此之外,还有一种解析字符串创建 LocalDate 的方式,这也是在实际开发中经常用到的。在默认情况下使用 ISO 8601 标准。有以下用例:

LocalDate specificDate = LocalDate.parse("2018-08-08");

获取到实例后,有各种实用的、可读性强的 API 供我们读取相关的时间信息。有以下用例:

LocalDate date = LocalDate.of(2018, 8, 8);
// 获取年份
int year = date.getYear();
// 获取月份对象,可以使用 getMonthValue 方法获取月份值,范围为 1-12
Month month = date.getMonth();
// 获取日期
int day = date.getDayOfMonth();
// 获取周对象
DayOfWeek dow = date.getDayOfWeek();
// 获取本日期的月份长度
int len = date.lengthOfMonth();
// 判断本日期是否为闰年
boolean leap = date.isLeapYear();

除此之外,LocalDate 还有一个 get 方法,可以接受一个 TemporalField 参数拿到指定的信息。该接口定义了如何获取各个日期、时间对象某个字段的值,枚举类 ChronoField 实现了该接口,可以很方便的通过传递 ChronoField 枚举类的实例来获取指定信息。有以下用例:

int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

要注意的是,不同的日期、时间对象可以接受的 TemporalField 参数是不同的,如果无法接受传递 TemporalField 的参数将会抛出异常 UnsupportedTemporalTypeException 。

除了获取信息外,修改日期、时间对象也是常见的需求,例如,要获取当前日期 3 天后的日期。可以利用日期、时间对象的 plus 方法便捷地完成该需求:

LocalDate date = LocalDate.now().plus(3, ChronoUnit.DAYS);

前面提到,LocalDate 是不可变对象,所以 plus 方法返回的是一个新的 LocalDate 对象。如果想获取的是三天前的日期,传递一个负数即可。

LocalTime

LocalTime 表示时间(时分秒),创建一个 LocalTime 实例有如下方式:

// 获取当前时间
LocalTime nowTime = LocalTime.now();
// 获取指定时间,12时30分30秒
LocalTime specificTime = LocalTime.of(12, 30, 30);
// 获取当天最小时间,即 00时00分
LocalTime minTime = LocalTime.MIN;
// 获取当天最大时间,即 23时59分59.999999999秒
LocalTime maxTime = LocalTime.MAX;
// 半夜十二点,与 LocalTime.MIN 一样的值
LocalTime midnightTime = LocalTime.MIDNIGHT;
// 中午十二点
LocalTime noonTime = LocalTime.NOON;
// 解析字符串获取时间对象,以 ISO 8601 标准为解析格式
LocalTime strSpecificTime = LocalTime.parse("12:30:30");

有以下获取 LocalTime 对象信息的方式:

int hour = time.getHour();
int minute = time.getMinute(); 
int second = time.getSecond();
int minuteOfDay = time.get(ChronoField.MINUTE_OF_DAY);

同样也可以使用 plus 方法修改对象,当然返回的将会是一个新的 LocalTime 实例。

// 获取当前时间 2 分钟后的时间
LocalTime time = LocalTime.now().plus(2, ChronoUnit.MINUTES);

LocalDateTime

LocalDateTime 表示时间日期,是 LocalDate 与 LocalTime 的结合,LocalDate 或是 LocalTime 有的特性 LocalDateTime 基本上都有。
可以通过以下方式获取 LocalDateTime 的实例:

// 获取当前时间和日期
LocalDateTime nowDateTime = LocalDateTime.now();
// 获取指定的时间日期,如 2018-08-12T12:30:30
LocalDateTime specificDateTime = LocalDateTime.of(2018, Month.AUGUST, 12, 12, 30, 30);
// 解析字符串获取日期时间
LocalDateTime strSpecificDateTime = LocalDateTime.parse("2018-08-12T12:30:30");
// 最小日期时间,通常用来表示很久很久以前
LocalDateTime minDateTime = LocalDateTime.MIN;
// 最大日期时间,通常用来表示很远很远的未来
LocalDateTime maxDateTime = LocalDateTime.MAX;

// 通过 LocalDate 实例以及 LocalTime 实例获取 LocalDateTime 实例
LocalDate date = LocalDate.of(2018, 8, 8);
LocalTime time = LocalTime.of(12, 30, 30);

LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt1 = date.atTime(time);
LocalDateTime dt2 = date.atTime(12, 30, 30);
LocalDateTime dt3 = time.atDate(date);

能够通过 LocalDate 实例以及 LocalTime 实例获取 LocalDateTime 实例,自然也能通过 LocalDateTime 对象获取到 LocalDate ,LocalTime 对象。

LocalDate date = dateTime.toLocalDate();
LocalTime time = dateTime.toLocalTime();

通过 LocalDate 和 LocalTime 获取的信息,基本上也可以通过 LocalDateTime 获取到。有以下简单的用例:

int year = dateTime.getYear();
int minute = dateTime.getMinute();
int dayOfHours = dateTime.get(ChronoField.HOUR_OF_DAY);

最后对于 LocalDateTime 对象的修改,同样,实际上没有修改 LocalDateTime 对象,而是返回一个新的对象。

LocalDateTime.now().plus(-1, ChronoUnit.DAYS);
LocalDateTime.now().plus(3, ChronoUnit.HOURS);

Instant

前面提到的日期时间类如 LocalDate ,对人类是友好的,因为它们以我们所属性的方式表现时间。但是对于机器来说,多少年,几号,周几这样的算法是不友好的,而像 UNIX 时间戳这种用数值来表示当前时间点的方式对机器跟为友好。
Instant 类是对时间线上的某个时间点的抽象,简单点说就是对时间戳的抽象。它也是一个不可变对象,当尝试去修改一个 Instant 实例时,将会得到一个新的 Instant 实例。
可以通过以下方式获取 Instant 的实例:

// 获取当前时间
Instant nowInstant = Instant.now();
// 通过时间戳数值获取时间,单位为秒
Instant specificInstantBySecond = Instant.ofEpochSecond(1540182630L);
// 在 1540182630 的基础上加上一百万纳秒,即 1 秒
Instant specificInstantBySecondAndNano = Instant.ofEpochSecond(1540182630L, 1_000_000_000);
// 通过时间戳数值获取时间,单位为毫秒
Instant specificInstantByMilli = Instant.ofEpochMilli(1540182630 * 1000L);
// 通过解析字符串获取时间,默认解析标准为 ISO 8601 。
Instant specificInstantByStr = Instant.parse("2018-10-22T12:30:00.000Z");

通过 Instant 对象,可以获取到的是时间戳数值。

long timeStampSecond = Instant.now().getEpochSecond();
long timeStampMill = Instant.now().get(ChronoField.MILLI_OF_SECOND);

对于 Instant 的修改,主要也是用到相关的 plus 方法。

Instant instant1 = instant.plusSeconds(1L);
Instant instant2 = instant.plusMillis(1000L);
Instant instant3 = instant.plus(1L, ChronoUnit.SECONDS);

Duration 和 Period

上面提到的相关时间类表示的是时间点,而 Duration 和 Period 是对时间段的抽象。
Duration 与 Period 的区别在于 Duration 主要是以整数值(秒和纳秒)来表示时间段,而 Period 则是以对人类友好的方式来表示时间段,即一周,一个月,一年等。

Duration 和 Period 可以用一系列 of 开头的静态方法来创建实例。有以下简单的例子:

// 十分钟
Duration tenMin = Duration.ofMinutes(10);
// 两周
Period twoWeek = Period.ofWeeks(2);

然后可以通过这两个对象获取一些简单的信息:

Duration tenMin = Duration.ofMinutes(10);
long seconds = tenMin.getSeconds();

Period twoWeek = Period.ofWeeks(2);
int days = twoWeek.getDays();

Duration 和 Period 的一个最常用功能就是它们的静态方法 between ,该方法可以计算两个时间实例之间的时间差。

LocalDateTime nowDateTime = LocalDateTime.now();
LocalDateTime weekAgoDateTime = nowDateTime.plus(-2, ChronoUnit.WEEKS);
Duration duration = Duration.between(weekAgoDateTime, nowDateTime);
System.out.println(duration.getSeconds()); // 1209600

LocalDate nowDate = LocalDate.now();
LocalDate weekAgoDate = nowDate.plus(-2, ChronoUnit.WEEKS);
Period period = Period.between(weekAgoDate, nowDate);
System.out.println(period.getDays()); // 14

注意的是,Duration 的 between 方法只能用于 Instant ,LocalTime 和 LocalDateTime 等对象的时间差计算。而 Period 的 between 方法只能用于 LocalDate 的时间差计算。而且不同的时间类型对象是不可以混用的,如试图计算 LocalDateTime 对象和 Instant 对象之间的时间差。

Duration 对象还有一个常见的用处,那就是作为参数传给各个日期、时间对象的 plus 方法。
上面提到的各个日期、时间对象的 plus 方法其实在内部都是利用了一个 Duration 对象来计算日期、时间对象修改后的值,当然也可以显示传递一个 Duration 对象。

LocalTime nowTime = LocalTime.now();
LocalTime afterTenMin = nowTime.plus(Duration.ofMinutes(10));

日期、时间对象的修改

首先要明确日期、时间对象都是不可变对象,所以“修改”实际上是创建了一个新的对象。
对于日期、时间对象的修改,最常用的就是它们的 plus 系列方法,在前面关于各个日期、时间对象的介绍中也有提到。除此之外,日期、时间对象还有一系列的 with 方法,可以进行相关的时间修改操作。这里以 LocalDate 为例:

// 初始日期为 2018-08-08
LocalDate date = LocalDate.of(2018, 8, 8);
// 将年份修改为 2017 年,日期变为 2017-08-08
LocalDate dateModifyYear = date.withYear(2017);
// 将月份修改为 9 月,日期变为 2018-09-08
LocalDate dateModifyMonth = date.withMonth(9);
// 将日期修改为 15 号,日期变为 2018-08-15
LocalDate dateModifyDay = date.with(ChronoField.DAY_OF_MONTH, 15);

如果需要更加复杂的修改操作,就需要用到接口 TemporalAdjuster 。

@FunctionalInterface
public interface TemporalAdjuster {
    /**
     * 设置日期变化的逻辑
     * @param temporal 输入的时间对象,即需要进行修改的时间对象
     * @return 输出的时间杜希昂,即修改后的时间对象
     */
    Temporal adjustInto(Temporal temporal);
}

该接口声明为函数式接口,即可以以函数式编程的方式使用。
各个日期、时间类都有一个 with 方法,该 with 方法接受一个参数,即 TemporalAdjuster 。修改日期或事件的逻辑就在于接口 TemporalAdjuster 方法 adjustInto 的具体实现。

在 Java 8 的时间 API 中,有不少类都实现了该接口,可以利用这些类完成一些时间修改的操作,而且有工厂类 TemporalAdjusters 实现了不少常用到的时间修改操作。

// 初始日期为 2018-08-08
LocalDate date = LocalDate.of(2018, 8, 8);
// 将日期修改为本周五的日期
LocalDate dateModifyDayOfWeek = date.with(DayOfWeek.FRIDAY);
// 将日期修改为月初
LocalDate dateModifyFirstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());

当然除此之外还可以自己实现 TemporalAdjuster 接口,实现自定义的日期、时间修改逻辑。

字符串与时间的转换

在开发中经常会遇到字符串与日期、时间对象的相互转换的需求,在 Java 8 中,新添加了一个时间格式化类 DateTimeFormatter 专门用于字符串与日期时间的相互转换。

DateTimeFormatter 自身定义了许多静态实例,代表了各种常用的日期时间表示格式,如下:

Formatter Description Example
BASIC_ISO_DATE Basic ISO date '20111203'
ISO_LOCAL_DATE ISO Local Date '2011-12-03'
ISO_OFFSET_DATE ISO Date with offset '2011-12-03+01:00'
ISO_DATE ISO Date with or without offset '2011-12-03+01:00'; '2011-12-03'
ISO_LOCAL_TIME Time without offset '10:15:30'
ISO_OFFSET_TIME Time with offset '10:15:30+01:00'
ISO_TIME Time with or without offset '10:15:30+01:00'; '10:15:30'
ISO_LOCAL_DATE_TIME ISO Local Date and Time '2011-12-03T10:15:30'
ISO_OFFSET_DATE_TIME Date Time with Offset 2011-12-03T10:15:30+01:00'
ISO_ZONED_DATE_TIME Zoned Date Time '2011-12-03T10:15:30+01:00[Europe/Paris]'
ISO_DATE_TIME Date and time with ZoneId '2011-12-03T10:15:30+01:00[Europe/Paris]'
ISO_ORDINAL_DATE Year and day of year '2012-337'
ISO_WEEK_DATE Year and Week 2012-W48-6'
ISO_INSTANT Date and Time of an Instant '2011-12-03T10:15:30Z'
RFC_1123_DATE_TIME RFC 1123 / RFC 822 'Tue, 3 Jun 2008 11:05:30 GMT'

字符串转换为日期、时间

在前面可以看到,LocalDate , LocalTime 以及 LocalDate 都可以利用静态方法 parse 将字符串转换为对应的日期、时间对象。而这个 parse 方法就是字符串转换为时间的方法。该方法还可以传递一个 DateTimeFormatter 对象作为解析格式,默认情况下使用的就是 DateTimeFormatter.ISO_LOCAL_DATE 、DateTimeFormatter.ISO_LOCAL_TIME , DateTimeFormatter.ISO_LOCAL_DATE_TIME 这三个表示 ISO 8601 标准的对象。

除了可以传递上述提到的静态对象,还可以自定义格式,然后进行字符串的解析,如下:

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date = LocalDate.parse("2018/08/08", dateTimeFormatter);

日期、时间转换为字符串

日期、时间转换为字符串主要是利用 DateTimeFormatter 的 format 方法。有如下示例:

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
String dateTimeStr = dateTimeFormatter.format(LocalDateTime.now());

除了常规的解析外,还可以进行国际化。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM uuuu", Locale.SIMPLIFIED_CHINESE);
String dateStr = formatter.format(LocalDate.now());
System.out.println(dateStR); // 十月 2018

更多预定义的格式

除了上面提到定义好的静态实例外,DateTimeFormatter 有三个静态方法 ofLocalizedDate ,ofLocalizedTime 和 ofLocalizedDateTime ,接受参数 FormatStyle 对象。FormatStyle 是一个枚举类,它有以下定义:

public enum FormatStyle {
    /**
     * Full text style, with the most detail.
     * For example, the format might be 'Tuesday, April 12, 1952 AD' or '3:30:42pm PST'.
     */
    FULL,
    /**
     * Long text style, with lots of detail.
     * For example, the format might be 'January 12, 1952'.
     */
    LONG,
    /**
     * Medium text style, with some detail.
     * For example, the format might be 'Jan 12, 1952'.
     */
    MEDIUM,
    /**
     * Short text style, typically numeric.
     * For example, the format might be '12.13.52' or '3:30pm'.
     */
    SHORT;
}

根据传递的 FormatStyle 不同,获取到的 DateTimeFormatter 也不同,即解析的格式也不同,而且得到的 DateTimeFormatter 是进行了本地化的,有如下例子:

String dateStr = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(LocalDate.now());
System.out.println(dateStr); // 2018年10月22日 星期一

三个静态方法中,ofLocalizedDateTime 稍微特别一点,因为它可以接受两个 FormatStyle 对象作为参数,分别对应于解析日期和时间。

String dateStr = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.FULL)
        .format(LocalDate.now());
System.out.println(dateStr); // 2018年10月22日 星期一

如果不想本地化,可以利用方法 withLocal 方法改变语言。

String dateStr = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.FULL)
        .withLocale(Locale.US)
        .format(LocalDate.now());
System.out.println(dateStr); // Monday, October 22, 2018

时区

一个完整的时间表达是包含时区的,同一个时间点,对于不同时区的人来说时间的表达是不一样的,不同的时区,在日期和时间的计算上有不同的标准。
LocalDate 、LocalTime 和 LocalDateTime 是一种主观上的时间表示方式,对于人来说,同一个 LocalDate 实例所表达的意义是相同的,但是在时间线上,不同的时区在不同的时间点上。

对于时区信息的问题,可以使用类 ZoneId ,它由一个地区 ID 来进行标识,利用地区 ID 来获取 ZoneId 的实例,而且是不可变的,地区 ID 为“{区域}/{城市}”的格式。获取一个 ZoneId 实例:

ZoneId eastEightArea = ZoneId.of("Asia/Shanghai");

还有一种常用的方式是利用它的静态方法 systemDefault 来获取本地机器的时区信息:

ZoneId systemZoneId = ZoneId.systemDefault();

对于时区信息的获取,还可以利用UTC/格林尼治时间的固定偏差计算时区。

// 获取东八区时区的两种方式
ZoneOffset eastNightAreaZoneOffset = ZoneOffset.of("+8");
ZoneId eastNightAreaZoneId = ZoneId.of("UTC+8");

ZoneOffset 是对时区偏差的抽象,是 ZoneId 的一个子类。如果 ZoneId 不使用“{区域}/{城市}”的地区 ID 格式,内部将会使用偏差来计算时区。

获取到时区后,可以在相关的日期、时间对象上添加时区信息,构造成一个具有时区信息的日期、时间对象 ZonedDateTime。ZonedDateTime 也可以转换为其他日期、时间对象。

ZoneId eastEightArea = ZoneId.of("Asia/Shanghai");

// LocalDate 与 ZonedDateTime 的相互转换
LocalDate date = LocalDate.now();
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
date = zdt1.toLocalDate();

// LocalDateTime 与 ZonedDateTime 的相互转换
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
dateTime = zdt2.toLocalDateTime();

// Instant 与 ZonedDateTime 的相互转换
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
instant = zdt3.toInstant();

日历系统

ISO-8601 日历系统是世界文明日历系统的事实标准,也是 Java 8 时间 API 体系中默认使用的日历系统。除此之外,还有其他的日历系统,如我们常用的农历(可惜的是 Java 8 没提供),不同的日历系统,对时间的表示有所不同。
接口 Chronology 抽象了日历,相关的日历系统都实现了该接口。对此感兴趣的可以参考其 API 文档。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章