一. 简介

Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:

  • 非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
  • 设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
  • 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。

Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:

  • Local(本地) − 简化了日期时间的处理,没有时区的问题。
  • Zoned(时区) − 通过制定的时区处理日期时间。

新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。

二. 时间戳 Instant和Clock

1. 时刻 Instant

在新的时间API中,Instant表示一个精确的时间点,Duration和Period表示两个时间点之间的时间量(所以我们比较两个时间差,用新API更方便了,后面会有示例)。
Instant它是精确到纳秒的(而不是象旧版本的Date精确到毫秒,System.nanoTime倒是是精确到纳秒级别了),如果使用纳秒去表示一个时间则原来使用一位Long类型是不够的,需要占用更多一点的存储空间,所以它内部是用两个字段去存储的。第一个部分保存的是自格林威治时间(就是1970年1月1日开始)到现在的秒数(用long存储),第二部分保存的是纳秒数(用int存储,永远不会超过999,999,999)

image.png

示例1: 获取当前时间的毫秒级时间戳

可以调用Instant.now()方法获取当前时间的纳秒级时间戳,然后通过toEpochMilli()转换成毫秒级时间戳

  1. Instant now = Instant.now();
  2. System.out.println(now.toEpochMilli());

示例2: 获取指定时间的毫秒级时间戳

Instant.parse()可以通过传入指定的日期字符串获取对应的时间戳

  1. Instant now = Instant.parse();
  2. System.out.println(now.toEpochMilli());

示例3: 时间戳的加减

Instant定义了一系列的plusXXX()方法用来对时间戳进行加减,正加负减,同样,还可以传入TemporalUnit(时间单位)接口的实现类枚举ChronoUnit来决定修改的属性是天,小时或者秒等
PS: 这里使用了一个java5提供的时间工具类TimeUnit,可以很方便地对时间粒度进行转换

  1. Instant now = Instant.now();
  2. Instant instant_1 = now.plusSeconds(TimeUnit.HOURS.toSeconds(25));
  3. Instant instant_2 = now.plusMillis(1);
  4. Instant instant_3 = now.plusNanos(1);
  5. Instant instant_4 = now.plus(1, ChronoUnit.DAYS);

示例5: 统计程序运行的时间

java8中的Duration类可以用来表示时间差,而Period则用来表示日期差,在后面会专门讲解

  1. Instant start = Instant.now();
  2. Thread.sleep(10);
  3. Instant end = Instant.now();
  4. //计算时间差 采用Duration来处理时间戳的差
  5. Duration timeElapsed = Duration.between(start, end);
  6. long millis = timeElapsed.toMillis();
  7. System.out.println("millis = " + millis);

2. 时钟 Clock(使用频率不高)

公共抽象类Clock 提供使用时区的当前时刻,日期和时间的访问,可以使用存储的时区来解释当前时刻以查找当前日期和时间。因此,可以使用时钟代替System.currentTimeMillis()TimeZone.getDefault()
必须小心实现此抽象类,以确保其他类正确运行,可以实例化的所有实现必须是final的,不可变的并且是线程安全的。
此处提供的时钟实现基于System.currentTimeMillis()。该方法几乎不能保证时钟的准确性。需要更精确时钟的应用程序必须使用其他外部时钟(例如NTP服务器)自己实现此抽象类。

示例1: 打印时区的日期和毫秒数

通过调用静态方法获取Clock对象,并且可以转换为时间戳或者毫秒数

  1. final Clock clock = Clock.systemUTC();
  2. System.out.println( clock.instant() );
  3. System.out.println( clock.millis() );

示例2: 打印指定的日期和毫秒数

通过传入ZoneId可以指定Clock的时区,并且Java的其他时间类支持传入Clock进行构造

  1. Clock shanghai = Clock.system(ZoneId.of("Asia/Shanghai")); // 上海时间
  2. System.out.println(LocalDateTime.now(shanghai));
  3. System.out.println(LocalDate.now(shanghai));
  4. System.out.println(LocalTime.now(shanghai));

三. 日期初识 LocalData

LocalData有三个主要属性year、month、day,分别是int、short、short类型,用来表示日期
image.png
API的设计者推荐使用不带时区的时间,除非真的希望表示绝对的时间点。
可以使用静态方法now()和of()创建LocalDate。

示例1: 获取当天日期

Java 8中的 LocalDate 用于表示当天日期。和java.util.Date不同,它只有日期,不包含时间。调用静态方法LocalDate.now()获取当天日期

  1. LocalDate date = LocalDate.now();
  2. System.out.println("当前日期=" + date);//2019-12-10

