第12章 新的日期和时间 API

笔记来源:《Java实战(第2版)》ISBN:978-7-115-52148-4 作者:拉乌尔·加布里埃尔·乌尔玛,马里奥·富斯科,艾伦·米克罗夫特.

Java实战(第2版)学习笔记目录

  • Joda-Time 第三方日期和时间库

12.1 LocalDate、LocalTime、LocalDateTime、Instant、Duration 以及 Period

12.1.1 使用 LocalDate 和 LocalTime

LocalDate

  • 是一个不可变对象
  • 只提供了简单的日期
  • 不含当天的时间信息
  • 也不附带任何与时区相关的信息

可以通过静态工厂方法 of 创建一个 LocalDate 实例

1
2
3
4
5
6
7
8
    LocalDate date = LocalDate.of(2014, 3, 18);
    int year = date.getYear(); // 2014
    Month month = date.getMonth(); // MARCH
    int day = date.getDayOfMonth(); // 18
    DayOfWeek dow = date.getDayOfWeek(); // TUESDAY
    int len = date.lengthOfMonth(); // 31 (days in March)
    boolean leap = date.isLeapYear(); // false (not a leap year)
    System.out.println(date);

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

1
LocalDate today = LocalDate.now();

LocalTime

  • 可以表示一天中的时间,比如 13:45:20
1
2
3
4
5
    LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
    int hour = time.getHour(); // 13
    int minute = time.getMinute(); // 45
    int second = time.getSecond(); // 20
    System.out.println(time);

使用静态方法 parse 也可以创建

1
2
LocalDate date = LocalDate.parse("2017-09-21");
LocalTime time = LocalTime.parse("13:45:20");

12.1.2 合并日期和时间

  • LocalDateTime:LocalDate 和 LocalTime 的合体
  • 同时表示日期和时间
  • 但不带有时区信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    // 2014-03-18T13:45
    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);
    System.out.println(dt1);

    LocalDate date1 = dt1.toLocalDate();
    System.out.println(date1);
    LocalTime time1 = dt1.toLocalTime();
    System.out.println(time1);

12.1.3 机器的日期和时间格式

java.time.Instant

  • 建模方式:表示一个持续时间段上某个点的单一大整型数
  • 以 Unix 元年时间(传统的设定为 UTC 时区 1970 年 1 月 1 日午夜时分)开始所经历的秒数进行计算
  • 可以通过向静态工厂方法 ofEpochSecond 传递代表秒数的值创建一个该类的实例
  • ofEpochSecond 增强的重载版本,它接受第二个以纳秒为单位的参数值,对传入作为秒数的参数进行调整
    • 确保保存的纳秒分片在 0 到 999 999 999 之间
1
2
3
4
5
6
    Instant instant = Instant.ofEpochSecond(44 * 365 * 86400);
    Instant now = Instant.now();
    // 2 秒之后再加上 10 亿纳秒(1 秒)
    Instant instant = Instant.ofEpochSecond(2, 1_000_000_000);
    // 4 秒之前的 10 亿纳秒(1 秒)
    Instant instant = Instant.ofEpochSecond(4, -1_000_000_000);
  • Instant 的设计初衷是为了便于机器使用
    • 它包含的是由秒及纳秒所构成的数字
    • 它无法处理那些非常容易理解的时间单位(例如,获得今天是这个月的第几天)
      • 可以通过 Duration 或 Period 类使用 Instant

12.1.4 定义 Duration 或 Period

Duration 类主要用于以秒和纳秒衡量时间

  • 不能仅向 between 方法传递一个 LocalDate 对象做参数
1
2
3
4
5
6
7
8
        LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
        Instant instant = Instant.ofEpochSecond(44 * 365 * 86400);
        Instant now = Instant.now();

        Duration d1 = Duration.between(LocalTime.of(13, 45, 10), time);
        Duration d2 = Duration.between(instant, now);
        System.out.println(d1.getSeconds());
        System.out.println(d2.getSeconds());

Period 以年、月、日的方式对多个时间单位建模

工厂方法 between 可以得到两个 LocalDate 之间的时长

1
2
3
        Period tenDays = Period.between(LocalDate.of(2017, 9, 11),
                                        LocalDate.of(2017, 9, 21));
        System.out.println(tenDays.getDays());

Duration 和 Period 便利的创建工厂

1
2
3
4
5
        Duration threeMinutes = Duration.ofMinutes(3);
        Duration threeDays = Duration.of(3, ChronoUnit.DAYS);
        Period tenDays = Period.ofDays(10);
        Period threeWeeks = Period.ofWeeks(3);
        Period towYearsSixMonthsOneDay = Period.of(2, 6, 1);

表 12-1 Duration 和 Period 中表示时间间隔的通用方法

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

截止目前,Duration 和 Period 对象都是不可修改的

  • 更好的支持函数式编程
  • 线程安全

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

以下代码中所有的方法都返回一个修改了属性的对象,不会修改原来的对象

