大纲
新日期类的出现
JDK8之前的日期类计算两个时间的差值比较麻烦,比如说求出两个时间的间隔天数需要做很多操作。在格式化和解析时间方面,SimpleDateFormat类是线程不安全的,在多线程的情况下,可能会出现解析异常的情况。
下面举个例子看下:
/**
* 测试SimpleDateFormat的线程不安全问题
*/
@Test
public void testOldFormat(){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 20; i++) {
new Thread(()->{
try {
Date date = dateFormat.parse("2020-08-05 23:13:54");
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
运行上面的代码后,可能会出现异常和解析时间错误的情况,此次我出现的异常如下:
Exception in thread "Thread-3" Exception in thread "Thread-2" java.lang.NumberFormatException: For input string: ".220222022202E"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:578)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
Wed Aug 05 23:13:54 CST 2020
at com.gsgb.local.LocalTest.lambda$testOldFormat$0(LocalTest.java:43)
at java.lang.Thread.run(Thread.java:748)
这也说明了SimpleDateFormat类是线程不安全的。
总的来说,JDK8之前的日期类的问题有:
-
设计不合理。例如util包的Date类里面已经有很多类过时了。用于解析的类在java.text包里,而且线程不安全。 -
线程不安全util包的Date类也是线程不安全的,类没有用final修饰,是可变的。而新日期类基本都是不可变的类。 -
时区相关问题不好处理。
基本日期类
本节主要说说LocalDateTime、LocalDate、LocalTime,由于它们的用法基本上相同,主要针对LocalDateTime讲解。
获取当前时间
JDK8之前的日期类有线程安全的问题,JDK8的新日期类大部分都是final修饰的,是不可变的,而且其构造函数都是私有的,我们需要调用静态方法生成日期类的对象。
/**
* now方法获取当前时间
*/
@Test
public void testNow() {
LocalDateTime localDateTime = LocalDateTime.now();
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
System.out.println("localDateTime的时间是: " + localDateTime);
System.out.println("localDate的时间是: " + localDate);
System.out.println("localTime的时间是: " + localTime);
}
控制台:
localDateTime的时间是:2020-08-05T23:41:00.902
localDate的时间是:2020-08-05
localTime的时间是:23:41:00.903
一些常量
在上面三个日期类中有一些静态常量,我觉得LocalTime的还有点用,其他的用处好像不是很大,感兴趣可以去翻下源码看看:
/**
* 获取一些常量
* 测试时间日期类的一些常量
* 主要看{@link LocalTime}的一些
*/
@Test
public void testConstant() {
System.out.println("某一天的结束时间是: " + LocalTime.MAX);
System.out.println("某一天的开始时间是: " + LocalTime.MIN);
System.out.println("某一天的半夜时间是: " + LocalTime.MIDNIGHT);
System.out.println("某一天的正中午时间是: " + LocalTime.NOON);
}
控制台:
某一天的结束时间是: 23:59:59.999999999
某一天的开始时间是: 00:00
某一天的半夜时间是: 00:00
某一天的正中午时间是: 12:00
获取指定时间
假如我们要获取指定的时间,可以使用of方法来获得。
基本上time包下的时间里都有of方法,of方法有很多重载形式,用法都差不多。
/**
* of方法获取指定时间
* 测试不带时区的时间日期类的of方法,获取指定时间
*/
@Test
public void testOf() {
// 1.LocalDate
LocalDate date1 = LocalDate.of(1997, 8, 8); // 设置1997-08-08
// Mouth是一个代表月份枚举类,下面再讲
LocalDate date2 = LocalDate.of(2000, Month.JUNE, 23); // 设置2000-06-23
System.out.println("date1时间为:" + date1);
System.out.println("date2时间为:" + date2);
// 2.LocalTime
LocalTime time1 = LocalTime.of(8, 10);
System.out.println("time1时间为:" + time1);
// 3.LocalDateTime
// 设置2015-09-05 8:30
LocalDateTime dateTime1 = LocalDateTime.of(2015, 9, 5, 8, 30);
// 设置2019-06-25 12:00
LocalDateTime dateTime2 = LocalDateTime.of(2019, Month.JUNE, 25, 12, 0);
System.out.println("dateTime1时间为:" + dateTime1);
System.out.println("dateTime2时间为:" + dateTime2);
// LocalDateTime有个特殊的of方法
// LocalDateTime of(LocalDate date, LocalTime time)
LocalDateTime dateTime3 = LocalDateTime.of(date1, time1);
System.out.println("特殊of方法的dateTime3的时间为:" + dateTime3);
}
控制台:
date1时间为: 1997-08-08
date2时间为: 2000-06-23
time1时间为: 08:10
dateTime1时间为: 2015-09-05T08:30
dateTime2时间为: 2019-06-25T12:00
特殊of方法的dateTime3的时间为: 1997-08-08T08:10
获取日期的属性
在新的日期类中提供了很多get方法,比如说获取年、月、日、时、分、秒、纳秒这些,就像之前的Calendar类一样。
/**
* get方法
* 测试不带时区的时间日期类的get方法,获取日期时间对象的各个字段值
* 以LocalDateTime为例,其他基本是一样的
*/
@Test
public void testGet() {
LocalDateTime dateTime = LocalDateTime.now();
System.out.println("当前时间:" + dateTime);
System.out.println("获取年份:" + dateTime.getYear());
System.out.println("获取月份枚举:" + dateTime.getMonth());
System.out.println("获取月份:" + dateTime.getMonthValue());
System.out.println("获取月的第几天:" + dateTime.getDayOfMonth());
System.out.println("获取小时:" + dateTime.getHour());
}
控制台:
当前时间:2020-08-05T23:47:50.149
获取年份:2020
获取月份枚举:AUGUST
获取月份:8
获取月的第几天:5
获取小时:23
除了上面这些get方法,还有一个需要传递参数的get方法
-
int get(TemporalField field)
这个TemporalField 类是一个接口,我们可以去查看它的子类ChronoField的代码,它是一个枚举类,这个类里面定义了一些特殊含义的时间名字,可以去源码里面查看
下面举两个例子。
/**
* 日期类的特殊的Get方法
* 以LocalDateTime为例,其他两个是差不多的
* 在{@link ChronoField}类中定义了一些具有特殊含义的枚举
*
* @see ChronoField
*/
@Test
public void testGet02() {
LocalDateTime dateTime = LocalDateTime.now();
// AMPM_OF_DAY 表示时间是上午还是下午,0-上午,1-下午
int amPmOfDay = dateTime.get(ChronoField.AMPM_OF_DAY);
System.out.println("现在时间在当天是:" + (amPmOfDay == 0 ? "AM" : "PM"));
// MINUTE_OF_HOUR 表示时间的分钟数
int clockHourOfDay = dateTime.get(ChronoField.MINUTE_OF_HOUR);
System.out.println("现在时间的分钟是第" + clockHourOfDay + "分");
}
控制台
现在时间在当天是:PM
现在时间的分钟是第48分
比较两个日期大小
JDK8之前比较时间的大小比较麻烦,现在有现成的方法给我们调用。
/**
* 日期类的比较方法,以LocalDateTime为例
* isAfter
* isBefore
* isEqual
*/
@Test
public void testCompare() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime dateTime = LocalDateTime.of(2020, 7, 30, 12, 0);
// 调用比较方法
boolean before = now.isBefore(dateTime);
boolean equal = now.isEqual(dateTime);
boolean after = now.isAfter(dateTime);
System.out.println("当前时间是否在指定时间之前: " + before);
System.out.println("当前时间是否和指定时间相等: " + equal);
System.out.println("当前时间是否在指定时间之后: " + after);
}
修改日期时间字段值
修改指定实时间字段方法
假如我们要修改日期类到我们指定的时间,就需要使用到with方法了,可以修改年月日时分秒等。以LocalDateTime为例。
需要注意的是,新的日期类大部分都是带final修饰的,所以返回的时间对象一般的是新创建的,和旧时间对象不是同一个对象,下面有测试。
with方法
/**
* with方法,直接修改日期
*/
@Test
public void testWith() {
LocalDateTime now = LocalDateTime.now();
// 1.修改年份为2022年
LocalDateTime dateTime1 = now.withYear(2022);
// 2.修改分钟为10分
LocalDateTime dateTime2 = now.withMinute(10);
System.out.println("修改年份为2022后的时间为: " + dateTime1);
System.out.println("修改分钟为10分后的时间为: " + dateTime2);
// 3.修改前后是否是同一个对象
System.out.println("修改前后是否是同一个对象:" + (now == dateTime1));
}
控制台
修改年份为2022后的时间为: 2022-08-06T00:03:10.255
修改分钟为10分后的时间为: 2020-08-06T00:10:10.255
修改前后是否是同一个对象:false
根据接口修改指定实时间字段
从上图我们可以看到with有一个方法是需要传入TemporalField类型的参数的,这个TemporalField 是一个接口,有个枚举类型的实现类ChronoField,里面很多枚举含义,具体可以查看源码。get方法也可以传这个参数。
-
with(TemporalField field, long newValue)
/**
* 10.with方法,根据TemporalField类型修改日期
*/
@Test
public void testWith02() {
LocalDateTime now = LocalDateTime.now();
// 1.将日期中的星期修改为星期一
LocalDateTime dateTime1 = now.with(ChronoField.DAY_OF_WEEK, 1);
// 2.将日期中的小时数修改为22时
LocalDateTime dateTime2 = now.with(ChronoField.HOUR_OF_DAY, 22);
System.out.println("将日期中的星期修改为星期一:" + dateTime1);
System.out.println("将日期中的小时数修改为21时:" + dateTime2);
}
时间调节器TemporalAdjuster
with有一个重载方法需要传入一个接口类型的TemporalAdjuster的值,有一个现成的TemporalAdjusters类提供了一些特定的字段,直接使用即可。
/**
* 11.with方法,TemporalAdjuster接口和TemporalAdjusters类
*/
@Test
public void testWith03() {
// 将日期转换为下个月的第一天的日期
LocalDate now = LocalDate.now();
LocalDate dateTime = now.with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println("将日期转换为下个月的第一天的日期:" + dateTime);
}
自定义TemporalAdjuster调节器
通过查看源码,我们可以看到TemporalAdjusters类里面的方法都是直接重写了TemporalAdjuster接口的方法。
TemporalAdjuster是一个函数式接口,我们也可以重写该接口的方法来实现我们需要对日期的操作。
比如我们要计算距离当前时间100天的日期。
/**
* 12.with方法,重写TemporalAdjuster接口,自定义操作
*/
@Test
public void testWith04() {
// 计算距离当前时间100天的日期
LocalDate now = LocalDate.now();
LocalDate localDate = now.with(temporal -> {
LocalDate date = (LocalDate) temporal;
return date.plusDays(100);
});
System.out.println("计算距离当前时间100天的日期:" + localDate);
}
增加减少指定字段
增加指定字段的方法
plus方法其实和minus方法是一样的,minus方法里面就是调用的plus方法,因此只需要关注plus方法即可。
plus方法的作用就是针对日期类的某些字段进行偏移的,获得偏移之后的日期。plus方法
/**
* minus和plus方法
*/
@Test
public void testPlusAndMinus() {
LocalDateTime now = LocalDateTime.now();
// 1.当前日期增加一个星期
LocalDateTime plusWeeks = now.plusWeeks(1);
// 2.当前日期增加20天
LocalDateTime plusDays = now.plusDays(20);
System.out.println("当前日期增加一个星期:"+plusWeeks);
System.out.println("当前日期增加20天:"+plusDays);
}
TemporalUnit接口修改时间
plus方法有个重载方法,第一个参数表示偏移量,第二个参数TemporalUnit是一个接口,可以之间看它的实现类ChronoUnit,里面定义了一些字段。
-
plus(long amountToAdd, TemporalUnit unit)
/**
* minus和plus方法,ChronoUnit
*/
@Test
public void testPlusAndMinus02() {
LocalDateTime now = LocalDateTime.now();
// 1.当前日期增加两年
LocalDateTime plusY = now.plus(2, ChronoUnit.YEARS);
// 2.下一个世纪
LocalDateTime plusC = now.plus(1, ChronoUnit.CENTURIES);
System.out.println("当前日期增加两年:"+plusY);
System.out.println("下一个世纪:"+plusC);
}
TemporaAmount接口修改时间
TemporaAmount接口有两个重要的实现类Duration和Period。
其中Duration类表示秒或纳秒时间间隔,Period表示一段时间的年、月、日。这两个类后面详细讲解。
/**
* minus和plus方法,ChronoUnit
*/
@Test
public void testPlusAndMinus03() {
LocalDateTime now = LocalDateTime.now();
// 1.当前时间增加1年1月1日
LocalDateTime plusP = now.plus(Period.of(1, 1, 1));
// 2.当前时间增加10分钟
LocalDateTime plusD = now.plus(Duration.ofMinutes(10));
System.out.println("当前时间增加1年1月1日:" + plusP);
System.out.println("当前时间增加10分钟:" + plusD);
}
自定义查询日期策略
在LocalDate、LocalTime、LocalDateTime这些类中都有一个query方法个,可以针对日期进行查询。
<R> R query(TemporalQuery<R> query)
该方法是一个泛型方法,点开源码我们可以发现这个接口是一个函数式接口,里面就只有一个抽象方法
R queryFrom(TemporalAccessor temporal);
TemporalAccessor这个类是JDK8日期相关类的顶级接口,我们可以通过重写这个接口中的方法来完成我们想要的查询日期的操作。
/**
* query查询方法
*/
@Test
public void testQuery() {
// 计算下一个国庆节还有多少天
LocalDate now = LocalDate.now();
long betweenDays = (long) now.query(temporal -> {
LocalDate date = (LocalDate) temporal;
int year = date.getYear();
// 获取当年国庆节的日期
LocalDate nationalDay = LocalDate.of(year, 10, 1);
if (date.isAfter(nationalDay)) {
// 今年的国庆节已过,计算距离下一年国庆节的时间
nationalDay = nationalDay.plusYears(1);
}
// 今年国庆节未过
return ChronoUnit.DAYS.between(date,nationalDay);
});
System.out.println("下一个国庆节还有" + betweenDays + "天");
}
时间戳Instant类
Instant时间戳类记录的是格林威治时间,是给程序代码来使用计算的,Instant类可以作为旧版日期类和新版日期类转换的桥梁,后面用专门的章节说说。
Instant类的用法和LocalDateTime这些类是类似的。
/**
* 测试时间戳类
*/
@Test
public void testInstant() {
// 获取当前时间戳
Instant now = Instant.now();
long epochMilli = now.toEpochMilli();
System.out.println("当前时间:" + now);
System.out.println("当前时间戳:" + epochMilli);
}
控制台:
当前时间:2020-08-06T15:01:31.136Z
当前时间戳:1596726091136
代表年月日的日期类
关于精确的年月日日期类有三个,分别是:
-
Year类 -
YearMonth类 -
MonthDay类
这三个类也和LocalDateTime的用法类似,也有now这些方法。它们自己也有自己独有的方法,例如Year类和YearMouth类都有判断某年是否是闰年的方法。
@Test
public void testYear() {
// 1.判断某年是否是闰年
boolean leap1 = Year.isLeap(1997);
System.out.println("1997年是否是闰年:" + leap1); // false
// 2.判断Year对象封装的年份是否是闰年
boolean leap2 = Year.now().isLeap();
System.out.println("今年是否是闰年:" + leap2); // 2020 true
// 3.YearMonth判断其封装的年份是否是闰年
boolean leapYear = YearMonth.now().isLeapYear();
System.out.println("今年是否是闰年:" + leapYear); // 2020 true
}
关于YearMonth类还有其他独有的方法,比如
int lengthOfYear(); // 获得封装的年份的一年的天数
int lengthOfMonth(); // 获得封装月份的一月的天数
boolean isValidDay(int dayOfMonth); // 判断传入的天数在封装月份中是否合法
MonthDay类也有自己的方法,和YearMonth类差不多,可以自己去看下源码。
月份和星期枚举
-
Month枚举类:表示月份,每个枚举值的名称都是月份的英文单词。 -
DayOfWeek 枚举类:表示星期,每个枚举值的名称都是星期的英文单词。
/**
* Month枚举类
*/
@Test
public void testMonth() {
// 1.of方法创建对象
Month month = Month.of(7);
// 2.获取某月的最长天数和最短天数
int maxLength = Month.FEBRUARY.maxLength();
int minLength = Month.FEBRUARY.minLength();
// 3.获取某个月的 在/不在 闰年的长度 布尔值表示是否是闰年
int julyLength = Month.JULY.length(true);
// 4.获取当前月份所在的季度的 第一个月份
// DECEMBER是12月,所以返回OCTOBER 9月
Month month2 = Month.DECEMBER.firstMonthOfQuarter();
// 5.获取某个月 在/不在 闰年的第一天在该年的天数
int monthValue = Month.JULY.firstDayOfYear(false);
System.out.println("Mouth的of方法: " + month); // JULY
System.out.println("某月的最长天数: " + maxLength); // 29
System.out.println("某月的最短天数: " + minLength); // 28
System.out.println("七月在闰年一个月的长度为: " + julyLength); // 31
System.out.println("获取12月所在的季度的第一个月份: " + month2); // OCTOBER
System.out.println("获取7月不在闰年的第一天在该年的天数: " + monthValue); // 182
}
原文始发于微信公众号(肝帝笔记):JDK8新日期类(一)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/31562.html