示例2: 构造指定日期

调用工厂方法LocalDate.of()创建任意日期, 该方法需要传入年、月、日做参数,返回对应的LocalDate实例。这个方法的好处是没再犯老API的设计错误,比如年度起始于1900,月份是从0开 始等等

  1. LocalDate date = LocalDate.of(2000, 1, 1);
  2. System.out.println("千禧年=" + date);

示例3: 获取年月日闰年等信息

可以通过get方法获取对应的年月日信息,如getYear(),getMonthValue(),getDayOfMonth()

  1. LocalDate date = LocalDate.now();
  2. System.out.printf("年=%d, 月=%d, 日=%d", date.getYear(), date.getMonthValue(), date.getDayOfMonth());

获取信息的方法列举如下

  • int getYear() :年份信息
  • int getMonthValue() :月份信息
  • Month getMonth() :Month对象,这是一个月份的枚举
  • int getDayOfMonth() : 相对于月份的天
  • int getDayOfYear() : 相对于年份的天
  • DayOfWeek getDayOfWeek() : DayOfWeek对象,这是一个星期一到星期天的枚举
  • boolean isLeapYear() : 是否是闰年
  • int lengthOfMonth() :月份天数
  • int lengthOfYear() :年份天数

示例4: 比较两个日期是否相等

LocalDate实现了equals方法,可以通过它判断日期是否相等

  1. LocalDate date1 = LocalDate.now();
  2. LocalDate date2 = LocalDate.now();
  3. System.out.println("日期是否相等=" + date1.equals(date2));

四. 时间初识 LocalTime

Java 8中的 LocalTime 用于表示当天时间。和java.util.Date不同,它只有时间,不包含日期。
LocalTime有四个主要属性hour、minute、second、nano,分别是byte、byte、byte、int类型,用来表示使劲按
image.png

示例1: 获取当前时间

同样,我们通过LocalTime**.**now()方法获取当前时间

  1. LocalTime time = LocalTime.now();
  2. System.out.println("当前时间=" + time);

示例2: 获取指定时间

调用工厂方法LocalTime**.of()**创建任意时间, 该方法需要传入时、分、秒、纳秒做参数,可以传入部分或全部

  1. LocalTime time_1 = LocalTime.of(1, 2);//hour minute
  2. LocalTime time_2 = LocalTime.of(1, 2, 30);//hour minute second
  3. LocalTime time_3 = LocalTime.of(1, 2, 30, 40);//hour minute second nonoOfSecond

实例3:获取时分秒等信息

同样,LocalTime也可以获取时、分、秒、纳秒的信息,并且通过equals判断是否相等

  1. LocalTime time_1 = LocalTime.of(1, 2, 30, 40);
  2. LocalTime time_2 = LocalTime.of(1, 2, 30, 40);
  3. System.out.printf("时=%d, 分=%d, 秒=%d, 纳秒=%d", time_1.getHour(), time_1.getMinute(), time_1.getMinute(), time_1.getNano());
  4. System.out.printf("\n 是否相等=" + time_1.equals(time_2));

实例4:获取时间常量

LocalTime中拥有一些方便的时间常量,省去我们定义的功夫,

  • 每天的小时数:HOURS_PER_DAY = 24
  • 每小时的分钟数:MINUTES_PER_HOUR = 60
  • 每天的分钟数:MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY
  • 每分钟的秒数:SECONDS_PER_MINUTE = 60
  • 每小时的秒数:SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR
  • 每天的分钟数:SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY
  • 每天的毫秒数:MILLIS_PER_DAY = SECONDS_PER_DAY * 1000L
  • 每天的微秒数:MICROS_PER_DAY = SECONDS_PER_DAY * 1000_000L
  • 每秒纳秒数:NANOS_PER_SECOND = 1000_000_000L
  • 每分钟纳秒数:NANOS_PER_MINUTE = NANOS_PER_SECOND * SECONDS_PER_MINUTE
  • 每小时纳秒数:NANOS_PER_HOUR = NANOS_PER_MINUTE * MINUTES_PER_HOUR
  • 每天的那描述:NANOS_PER_DAY = NANOS_PER_HOUR * HOURS_PER_DAY;

五、比较与计算

Java8提供了新的plusXxx()方法用于计算日期时间增量值,替代了原来的add()方法。新的API将返回一个全新的日期时间示例,需要使用新的对象进行接收。

示例1: 时间的加减

