Java8全新日期、时间API在这全明白了

开发 前端
在Java1.0中,对日期和时间的支持只能依赖java.util.Date类。这个类无法表示日期,只能以毫秒的精度表示时间。而且设计不合理,比如:年份的起始选择是1900年,月份的起始从0开始。

时间对生活来说非常重要,Java也为我们提供了时间的API,多数程序员都在吐槽Java8之前的日期和时间,在Java8中引入全新的日期和时间API,目前我们项目中都在使用这一新的API。之前的API到底不好在哪里?Java8的时间API到底是在吹还是真的不错?在这篇文章中都有答案!

接下来会先介绍之前API的弊病,然后转而介绍新API的升级和具体应用,老样子文章写的非常用心,都喜欢长的,建议收藏反复观看,对于日期和时间的操作都在这了!

Java老API的缺点主要为以下几点:

  • 日期和时间的计算方式不合理
  • 日期和时间的输出格式不合理
  • 时间格式化工具:DateFormat线程不安全
  • 类库包设计杂乱

第一代时间类库

java.util.Date

在Java1.0中,对日期和时间的支持只能依赖java.util.Date类。这个类无法表示日期,只能以毫秒的精度表示时间。而且设计不合理,比如:年份的起始选择是1900年,月份的起始从0开始。如果你想表示女朋友的生日,即2022年12月10日,需要通过以下方式:

/*
参数1:年份,其中 122就是 2022 - 1900
参数2:月份,从0开始,即1月就是0月,12月就要写成 11
*/
Date date = new Date(122,11,10);
// 输出,会默认调用 toString()
System.out.println(date);

输出结果为:

Sat Dec 10 00:00:00 CST 2022

其中,Sat:是 Saturday,周六的缩写,Dec:是 December,12月的缩写,手把手解释,就是这么细,10就不解释了吧,都知道是10号的意思!

总之,看起来非常别扭,它的返回值中的 CST 其实是JVM的默认时区,即美国、澳大利亚、古巴或中国的标准时间,具体是哪一个我也不清楚,毕竟这几个时区都可以简写为 CST,如果能识别出来当前程序在中国运行的,那么可以理解为代表了中国标准时间

  • 美国中部时间 Central Standard Time (USA)
  • 中国标准时间 China Standard Time UT+8:00,中国在东八区时间要快8小时,需要 + 8:00
  • 古巴标准时间 Cuba Standard Time UT-4:00

java.util.Date源码:

通过源码就会明白几个为什么:

  • 为什么年份要减去1900
  • 为什么月份使用0开始的
  • 为什么输出的时间会有 00:00:00 的时间

java.sql.Date

在java.sql包中也有一个Date类,通过源码可以看出,其实java.sql.Date是继承了java.util.Date

类上边的注解这么写道:

/**
* <P>A thin wrapper around a millisecond value that allows
* JDBC to identify this as an SQL <code>DATE</code> value. A
* milliseconds value represents the number of milliseconds that
* have passed since January 1, 1970 00:00:00.000 GMT.
* <p>
* To conform with the definition of SQL <code>DATE</code>, the
* millisecond values wrapped by a <code>java.sql.Date</code> instance
* must be 'normalized' by setting the
* hours, minutes, seconds, and milliseconds to zero in the particular
* time zone with which the instance is associated.
*/

翻译:

一个围绕毫秒值的精简包装器,允许JDBC将其标识为SQL DATE值。毫秒值表示自1970年1月1日00:00:00.000 GMT以来经过的毫秒数。

为了符合SQL DATE的定义,毫秒值由java.sql.Date包装。必须通过在与实例关联的特定时区中将小时、分钟、秒和毫秒设置为零来“规范化”日期实例

通过上边的翻译可以看出,Java想用java.sql.Date类来映射SQL中的时间戳,非常理想化,我们一般都需要时分秒,这个类直接将时分秒设置为0啦,基本用不了

总结:

  • 使用Date类时需要对年份 减1900,月份 减1 来初始化一个Date对象,不合常理
  • 时间的输出格式对于人类来说难以阅读,竟然包含时区,而且顺序也比较混乱
  • 无论是使用还是阅读都不合理

第二代时间类库

Java 1.1中,Date类中的很多方法被废弃了,取而代之的是java.util.Calendar类,Calendar类也有类似的问题和设计缺陷,导致编码时非常容易出错。比如,月份依旧是从0开始计算(不过,至少Calendar类拿掉了由1900年开始计算年份这一设计)。同时存在Date和Calendar这两个类,出现了选择困难症。有的特性只在某一个类有提供,比如格式化和解析日期或时间的DateFormat方法就只在Date类里有,那我用 Calendar 干什么,这就出现了 Calendar 使用很少很少的现象,我觉得是一个失败的升级,小伙伴们在编程或者阅读别人代码的时候,见过 Calendar 吗?

