时间对生活来说非常重要,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);
运行结果:
获取年月日:
// 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天:有效值为 1 到 365,闰年的有效值为 1 到 366
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可以更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器
- 格式化时间也变得线程安全,而且可以根据自己的意愿自定义格式化风格
文章出自:石添的编程哲学,如有转载本文请联系【石添的编程哲学】今日头条号。