调用对应属性的plus方法来对其进行修改,正加负减

  1. LocalTime time = LocalTime.now();
  2. LocalTime time_2 = time.plusHours(2);
  3. LocalTime time_3 = time.plusMinutes(2);
  4. LocalTime time_4 = time.plusSeconds(2);
  5. LocalTime time_5 = time.plusNanos(2);
  6. LocalTime time_6 = time.plusNanos(-2);

同样,还可以传入TemporalUnit(时间单位)接口的实现类枚举ChronoUnit来决定修改的属性

  1. LocalTime time_6 = time.plus(1, ChronoUnit.HOURS);
  2. LocalTime time_7 = time.plus(1, ChronoUnit.SECONDS);

示例2: 日期的加减

LocalDate日期的加减同理

  1. LocalDate date = LocalDate.now();
  2. LocalDate date_2 = date.plusYears(1);
  3. LocalDate date_3 = date.plusMonths(1);
  4. LocalDate date_4 = date.plusWeeks(1);
  5. LocalDate date_5 = date.plusDays(1);
  6. LocalDate date_6 = date.plus(1, ChronoUnit.DAYS);
  7. LocalDate date_7 = date.plus(Period.of(2019, 12, 10));

示例3: 时间的比较

Java 8提供了isAfter()isBefore()用于判断当前日期时间和指定日期时间的比较LocalDate、LocalTime和LocalDateTime都有这个方法

  1. LocalDate now = LocalDate.now();
  2. LocalDate date1 = LocalDate.of(2000, 1, 1);
  3. if (now.isAfter(date1)) {
  4. System.out.println("千禧年已经过去了");
  5. }
  6. LocalDate date2 = LocalDate.of(2020, 1, 1);
  7. if (now.isBefore(date2)) {
  8. System.out.println("2020年还未到来");
  9. }

六、日期时间差 Period 和 Duration

java 8 中引入的两个与日期相关的新类:Period 和 Duration。两个类表示时间量或两个日期之间的差,两者之间的差异为:Period基于日期值,而Duration基于时间值。

1. Period(一段时期)

示例1: 获取两个日期的差

Period 类表示一段时间的年、月、日,使用between()方法获取两个日期之间的差作为Period 对象返回,通过get方法可以获取间隔的年、月、日;

  1. LocalDate startDate = LocalDate.of(2015, 2, 20);
  2. LocalDate endDate = LocalDate.of(2017, 1, 15);
  3. Period period = Period.between(startDate, endDate);
  4. System.out.printf("years:%d, months:%d, days:%d",period.getYears(), period.getMonths(), period.getDays());

示例2: 比较日期的前后关系

在这种情况下,任何一个属性为负值,则isNegative()方法返回true,因此可以用于判断startDate是否晚于endDate

  1. LocalDate startDate = LocalDate.of(2017, 1, 15);
  2. LocalDate endDate = LocalDate.of(2015, 2, 20);
  3. Period period = Period.between(startDate, endDate);
  4. if (period.isNegative()) {
  5. System.out.println("开始时间比结束时间晚");
  6. }

示例3: 创建指定的日期间隔

Period提供了一系列ofXXX()的静态方法用来构建自己

  1. Period fromUnits = Period.of(3, 10, 10);
  2. Period fromDays = Period.ofDays(50);
  3. Period fromMonths = Period.ofMonths(5);
  4. Period fromYears = Period.ofYears(10);
  5. Period fromWeeks = Period.ofWeeks(40);
  6. assertEquals(280, fromWeeks.getDays());

示例4: 文本参数转Period

我们也可以通过解析文本序列来创建Period,其格式为“PnYnMnD”,YMD分别表示为年月日

  1. Period fromCharYears = Period.parse("P2Y");
  2. assertEquals(2, fromCharYears.getYears());//2年
  3. Period fromCharUnits = Period.parse("P2Y3M5D");
  4. assertEquals(5, fromCharUnits.getDays());//2年3个月5天

示例5:Period的加减

Period的年月日可以通过plusX()minusX()方法进行增加或减少,其中X表示日期单元:

  1. assertEquals(56, period.plusDays(50).getDays());
  2. assertEquals(9, period.minusMonths(2).getMonths());

2. Duration(持续时间)

Duration类表示秒或纳秒时间间隔,适合处理较短的时间,需要更高的精确性,拥有long **secondsint **nanos两个属性。我们能使用between()方法比较两个瞬间的差,同时用get()方法获取秒和纳秒的值

示例1: 获取时间差

利用Duration.between()获取Instant的时间差

  1. Instant start = Instant.parse("2017-10-03T10:15:30.00Z");
  2. Instant end = Instant.parse("2017-10-03T10:16:30.00Z");
  3. Duration duration = Duration.between(start, end);
  4. System.out.printf("seconds:%d, nanos:%d", duration.getSeconds(), duration.getNano());