最后,Date和Calendar类设计了拥有set方法都是可变的,能把原本的时间改为任意一个时间意味着什么呢?意味着你程序中从数据库读取到的时间可以被修改掉,比如交易日期本来是2022年12月8日,一个set方法就可以改成其他时间,在维护时绝对是灾难,交易时间等应该是不可变的才对,并没有内鬼!

// 获取日历类,获取当前时间
Calendar calendar = Calendar.getInstance();
// 转换为Date类
Date time = calendar.getTime();
// 输出 格式化后的 时间
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(dateFormat.format(time));

// 变更时间,由当前时间变更为 2025年12月12日
calendar.set(2025,12,12);
Date time2 = calendar.getTime();
System.out.println("我改时间了:");
System.out.println(dateFormat.format(time2));

运行结果:

时间格式化

DateFormat是时间格式化的抽象类,我们经常使用它的子类SimpleDateFormat来格式化和解析日期与时间,但是也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果尝试使用同一个formatter解析日期,你可能会得到无法预期的结果。而且格式化和解析时间的类在 java.text 包下,这是想害死强迫症,劝退处女座。

DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
// 开5个线程,方便截图
for (int i = 0; i < 5; i++) {
// Lambda表达式创建线程
new Thread(() -> {
try {
// 将字符串时间转换为Date日期对象
Date date = dateFormat.parse("2022-12-10");
System.out.println(Thread.currentThread().getName() + "====>" + date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
}

运行结果:

解决线程安全:

解决线程安全问题,首先想到的就是加锁

// 创建时间格式化对象
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
// 开5个线程,方便截图
for (int i = 0; i < 5; i++) {
// Lambda表达式创建线程
new Thread(() -> {
try {
// 选择一个唯一的锁对象,dateFormat拿来就用呗
synchronized (dateFormat) {
Date date = dateFormat.parse("2022-12-10");
System.out.println(Thread.currentThread().getName() + "====>" + date);
}
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
}

或者通过 JUC 中,也就是Java5新增的Lock对象加锁,这里锁不是重点

DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
// 创建 可重入锁,一定要创建在循环外边,可以防止死锁
Lock lock = new ReentrantLock();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
// 上锁
lock.lock();
Date date = dateFormat.parse("2022-12-10");
System.out.println(Thread.currentThread().getName() + "====>" + date);
// 释放锁
lock.unlock();
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
}

运行结果:

总结:

  • 早期的日期时间类库设计不统一,分布在不同的包中
  • 日期设计不合理,Date中的年从1900年算起,包括 Calendar 中的月份从0开始
  • Date和Calendar 的时间可变,会造成很大的安全隐患
  • DateFormat线程不安全,多线程场景需要手动加锁解决

第三代时间类库

所有这些缺陷和不一致导致用户转投第三方的日期和时间库,比如Joda-Time。Oracle觉得面子挂不住,决定在原生的Java API中提供高质量的日期和时间支持。所以,在Java 8在java.time包中整合了很多Joda-Time的特性。这一章中,我们会一起探索新的日期和时间API所提供的新特性。首先看一下java.time包中都包含哪些东西:

开发中常用的看下边脑图:

将从以下几点系统学习新日期API:

  • 基本的用例,比如创建简单的日期和时间
  • 高级操作,日期和时间的操纵、解析、打印输出
  • 使用不同的时区和年历

小贴士:这些API中的方法比较多,就不贴源码了,演示最常用的方法,感兴趣的可以自己打开IDE练习方法和翻阅源码

LocalDate

作用:该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。它也不附带任何与时区相关的信息

创建:通过静态工厂方法of创建一个LocalDate实例,提供了多种方法来读取常用的值,比如年份、月份、星期几等

代码实现:

// 通过 of方法指定年月日
// 注意:年取值范围是,-999999999到999999999,彰显格局,月:1-12,日期:1-31
LocalDate date = LocalDate.of(2022, 12, 31);
// 获取年:2022
int year = date.getYear();
// 获取月份,返回Month对象,因为还可以基于月份做很多其他的判断,所以返回的并不是一个数字
// 直接打印month的话输出的是:DECEMBER,12月,也就是当前对象的英文表示
Month month = date.getMonth();
// 调用getValue返回的是数字月份:12
int monthValue = month.getValue();
// 获取是几号:31
int dayOfMonth = date.getDayOfMonth();
// 获取是周几,返回值与月份同理:SATURDAY
DayOfWeek dayOfWeek = date.getDayOfWeek();
// 调用getValue返回数字:6,因为31号是周六
int dayOfWeekValue = dayOfWeek.getValue();
// 获取是年中的第几天:365
int dayOfYear = date.getDayOfYear();
// 判读是否为闰年:false
boolean leapYear = date.isLeapYear();

System.out.println("year==>" +year);
System.out.println("mnotallow==>" +month);
System.out.println("mnotallow==>" +monthValue);
System.out.println("dayOfMnotallow==>" +dayOfMonth);
System.out.println("dayOfWeek==>" +dayOfWeek);
System.out.println("dayOfWeekValue==>" +dayOfWeekValue);
System.out.println("dayOfYear==>" +dayOfYear);
System.out.println("leapYear==>" +leapYear);

运行结果:

直接获取当前日期:

使用工厂方法从系统时钟中获取当前的日期:

LocalDate date = LocalDate.now();

TemporalField读取值:

LocalDate提供了get方法通过传入TemporalField接口获取值,它是接口无法直接使用,可以使用实现该接口的ChronoField枚举类实现

// 获取当前日期
LocalDate date = LocalDate.now();
// 获取年
int year = date.get(ChronoField.YEAR);
// 获取 月份
int month = date.get(ChronoField.MONTH_OF_YEAR);
// 获取 几号
int dayOfMonth = date.get(ChronoField.DAY_OF_MONTH);
// 获取 今天是今年第几天
int dayOfYear = date.get(ChronoField.DAY_OF_YEAR);
// 获取 星期几
int dayOfWeek = date.get(ChronoField.DAY_OF_WEEK);

这样获取月份和星期就不需要转换了

LocalTime

LocalTime可以用来表示一天内的时间,比如 13:14:52 ,可以通过工厂方法of实现LocalTime的创建,可以指定,时,分,秒,纳秒

取值范围合理:

  • 小时:0-23
  • 分钟:0-59
  • 秒钟:0-59
  • 纳秒:0-999999999

代码实现:

// 设置指定时间
LocalTime time = LocalTime.of(13, 14, 52);
// 时钟:13
int hour = time.getHour();
// 分钟:14
int minute = time.getMinute();
// 秒钟:52
int second = time.getSecond();
// 纳秒:0
int nano = time.getNano();
// 也通过get根据 ChronoField枚举值获取数据
int hourOfDay = time.get(ChronoField.HOUR_OF_DAY);

// 通过 now() 获取当前时间
LocalTime now = LocalTime.now();

字符串创建:

可以调用LocalDate 和LocalTime 的parse方法根据字符串解析为日期和时间对象

LocalDate localDate = LocalDate.parse("2022-12-31");
LocalTime localTime = LocalTime.parse("13:14:52");

小贴士:如果你传递的日期和时间是错误的运行时则会报错,比如2022年不是闰年所以2月只有28号,请观察下方动图,其中鼠标选中的leap year是闰年的意思,报错信息提示2022年不是闰年

抛出的是 DateTimeParseException 继承自 RuntimeException

LocalDateTime

是对LocalDate和LocalTime的合并,同时表示了日期和时间,但不带有时区信息,可以直接创建,也可以通过合并日期和时间对象构造

// 创建日期
LocalDate date = LocalDate.now();
// 创建时间
LocalTime time = LocalTime.now();
// 1、合并日期和时间
LocalDateTime dateTime1 = LocalDateTime.of(date, time);
// 2、通过年月日时分秒创建
LocalDateTime dateTime2 = LocalDateTime.of(2022, 12, 31, 13, 14, 52);
// 3、创建当前日期和时间
LocalDateTime now = LocalDateTime.now();

LocalDate的atTime方法:

支持传入LocalTime对象,一起合并返回一个LocalDateTime对象

// 创建日期
LocalDate date = LocalDate.now();
// 创建时间
LocalTime time = LocalTime.now();
// 通过atTime方法
LocalDateTime localDateTime = date.atTime(time);

LocalTime的atDate方法:

LocalTime就只有一个 atDate方法接收LocalDate对象,不支持传入年月日,一起返回LocalDateTime对象

// 创建日期
LocalDate date = LocalDate.now();
// 创建时间
LocalTime time = LocalTime.now();

// 通过atDate方法
LocalDateTime localDateTime = time.atDate(date);

LocalDateTime的toLocalDate和toLocalTime:

可以将日期时间对象拆解出 LocalDate和LocalTime对象

// 创建日期时间
LocalDateTime localDateTime = LocalDateTime.now();
// 获取日期对象
LocalDate localDate = localDateTime.toLocalDate();
// 获取时间对象
LocalTime localTime = localDateTime.toLocalTime();

Instant

Instant 类 是Java8 中补充的一个 时间戳类。相较于 System.currentTimeMillis()获取到【毫秒】,Instant 可以更为精确的获取到【纳秒】。该类是基于计算机角度描述时间,以Unix元年时间,也就是1970年1月1日0点开始所经历的秒数

// 获取当前时间的Instant对象
Instant instant1 = Instant.now();

System.out.println(instant1);
System.out.println("纪元秒 : "+instant1.getEpochSecond());
System.out.println("时间戳 : "+System.currentTimeMillis());
System.out.println("毫 秒 : "+instant1.toEpochMilli());
System.out.println("纳 秒 : "+instant1.getNano());
System.out.println("=============指定时间==============");
// 2.获取指定时间的Instant对象
Instant instant2 = Instant.ofEpochSecond(100);
System.out.println(instant2);
System.out.println("纪元秒 : "+instant2.getEpochSecond());
System.out.println("毫 秒 : "+instant2.toEpochMilli());
System.out.println("纳 秒 : "+instant2.getNano());
System.out.println("=============指定时间,带时区==============");
//3.指定时间戳创建 带时区的日期时间对象 ZoneDateTime
Instant instant3= Instant.ofEpochSecond(1670679915);// 2022-12-10 21:45:15
ZonedDateTime zonedDateTime = instant3.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime);
System.out.println("=============指定时间,默认时区==============");

// 4.指定时间戳创建 默认时区的日期时间对象 LocalDateTime
Instant instant4 = Instant.ofEpochSecond(1670679915); // 2022-12-10 21:45:15
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant4, ZoneId.systemDefault());
System.out.println(localDateTime);

运行结果:

Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。比如我们想要和LocalDate那样获取获取当前星期的话:

// 通过 ChronoField 枚举获取日期
int dayOfWeek = Instant.now().get(ChronoField.DAY_OF_WEEK);
System.out.println(dayOfWeek);

就会出现UnsupportedTemporalTypeException 异常

Period 和 Duration

作用:

  • Period:计算两个“日期”间隔的类
  • Duration:计算两个“时间”间隔的类

Period 类与 Duration 类都是一段持续时间的概念,如果需要对比时间,它们就需要一个固定的时间值,所以就需要 LocalDate 、LocalDateTime、LocalTime、Instant 、类来配合它们使用

Period 对应使用 LocalDate ,它们的作用范围域都是日期(年/月/日),Duration 对应使用 Instant、LocalTime、LocalDateTime,它们的作用范围域都是时间(天/时/分/秒/毫秒/纳秒)

Period:

Period可以用于计算两个日期之间的间隔,但是得到的是年月日,如两个日期相差1年2月3天,但是没办法知道1年2月3天具体是多少天,可以通过ChronoUnit.between()方法计算两个单元相差的天数、月数、年数…这个后便会说到

Period默认输出格式为PnYnMnD,如P2022Y12M31D 以P前缀开头,Y:年 M:月份 D:天 当Period为0时,默认为:P0D

常用API:

方法

说明

static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)

计算两个日期之间的间隔

boolean isNegative()

检查此时间段的三个单位中是否有一个为负数。这将检查年,月或天的单位是否小于零。如果此期间的任何单位为负,则为true

int getYears()

获取年

int getMonths()

获取月

int getDays()

获取日

构建Period:

// 1、传入年月日创建Period对象
Period p = Period.of(2022, 12, 31);
System.out.println("年月日:"+p);
// 2、传入年构造
p = Period.ofYears(2022);
System.out.println("年:"+p);
// 3、传入月份构造
p = Period.ofMonths(11);
System.out.println("月:" + p);
// 4、传入周构建,1周为7天
p = Period.ofWeeks(1);
System.out.println("周的天数:"+p);
// 5、传入天数构建
p = Period.ofDays(12);
System.out.println("天数:" + p);
// 6、传入负数日期
Period period = Period.of(2022,-12,6);
// 判断日期中是否包含负数,有返回true
boolean negative = period.isNegative();
System.out.println("日期是否包含负数:" + negative);

运行结果:

获取年月日:

  • getYears
  • getMonths
  • getDays
// 1、传入年月日创建Period对象
Period p = Period.of(2022, 12, 31);
int years = p.getYears();
int months = p.getMonths();
int days = p.getDays();
System.out.println("年:" + years);
System.out.println("月:" + months);
System.out.println("日:" + days);

// 如果通过 ofXXX创建,则其他值为0
Period p2 = Period.ofMonths(12);
// 年份为:0
System.out.println(p2.getYears());
// 日期为:0
System.out.println(p2.getDays());

获取两个时间差:

// 创建两个日期
LocalDate date1 = LocalDate.of(2021,11,11);
LocalDate date2 = LocalDate.of(2022,12,12);

// 获取相差多久
Period period = Period.between(date1, date2);
// 输出的默认格式为 P1Y1M1D---》P1年1月1天
System.out.println(period);

// 通过get方法分别获取年月日
System.out.println("相差:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "日");

运行结果:

计算相差具体天数:

方法一:通过ChronoUnit也可以计算两个日期之间的天数、月数或年数

// 创建两个日期
LocalDate date1 = LocalDate.of(2021,11,11);
LocalDate date2 = LocalDate.of(2022,12,12);

// 获取相差几年 结果:1
long years = ChronoUnit.YEARS.between(date1, date2);
System.out.println(years);

// 获取相差几月 结果:13
long months = ChronoUnit.MONTHS.between(date1, date2);
System.out.println(months);

// 获取相差几天 结果:396
long days = ChronoUnit.DAYS.between(date1, date2);
System.out.println(days);

方法二:调用LocalDate类的toEpochDay方法,返回距离1970年1月1日的long值,此方法只能计算两个LocalDate日期间的天数,不能计算月份、年数

// 创建两个日期
LocalDate date1 = LocalDate.of(2021,11,11);
LocalDate date2 = LocalDate.of(2022,12,12);
// 通过toEpochDay将时间转换为距离1970年1月1日0时的时间,相减获取相差天数
long days = date2.toEpochDay() - date1.toEpochDay();
// 结果:396
System.out.println(days);

Duration:

Duration 表示一个时间段,Duration 包含:seconds 表示秒,nanos 表示纳秒, 不包含毫秒,它们的组合表达了时间长度。因为 Duration 表示时间段,并不存在now()这样的静态方法。

Duration只能处理时间,例如LocalTime, LocalDateTime, ZonedDateTime; 如果传入的是LocalDate这种描述日期的类,将会抛出异常

Duration默认输出格式为PTnHnMnS,如PT8H6M12.345S 以PT前缀开头,H:小时 M:分钟 S:秒 当Duration为0时,默认为:PT0S

常用API:

方法

说明

static Duration between(Temporal startInclusive, Temporal endExclusive)

计算两个时间的间隔,默认是秒

boolean isNegative()

检查Duration实例是否小于0,若小于0返回true, 若大于等于0返回false

long toDays()

将时间转换为以天为单位的long值

long toHours()

将时间转换为以时为单位的long值

long toMinutes()

将时间转换为以分钟为单位的long值

long toSeconds()

将时间转换为以秒为单位的long值

long toMillis()

将时间转换为以毫秒为单位的long值

long toNanos()

将时间转换为以纳秒为单位的long值

简单API调用:

API和Period基本相似,两者区别在于Period计算日期差,Duration计算时间差

// 创建两个时间
LocalDateTime start = LocalDateTime.of(2021,11,11,00,00,00);
LocalDateTime end = LocalDateTime.of(2022,12,12,12,12,12);

// between的用法是end-start的时间,若start的时间大于end的时间,则所有的值是负的
Duration duration = Duration.between(start, end);
// PT9516H12M12S-->9516小时12分12秒
System.out.println(duration);

System.out.println("相差的天数="+duration.toDays());
System.out.println("相差的小时="+ duration.toHours());
System.out.println("相差的分钟="+duration.toMinutes());
// 获取秒,在JDK9之上才可调用,JDK8中为私有方法
System.out.println("相差的秒数="+duration.toSeconds());
// JDK8可以调用 getSeconds获取秒
System.out.println("相差的秒数="+duration.getSeconds());
System.out.println("相差的毫秒="+duration.toMillis());
System.out.println("相差的纳秒="+duration.toNanos());

//isNegative返回Duration实例对象是否为负
//false end-start为正,所以此处返回false
System.out.println(Duration.between(start, end).isNegative());
//true start-end为负,所以此处返回true
System.out.println(Duration.between(end, start).isNegative());
//false start-start为0,所以此处为false
System.out.println(Duration.between(start, start).isNegative());

计算时间间隔:

方法一:通过Duration计算两个LocalTime相差的时间

LocalTime start = LocalTime.of(11,11,10);
LocalTime end = LocalTime.of(12,12,30);

Duration duration = Duration.between(start, end);
// 结果:两个时间相差:3680秒,相差:1小时,相差:61分钟
System.out.println("两个时间相差:"+duration.getSeconds()+"秒,相差:"+duration.toHours()+"小时,相差:"+duration.toMinutes()+"分钟");

方法二:与Period相似,通过ChronoUnit类的between() 方法来执行相同的操作

LocalTime start = LocalTime.of(11,11,10);
LocalTime end = LocalTime.of(12,12,30);
// 计算小时: 1
long hour = ChronoUnit.HOURS.between(start , end );
// 计算分钟:61
long minute = ChronoUnit.MINUTES.between(start , end );
// 计算秒: 3680
long seconds = ChronoUnit.SECONDS.between(start , end );

方法三:通过LocalTime类的toSecondOfDay()方法,返回时间对应的秒数,然后计算出两个时间相差的间隔

LocalTime start = LocalTime.of(11,11,10);
LocalTime end = LocalTime.of(12,12,30);
// 结果:3680
int time = end.toSecondOfDay() - start.toSecondOfDay();

计算两个时间戳的间隔:

// 获取当前时间
long todayTimeMillis = System.currentTimeMillis();
// 设置昨天时间戳【方便看结果】,可以是任意两个时间戳
long yesterdayTimeMillis = todayTimeMillis - 24 * 60 * 60 * 1000;
//通过Instant类,可以直接将毫秒值转换为Instant对象
Instant yesterday = Instant.ofEpochMilli(yesterdayTimeMillis);
Instant today = Instant.ofEpochMilli(todayTimeMillis);

Duration duration = Duration.between(yesterday, today);
//天数 = 1
System.out.println("天数 = "+duration.toDays());

总结:

  • Period用来计算两个日期之间的间隔,Duration用来计算两个时间之间的间隔
  • 两者的结果默认有自己的特殊格式,可以通过getXX或者toXX方法获取相应的间隔单位
  • 可以配合 ChronoUnit 来单独计算指定单位的日期差和时间差

至此,已经为大家讲解了基础的日期、时间、时间戳、两个日期、时间的间隔,累的话可以喝口茶休息一下,接下来讲解操作、格式化、解析日期

操作日期

注意:LocalDate是不可变的,计算后都会返回一个新的LocalDate对象

增加日期

增加日期API:

方法

作用

plus(TemporalAmount amountToAdd)

通过添加指定的TemporalAmount返回LocalDate实例

plus(long amountToAdd, TemporalUnit unit)

通过增加给定的数量返回LocalDate实例。

plusYears(long yearsToAdd)

通过添加指定的年数返回LocalDate实例。

plusMonths(long monthsToAdd)

通过添加指定的月数返回LocalDate实例。

plusWeeks(long weeksToAdd)

通过添加指定的星期数返回LocalDate实例。

plusDays(long daysToAdd)

通过添加指定的天数返回LocalDate实例。

代码实现:

LocalDate localDate1 = LocalDate.now();
// 1、添加15天
LocalDate localDate2 = localDate1.plus(15, ChronoUnit.DAYS);
// 2、添加15天
LocalDate localDate3 = localDate1.plus(Period.ofDays(15));
// 3、增加15天
LocalDate localDate4 = localDate1.plusDays(15);
// 4、增加15周
LocalDate localDate5 = localDate1.plusWeeks(15);
// 5、增加15月
LocalDate localDate6 = localDate1.plusMonths(15);
// 6、增加15年
LocalDate localDate7 = localDate1.plusYears(15);

减去日期

用法和plus基本相同,区别在于方法切换为minus

减去日期API:

方法

作用

minus(TemporalAmount amountToSubtract)

通过减去指定的TemporalAmount返回LocalDate实例

minus(long amountToSubtract, TemporalUnit unit)

通过减去给定的数量返回LocalDate实例。

minusYears(long yearsToSubtract)

通过减去指定的年数返回LocalDate实例。

minusMonths(long monthsToSubtract)

通过减去指定的月数返回LocalDate实例。

minusWeeks(long weeksToSubtract)

通过减去指定的星期数返回LocalDate实例。

minusDays(long daysToSubtract)

通过减去指定的天数返回LocalDate实例。

代码实现:

// 获取当前日期
LocalDate localDate1 = LocalDate.now();
// 1、减去 15
LocalDate localDate2 = localDate1.minus(15, ChronoUnit.DAYS);
// 2、减去15天
LocalDate localDate3 = localDate1.minus(Period.ofDays(15));
// 3、减去15天
LocalDate localDate4 = localDate1.minusDays(15);
// 4、减去15周
LocalDate localDate5 = localDate1.minusWeeks(15);
// 5、减去15月
LocalDate localDate6 = localDate1.minusMonths(15);
// 6、减去15年
LocalDate localDate7 = localDate1.minusYears(15);

调整日期

修改原LocalDate对象的年、月、日、周等数据,并返回一个新的LocalDate对象,这写方法都是with开头的

调整日期API:

方法

作用

with(TemporalAdjuster adjuster)

返回用给定的TemporalAdjuster调整的LocalDate实例

with(TemporalField field, long newValue)

将指定字段的LocalDate实例返回到一个新值

withYear(int year)

通过用给定值改变年份来返回LocalDate实例

withMonth(int month)

通过用给定的值改变年的月份来返回LocalDate实例。有效值是到12

withDayOfMonth(int dayOfMonth)

通过用给定的值改变月份中的号数来返回LocalDate实例

withDayOfYear(int dayOfYear)

通过使用给定值更改一年中的某一天来返回 LocalDate 实例。一年中的第几天的有效值为1到 365,闰年的有效值为 1到 366

代码实现:

// 获取当前日期
LocalDate localDate1 = LocalDate.now();
System.out.println(localDate1);
// 2、设置为周二
LocalDate localDate2 = localDate1.with(DayOfWeek.THURSDAY);
System.out.println(localDate2);
// 3、设置到2050年
LocalDate localDate3 = localDate1.with(ChronoField.YEAR, 2050);
System.out.println(localDate3);
// 4、设置到10号
LocalDate localDate4 = localDate1.withDayOfMonth(10);
// 5、设置到一年的第364天:有效值为 1365,闰年的有效值为 1366
LocalDate localDate5 = localDate1.withDayOfYear(364);
// 6、设置到10月份: 有效值是1到12
LocalDate localDate6 = localDate1.withMonth(10);
// 7、设置到2050年
LocalDate localDate7 = localDate1.withYear(2050);

比较日期

方法

作用

isAfter(ChronoLocalDate other)

检查此日期是否在给定日期之后

isBefore(ChronoLocalDate other)

检查此日期是否在给定日期之前

isEqual(ChronoLocalDate other)

检查此日期是否等于给定日期

compareTo(ChronoLocalDate other)

将此日期与指定日期进行比较

equals(Object obj)

检查此日期是否等于指定日期

isLeapYear()

判断是否为闰年

isSupported(TemporalField field)

检查是否支持给定字段,在从日期获取任何字段之前,可以检查该字段是否受支持,否则可能会出错

isSupported(TemporalUnit unit)

检查是否支持给定的单位。在使用加减之前,可以检查是否支持给定的单位,否则可能会出错

lengthOfMonth()

给出月份的最大天数,例如 28、29、30、31

lengthOfYear()

给出年份的最大天数 365 或 366(闰年)

代码实现:

LocalDate localDate1 = LocalDate.parse("2021-02-14");
LocalDate localDate2 = LocalDate.parse("2022-05-20");
// false
System.out.println(localDate1.isAfter(localDate2));
// true
System.out.println(localDate1.isBefore(localDate2));
// false
System.out.println(localDate1.isEqual(localDate2));
System.out.println("================判断==============");
// false
System.out.println(localDate1.isLeapYear());
// true
System.out.println(localDate1.isSupported(ChronoField.DAY_OF_MONTH));
// false,不支持小时字段
System.out.println(localDate1.isSupported(ChronoUnit.HOURS));

System.out.println("=================比较==========================");
// false
System.out.println(localDate1.equals(localDate2));
// 1:大于,0:等于,-1:小于
System.out.println(localDate1.compareTo(localDate2));
System.out.println("=================最大值==========================");
// 28
System.out.println(localDate1.lengthOfMonth());
// 365
System.out.println(localDate1.lengthOfYear());

小贴士:LocalTime和LocalDateTime都有这些操作日期的API,大家可以放心食用

格式化时间

Java 8 的 java.time.format 包中提供了 DateTimeFormatter 和 DateTimeFormatterBuilder 来以不同的方式格式化日期、时间或两者

DateTimeFormatter:具有可直接用于解析字符序列的内置格式。

DateTimeFormatterBuilder:提供自定义方式来创建格式化器

格式化LocalDate:

LocalDate localDate = LocalDate.now();
// 通过 LocalDate 的 format方法根据传入的 DateTimeFormatter类中的常量【也就是时间格式】进行格式化
String format1 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
String format2 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
// 2022-12-11
System.out.println(format1);
// 20221211
System.out.println(format2);

也可以通过工厂方法解析字符串来创建 LocalDate

LocalDate localDate = LocalDate.parse("20220520",DateTimeFormatter.BASIC_ISO_DATE);
// 2022-05-20
System.out.println(localDate);

老API中的解析时间多线程不安全还记得吗?DateTimeFormatter 实例都是线程安全的

DateTimeFormatterBuilder: 提供了更复杂的格式器,可以自定义格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,所有的格式化器都是用DateTimeFormatterBuilder创建的,可以通过appendValue、appendLiteral和appendText等,用于生成一个格式化器

// 创建对象
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
// 设置格式化
DateTimeFormatter formatter = builder.appendLiteral("今天是:")
.appendValue(ChronoField.YEAR)
.appendLiteral("年,")
.appendValue(ChronoField.MONTH_OF_YEAR)
.appendLiteral("月,")
.appendValue(ChronoField.DAY_OF_MONTH)
.appendLiteral("日,周")
.appendValue(ChronoField.DAY_OF_WEEK)
.toFormatter();
LocalDateTime dateTime = LocalDateTime.now();
// 使用格式化对象
String str = dateTime.format(formatter);
// 今天是:2022,12,11,周7
System.out.println(str);

操作时区

之前的日期和时间都不包含时区信息。时区的处理是新版日期和时间API新增的重要功能,使用新版日期和时间API时区的处理被极大地简化。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心。跟其他日期和时间类一样,ZoneId类也是无法修改的。时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识

// 创建时区对象
ZoneId zoneId = ZoneId.of("Asia/Shanghai");

地区ID都为{区域}/{城市}的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId

// 通过TimeZone的toZoneId转换为新的时区对象
ZoneId zoneId1 = TimeZone.getDefault().toZoneId();

一旦得到ZoneId对象,就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点

// 创建时区
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
LocalDate date=LocalDate.of(2022, 12, 12);
// 根据 LocalDate 获取 ZonedDateTime
ZonedDateTime zdt1=date.atStartOfDay(zoneId);

LocalDateTime dateTime=LocalDateTime.of(2022, 12, 12, 13, 14);
// 根据 LocalDateTime 获取 ZonedDateTime
ZonedDateTime zdt2=dateTime.atZone(zoneId);

Instant instant=Instant.now();
// 根据 Instant 获取 ZonedDateTime
ZonedDateTime zdt3=instant.atZone(zoneId);

重要:LocalDate、LocalTime、ZoneId、LocalDateTime、ZonedDateTime五者关系如下:

相信这五张图对您理解个对象之间的关系和差异有非常有力的帮助

总结

  • Java 8之前老版的java.util.Date类以及其他用于日期时间的类在设计上有缺陷,比如可变性,尴尬的起始时间,默认值和包名
  • 新版的日期和时间API中,日期-时间对象是不可变的
  • 新的API提供了两种不同的时间表示方式,有效地区分了运行时人【LocalDate】和机器【Instant】的不同需求
  • 操纵时间时钟返回一个全新的日期和时间,不会改变原本的日期和时间
  • 使用TemporalAdjuster可以更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器
  • 格式化时间也变得线程安全,而且可以根据自己的意愿自定义格式化风格

文章出自:​​石添的编程哲学​​,如有转载本文请联系【石添的编程哲学】今日头条号。

责任编辑:武晓燕 来源: 今日头条
相关推荐

2014-12-22 10:14:31

Java8

2024-03-18 00:00:00

CalendaJava8Date

2020-12-01 07:18:35

Java8日期时间

2023-04-14 08:50:11

JavaDateLocalDate

2021-02-24 10:03:17

Java8日期API

2020-01-15 15:12:38

Java8日期处理代码

2020-02-06 11:35:58

Java 8APIJava

2018-06-13 15:48:21

Spring BootJava 8API

2023-05-12 07:40:01

Java8API工具

2016-11-29 12:46:24

JavaJava8时间日期库

2024-02-04 08:35:03

APIJava 8数据库

2024-02-02 11:18:37

Java 8API对象

2012-03-27 09:20:57

Java

2013-07-19 09:50:10

Java8API

2020-10-28 09:43:40

前端开发Vue

2023-07-26 07:13:55

函数接口Java 8

2023-04-06 08:24:25

Java8管理LocalDate

2024-03-11 11:02:03

Date类JavaAPI

2019-11-07 14:21:05

微软 Windows Linux

2020-07-24 08:11:04

Java8ava5语言
点赞
收藏

51CTO技术栈公众号