1
2
3
4
        LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21
        LocalDate date2 = date1.withYear(2011); // 2011-09-21
        LocalDate date3 = date2.withDayOfMonth(25); // 2011-09-25
        LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); // 2011-02-25
  • 使用 get 和 with 方法,可以将 Temporal 对象值的读取和修改区分开
    • with 方法并不会直接修改现有的 Temporal 对象
    • 它会创建现有对象的副本并更新对应的字段
    • 这一过程也被称作函数式更新

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

1
2
3
4
        LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21
        LocalDate date2 = date1.plusWeeks(1); // 2017-09-28
        LocalDate date3 = date2.minusYears(6); // 2011-09-28
        LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2012-03-28

12.2.1 使用 TemporalAdjuster

  • 将日期调整到下个周日、下个工作日
  • 本月的最后一天
1
2
3
4
5
        import static java.time.temporal.TemporalAdjusters.*;

        LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
        LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23
        LocalDate date3 = date2.with(lastDayOfMonth()); // 2014-03-31

表 12-3 TemporalAdjusters 类中的工厂方法

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

实现自己的时间调节器

  • 使用 Lambda 表达式定义 TemporalAdjuster 对象,推荐使用静态工厂方法 TemporalAdjusters.ofDateAdjuster

下一个工作日

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
        TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(temporal -> {
            DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); // 读取当前日期
            int dayToAdd = 1; // 正常情况:增加一天
            if (dow == DayOfWeek.FRIDAY) {
                dayToAdd = 3; // 如果当天是周五,增加三天
            }
            if (dow == DayOfWeek.SATURDAY) {
                dayToAdd = 2; // 如果当天是周六,增加两天
            }
            return temporal.plus(dayToAdd, ChronoUnit.DAYS); // 增加恰当的天数后,返回修改的日期
        });

        LocalDate date1 = LocalDate.of(2021, 1, 22); // 2021-01-22
        LocalDate date2 = date1.with(nextWorkingDay); // 2021-01-25

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

包 java.time.format

  • 格式化、解析日期
  • 最重要的类 DateTimeFormatter

格式化日期

1
2
3
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18

字符串转时间类型

1
2
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);

和老的 java.util.DateFormat 相比较, 所有的 DateTimeFormatter 实例都是线程安全的

按照自己喜欢的格式创建 DateTimeFormatter

1
2
3
4
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate dateILike = LocalDate.of(2021, 1, 27);
String formattedDate = dateILike.format(formatter); // 2021/01/27
LocalDate dateULike = LocalDate.parse(formattedDate, formatter);

创建一个本地化的 DateTimeFormatter

1
2
3
4
5
LocalDate date = LocalDate.of(2021, 1, 27);
DateTimeFormatter chinaFormatter = DateTimeFormatter.ofPattern("yyyy MMMM d", Locale.CHINA);
String formattedDate = date.format(chinaFormatter);
System.out.println(formattedDate);
// 2021 一月 27

DateTimeFormatterBuilder 自定义时间格式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
LocalDate date = LocalDate.of(2021, 1, 27);
DateTimeFormatter complexFormatter = new DateTimeFormatterBuilder()
        .appendText(ChronoField.YEAR)
        .appendLiteral(" 年 ")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendLiteral(" ")
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral(" 日 ")
        .parseCaseInsensitive()
        .toFormatter(Locale.CHINA);
// 2021 年 一月 27 日        

12.3 处理不同的时区和历法

新版 java.time.ZoneId 类

  • 是老版 java.util.TimeZone 类的代替品
  • 设计目标是让你无需为处理时区操碎了心
  • ZoneId 类是不可变的

12.3.1 使用时区

1
2
// ZoneId.of("区域/城市")
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
  • 时区数据由 IANA 的时区数据库提供

将老的 TimeZone 转为 ZoneId

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

ZoneId 对象可以整合下列对象构造 ZonedDateTime 实例

  • LocalDate
  • LocalDateTime
  • Instant
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");

LocalDate date = LocalDate.of(2021, 1, 27);
ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone);
// 2021-01-27T00:00+08:00[Asia/Shanghai]
System.out.println(zdt1);

LocalDateTime dateTime = LocalDateTime.of(2021, 1, 27, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone);
// 2021-01-27T18:13:45+08:00[Asia/Shanghai]
System.out.println(zdt2);

Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(shanghaiZone);
// 2021-01-27T23:47:02.923793+08:00[Asia/Shanghai]
System.out.println(zdt3);

图 12-1 理解 ZonedDateTime 可以很好的帮助理解它们的关系(详见书中插图)

  • LocalDate : 年 月 日
  • LocalTime : 时 分 秒
  • ZoneId : 时区
  • LocalDateTime : LocalDate + LocalTime = 年 月 日 时 分 秒
  • ZonedDateTime : LocalDateTime + ZoneId = 年 月 日 时 分 秒 时区
comments powered by Disqus