利用Duration.between()获取LocalDataTime或者LocalTime的时间差

  1. LocalTime start = LocalTime.of(1, 20, 25, 1024);
  2. LocalTime end = LocalTime.of(3, 22, 27, 1544);
  3. Duration.between(start, end).getSeconds();

示例2: 比较时间的前后关系

在这种情况下,任何一个属性为负值,则isNegative()方法返回true,因此可以用于判断startTime是否晚于endTime

  1. duration.isNegative()

示例3: 创建指定的日期间隔

Duration同样也提供了一系列ofXXX()的静态方法用来构建自己

  1. Duration duration1 = Duration.ofDays(1);
  2. Duration duration2 = Duration.ofHours(1);
  3. Duration duration3 = Duration.ofMinutes(1);
  4. Duration duration4 = Duration.ofSeconds(1);
  5. Duration duration5 = Duration.ofMillis(1);
  6. Duration duration6 = Duration.ofNanos(1);

示例4: 文本参数转Duration

我们也可以通过解析文本序列来创建Duration,其格式为“PnDTnHnMn.nS”,DHMS分别表示为日时分秒,P和T是固定格式,T在最后一位是要省略

  1. Duration fromChar1 = Duration.parse("P1DT1H10M10.5S");
  2. Duration fromChar2 = Duration.parse("PT10M");
  3. Duration fromChar3 = Duration.parse("-P2D");

七. 时区 ZoneDateTime

ZoneDateTime用来表示带有时区的时间,同样有三个主要属性:

  • LocalDateTime **dateTime**:聚合了LocalDate和LocalTime,用来表时完整的日期和时间
    • LocalDate
    • LocalTime
  • ZoneOffset **offse**t:表示时区的偏移量
    • totalSeconds
    • id
  • ZoneRegion **zone**:表示时区属性
    • id
    • ZoneRules rules

image.png

示例: 创建带有时区的日期时间

Java 8不仅分离了日期和时间,也把时区分离出来了。现在有一系列单独的类如ZoneId来处理特定时区,ZoneDateTime类来表示某时区下的时间。

  1. // 上海时间
  2. ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
  3. ZonedDateTime shanghaiZonedDateTime = ZonedDateTime.now(shanghaiZoneId);
  4. // 东京时间
  5. ZoneId tokyoZoneId = ZoneId.of("Asia/Tokyo");
  6. ZonedDateTime tokyoZonedDateTime = ZonedDateTime.now(tokyoZoneId);
  7. DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  8. System.out.println("上海时间: " + shanghaiZonedDateTime.format(formatter));
  9. System.out.println("东京时间: " + tokyoZonedDateTime.format(formatter));

八. 格式化

java8 提供了DateTimeFormatter来格式化时间

示例1: 使用预定义格式解析与格式化日期

LocalDate和LocalDateTime都拥有parse()方法,可以依据DateTimeFormatter内预设的日期格式对传入的日期字符串进行解析

  1. // 解析日期
  2. String dateText = "20180924";
  3. LocalDate date = LocalDate.parse(dateText, DateTimeFormatter.BASIC_ISO_DATE);
  4. System.out.println("格式化之后的日期=" + date);
  5. // 格式化日期
  6. dateText = date.format(DateTimeFormatter.ISO_DATE);
  7. System.out.println("dateText=" + dateText);

示例2: 日期和字符串的相互转换

可以通过DateTimeFormatter**.ofPattern()**方法,传入一个日期格式来初始化DateTimeFormatter,进行转换

  1. DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  2. // 日期时间转字符串
  3. LocalDateTime now = LocalDateTime.now();
  4. String nowText = now.format(formatter);
  5. System.out.println("nowText=" + nowText);
  6. // 字符串转日期时间
  7. String datetimeText = "1999-12-31 23:59:59";
  8. LocalDateTime datetime = LocalDateTime.parse(datetimeText, formatter);
  9. System.out.println(datetime);

九、相关类说明

  1. Instant 时间戳
  2. Duration 持续时间、时间差
  3. LocalDate 只包含日期,比如:2018-09-24
  4. LocalTime 只包含时间,比如:10:32:10
  5. LocalDateTime 包含日期和时间,比如:2018-09-24 10:32:10
  6. Peroid 时间段
  7. ZoneOffset 时区偏移量,比如:+8:00
  8. ZonedDateTime 带时区的日期时间
  9. Clock 时钟,可用于获取当前时间戳
  10. java.time.format.DateTimeFormatter 时间格式化类