JDK8日期API
在Java面世之初,标准库就引入了两种用于处理日期和时间的类,它们是java.util.Date和java.util.Calendar,而前者堪称类糟糕设计的典范,浏览API可以发现,从Java1.1开始,Date类中的所有方法就已经被弃用,Java1.1推荐采用Calendar类处理日期和时间,但是这个类同样存在不少问题。
1.对于日期的计算困难问题
毫秒值与日期直接转换比较繁琐,其次通过毫秒值来计算时间的差额步骤较多
2.线程安全问题
SimpleDateFormat类是线程不安全的,在多线程的情况下,全局共享一个SimpleDateFormat类中的Calendar对象,有可能会出现异常。
3.另外的一个问题就是在 java.util.Date 和 java.util.Calendar 类之前,枚举类型(ENUM)还没有出现,所以在字段中使用整数常量导致整数常量都是可变的,而不是线程安全的.为了处理实际开发中遇到的问题,标准库随后引入了java.sql.Date作为java.util.Date的子类,但是还是没能彻底解决问题.
最终JavaSE 8中引入了java.time包,这种全新的包从根本上解决了长久以来的存在的诸多弊端,java.time包基于Joda-Time库构件,是一种免费的开源解决方案,实际上在Java 8没有出现之前,公司中已经广泛使用Joda-Time来解决Java中的日期与时间问题,Joda-Time的设计团队也参与了java.time包的开发.
Date-Time API中的基本类使用:
1.常用类的概述与功能介绍
Instant类
Instant类对时间轴上的单一瞬时点建模,可以用于记录应用程序中的事件时间戳,使用Instant类作为中间类完成转换.
Duration类
Duration类表示秒或纳秒时间间隔,适合处理较短的时间,需要更高的精确性.
Period类
Period类表示一段时间的年、月、日.
LocalDate类
LocalDate是一个不可变的日期时间对象,表示日期,通常被视为年月日.
LocalTime类
LocalTime是一个不可变的日期时间对象,代表一个时间,通常被看作是小时-秒,时间表示为纳秒精度.
LocalDateTime类
LocalDateTime是一个不可变的日期时间对象,代表日期时间,通常被视为年-月-日-时-分-秒.
ZonedDateTime类
ZonedDateTime是具有时区的日期时间的不可变表示,此类存储所有日期和时间字段,精度为纳秒,时区为区域偏移量,用于处理模糊的本地日期时间。
2.now方法在日期/时间类的使用
Date-Time API中的所有类均生成不可变实例,它们是线程安全的,并且这些类不提供公共构造函数,也就是说没办法通过new的方式直接创建,需要采用工厂方法加以实例化。
now方法可以根据当前日期或时间创建实例:
public class Demo01 { public static void main(String[] args) { // Instant封装的时间为祖鲁时间并非当前时间. // 祖鲁时间也是格林尼治时间,也就是国际标准时间. Instant instant = Instant.now(); // LocalDate封装的只有年月日,没有时分秒,格式为yyyy-MM-dd. LocalDate localDate = LocalDate.now(); // LocalTime封装的只有时分秒,没有年月日,格式为hh:mm:ss.sss,最后的sss是纳秒. LocalTime localTime = LocalTime.now(); // LocalDateTime将LocalDate和LocalTime合二为一,在年月日与时分秒中间使用T作为分隔. LocalDateTime localDateTime = LocalDateTime.now(); // ZonedDateTime中封装了年月日时分秒,以及UTC(祖鲁时间)偏移量,并且还有一个地区名. // +8:00代表中国是东八区,时间比国际标准时间快八小时. ZonedDateTime zonedDateTime = ZonedDateTime.now(); // Instant:2020-04-24T03:56:32.582Z System.out.println("Instant:" + instant); // LocalDate:2020-04-24 System.out.println("LocalDate:" + localDate); // LocalTime:11:56:32.725 System.out.println("LocalTime:" + localTime); // LocalDateTime:2020-04-24T11:56:32.725 System.out.println("LocalDateTime:" + localDateTime); // ZonedDateTime:2020-04-24T11:56:32.729+08:00[Asia/Shanghai] System.out.println("ZonedDateTime:" + zonedDateTime); } }
不仅仅是上面提供的几个类可以使用now方法,Java8的Time包中还提供了其他的几个类可以更精准的获取某些信息.
Year类(表示年)
YearMonth类(表示年月)
MonthDay类(表示月日)
public class Demo02 { public static void main(String[] args) { //初始化Year的实例化对象. Year year = Year.now(); //初始化YearMonth的实例化对象 YearMonth month = YearMonth.now(); //初始化MonthDay的实例化对象. MonthDay day = MonthDay.now(); System.out.println(year); // 2020 System.out.println(month); // 2020-04 System.out.println(day); // --04-24 } }
3.of方法在日期/时间类的应用
of方法可以根据给定的参数生成对应的日期/时间对象,基本上每个基本类都有of方法用于生成的对应的对象,而且重载形式多变,可以根据不同的参数生成对应的数据.
public class Demo03 { public static void main(String[] args) { LocalDate date = LocalDate.of(2018, 8, 8); System.out.println("LocalDate:" + date); // LocalDate:2018-08-08 // LocalTime time = LocalTime.of(19, 0); // LocalTime:19:00 // LocalTime time = LocalTime.of(19, 10, 4); // LocalTime:19:10:04 LocalTime time = LocalTime.of(19, 10, 4, 12); // LocalTime:19:10:04.000000012 System.out.println("LocalTime:" + time); LocalDateTime time1 = LocalDateTime.of(2018, 8, 8, 19, 0, 0, 0); System.out.println(time1); // 2018-08-08T19:00 LocalDateTime time2 = LocalDateTime.of(date, time); System.out.println(time2); // 2018-08-08T19:10:04.000000012 } }
4.为LocalDateTime添加时区信息
在学习ZonedDateTime的时候,发现了这个对象里面封装的不仅有时间日期,并且还有偏移量+时区,那么时区如何在Java中获取呢,通过提供的一个类ZoneId的getAvailableZoneIds方法可以获取到一个Set集合,集合中封装了600个时区.
//获取所有的时区信息 Set<String> availableZoneIds = ZoneId.getAvailableZoneIds(); for (String zoneId : availableZoneIds) { System.out.println(zoneId); }
同样也提供了获取当前系统默认的时区的方式systemDefault()方法
//获取当前系统默认的时区信息 ZoneId zoneId = ZoneId.systemDefault(); System.out.println(zoneId);
我们可以通过给LocalDateTime添加时区信息来查看到不同时区的时间,比如说LocalDateTime中当前封装的是上海时间,那么想知道在此时此刻,纽约的时间是什么,就可以将纽约的时区Id添加进去,就可以查看到了,方式如下:
封装时间LocalDateTime并添加时区信息.
LocalDateTime time = LocalDateTime.of(2018, 11, 11, 8, 54, 38); ZonedDateTime zonedDateTime = time.atZone(ZoneId.of("Asia/Shanghai")); System.out.println("Asia/Shanghai的时间是:" + zonedDateTime);
更改时区信息查看对应时间.
ZonedDateTime otherZonedTime = zonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("在同一时刻,Asia/Tokyo的时间是:" + otherZonedTime);
5.Month枚举类的使用
java.time包中引入了Month的枚举,Month中包含标准日历中的12个月份的常量(从JANURAY到DECEMEBER),也提供了一些方便的方法供我们使用.
推荐在初始化LocalDate和LocalDateTime对象的时候,月份的参数使用枚举的方式传入,这样更简单易懂而且不易出错,因为如果是老的思维,Calendar传入0的话,那么会出现异常.
public class Demo05 { public static void main(String[] args) { LocalDateTime.of(2011, Month.JUNE, 15, 11, 11, 11); //of方法可以根据传入的数字返回对应的月份. Month month = Month.of(12); System.out.println(month); // DECEMBER } }
根据现有实例创建日期与时间对象:
想要修改某个日期/时间对象的现有实例时,我们可以使用plus和minus方法来完成操作.
Java8中日期时间相关的API中的所有实例都是不可改变的,一旦创建LocalDate,LocalTime,LocalDateTime就无法修改他们(类似于String),这对于线程安全非常有利.
1.plus方法在LocalDate与LocalTime中的使用
LocalDate中定义了多种对日期进行增减操作的方法:
LocalDate plusDays(long days) 增加天数
LocalDate plusWeeks(long weeks) 增加周数
LocalDate plusMonths(long months) 增加月数
LocalDate plusYears(long years) 增加年数
LocalTime中定义了多种对时间进行增减操作的方法:
LocalTime plusNanos(long nanos) 增加纳秒
LocalTime plusSeconds(long seconds) 增加秒
LocalTime plusMinutes(long minutes) 增加分钟
LocalTime plusHours(long hours) 增加小时
public class Demo06 { public static void main(String[] args) { LocalTime time = LocalTime.now(); LocalTime plusNanosTime = time.plusNanos(500); LocalTime plusSecondsTime = time.plusSeconds(45); LocalTime plusMinutesTime = time.plusMinutes(19); LocalTime plusHoursTime = time.plusHours(3); System.out.println("当前的时间是:" + time); System.out.println("45秒后的时间是:" + plusSecondsTime); System.out.println("19分钟后的时间是:" + plusMinutesTime); System.out.println("500纳秒后的时间是:" + plusNanosTime); System.out.println("3小时后的时间是:" + plusHoursTime); } }
本文中都是使用plusXXX的方法进行演示,实际上也有对应的减少方法,以minus开头的方法对应的即为减少,实际上minus方法调用的也是plus方法,只不过传入的参数是负数.
2.plus和minus方法的应用
刚才的plusXXX相关的方法都是添加了数值到具体的某一项上,其实还有两个单独的plus方法
plus(TemporaAmount amountToAdd)
TemporaAmount是一个接口,当接口作为方法的参数的时候,实际上传入的是接口的实现类对象,根据查看这个接口的体系,可以看到这个接口有一个实现类,名字叫做Period,表示一段时间.这个类本身提供了of(int year,int month,int day)来表示,例: Period.of(1,2,3)返回的对象表示的即为1年2个月3天
Period period = Period.of(1, 2, 3); LocalDate localDate = LocalDate.now().plus(period); System.out.println(localDate);
plus(long l,TemporaUnit unit)
在实际开发过程中,可能还会更精准的去操作日期或者说增加一些特殊的时间,比如说1个世纪,1个半天,1千年,10年等,Java8提供了这些日期的表示方式而不需要去单独进行计算
TemporaUnit是一个接口,通过查看体系接口发现,可以使用子类ChronoUnit来表示,ChronoUnit封装了很多时间段供我们使用.
注意:第一个参数为单位,第二个参数为时间长度.
例:plus(1, ChronoUnit.DECADES)加1个10年.
plus(1, ChronoUnit.CENTURIES)加1个100年.
LocalDateTime localDateTime = LocalDateTime.now(); LocalDateTime localDateTime1 = localDateTime.plus(1, ChronoUnit.DECADES);// 十年 System.out.println(localDateTime1); localDateTime1.plus(1, ChronoUnit.HALF_DAYS); // 半天
with方法在LocalDateTime类的应用
如果不需要对日期进行加减而是要直接修改日期的话,那么可以使用with方法,with方法提供了很多种修改时间的方式.
LocalDateTime withNano(int i) 修改纳秒
LocalDateTime withSecond(int i) 修改秒
LocalDateTime withMinute(int i) 修改分钟
LocalDateTime withHour(int i) 修改小时
LocalDateTime withDayOfMonth(int i) 修改日
LocalDateTime withMonth(int i) 修改月
LocalDateTime withYear(int i) 修改年
LocalDateTime endTime = localDateTime.withDayOfMonth(1); System.out.println("修改前的时间是:" + localDateTime); System.out.println("修改之后的时间是:" + endTime);
with(TemporalField field, long newValue)
TemporalField是一个接口,通过查看体系结构,可以使用它的子类ChronoField,ChronoField中封装了一些日期时间中的组成部分,可以直接选择之后传入第二个参数进行修改.
例:with(ChronoField.DAY_OF_MONTH,1); 将日期中的月份中的天数改为1.
例:with(ChronoField.YEAR,2021); 将日期中的年份改为2021.
LocalDateTime endTime1 = localDateTime.with(ChronoField.DAY_OF_MONTH, 1); System.out.println("修改前的时间是:" + localDateTime); System.out.println("修改之后的时间是:" + endTime1);
调节器TemporalAdjuster与查询TemporalQuery:
通过with方法可以修改日期时间对象封装的数据,但是当有一些时候可能会做一些复杂的操作,比如说将时间调整到下个周的周日,下一个工作日,或者本月中的某一天,这个时候可以使用调节器TemporalAdjuster来更方便的处理日期.
with方法有一个重载形式,需要传入一个TemporalAdjuster对象,通过查看发现TemporalAdjuster是一个接口,方法的参数也是一个接口,那么实际上传入的是这个接口的实现类对象。TemporalAdjuster是一个函数式接口,里面有一个抽象方法叫做Temporal adjustInto(Temporal temporal);传入一个Temporal对象通过实现逻辑返回一个Temporal对象,Temporal是LocalDate,LocalTime相关日期类的父接口,实际上传入的就是一个时间日期对象返回一个时间日期对象.
TemporalAdjusters类中常用静态方法的使用
static TemporalAdjuster firsyDayOfNextMonth() 下个月的第一天
static TemporalAdjuster firstDayOfNextYear() 下一年的第一天
static TemporalAdjuster firstDayOfYear() 当年的第一天
static TemporaAdjuster firstInMonth(DayOfWeek dayOfWeek) 当月的第一个周x(通过参数确定)
static TemporaAdjuster lastDayOfMonth() 当月的最后一天
static TemporaAdjuster lastDayOfYear() 当年的最后一天
static TemporaAdjuste lastInMonth(DayOfWeek dayOfWeek) 当月的最后一个周x(通过参数确定)
static TemporaAdjuster next(DayOfWeek dayOfWeek) 下一个周x(通过参数确定)
static TemporaAdjuster previous(DayOfWeek dayOfWeek) 上一个周x(通过参数确定)
public class Demo07 { public static void main(String[] args) { LocalDate time = LocalDate.now(); // 将时间修改为当月的第一天 LocalDate firstDayOfMonth = time.with(TemporalAdjusters.firstDayOfMonth()); // 将时间修改为下个月的第一天. LocalDate firstDayOfNextMonth = time.with(TemporalAdjusters.firstDayOfNextMonth()); //将时间修改为下一年的第一天. LocalDate firstDayOfNextYear = time.with(TemporalAdjusters.firstDayOfNextYear()); //将时间修改为本年的第一天. LocalDate firstDayOfYear = time.with(TemporalAdjusters.firstDayOfYear()); //将时间修改为本月的最后一天. LocalDate lastDayOfMonth = time.with(TemporalAdjusters.lastDayOfMonth()); //将时间修改为本年的最后一天. LocalDate lastDayOfYear = time.with(TemporalAdjusters.lastDayOfYear()); System.out.println("当月的第一天是:" + firstDayOfMonth); System.out.println("下个月的第一天是:" + firstDayOfNextMonth); System.out.println("下一年的第一天是:" + firstDayOfNextYear); System.out.println("本年的第一天是:" + firstDayOfYear); System.out.println("本月的最后一天是:" + lastDayOfMonth); System.out.println("本年的最后一天是:" + lastDayOfYear); } }
DayOfWeek的使用:DayOfWeek是一周中星期几的枚举类,其中封装了从周一到周日.
//将当前时间修改为下一个周日 LocalDate nextSunday = time.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)); //将当前时间修改为上一个周三 LocalDate previousWednesday = time.with(TemporalAdjusters.previous(DayOfWeek.WEDNESDAY)); System.out.println("下一个周日是:" + nextSunday); System.out.println("上一个周三是:" + previousWednesday);
自定义TemporalAdjuster调节器:
通过Java8本身提供的TemporalAdjusters中的方法可以完成一些常用的操作,如果要自定义日期时间的更改逻辑,可以通过实现TemporalAdjuster类接口中的方式来完成.
创建类实现TemporalAdjuster接口
实现TemporalAdjuster中的adjustInto方法,传入一个日期时间对象,完成逻辑之后返回日期时间对象.
通过with方法传入自定义调节器对象完成更改.
/** * 假如员工一个月中领取工资,发薪日是每个月的15号,如果发薪日是周末,则调整为周五. */ public class PayDayAdjuster implements TemporalAdjuster { @Override public Temporal adjustInto(Temporal temporal) { //1.将temporal转换为子类对象LocalDate,from方法可以将任何时态对象转换为LocalDate. LocalDate payDay = LocalDate.from(temporal); //2.判断当前封装的时间中的日期是不是当月15日,如果不是,则更改为15日. int day; if (payDay.getDayOfMonth() != 15) { day = 15; } else { day = payDay.getDayOfMonth(); } LocalDate realPayDay = payDay.withDayOfMonth(day); //3.判断realPayDay对象中封装的星期数是不是周六或者是周日,如果是周六或者是周日则更改为周五. if (realPayDay.getDayOfWeek() == DayOfWeek.SUNDAY || realPayDay.getDayOfWeek() == DayOfWeek.SATURDAY) { //说明发薪日是周末,则更改为周五. realPayDay = realPayDay.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); } return realPayDay; } }
public class Demo08 { public static void main(String[] args) { //封装LocalDate对象为2018年12月1日. LocalDate payDay = LocalDate.of(2018, 12, 1); //2018年12月15日为周末,所以要提前到周五发放工资,通过自定义调节器完成对时间的修改. LocalDate realPayDay = LocalDate.from(new PayDayAdjuster().adjustInto(payDay)); System.out.println("预计的发薪日是2018年12月15日,实际的发薪日为:" + realPayDay); } }
TemporalQuery的应用:
学习的时态类对象(LocalDate,LocalTime)都有一个方法叫做query,可以针对日期进行查询.
R query(TemporalQuery query)这个方法是一个泛型方法,返回的数据就是传入的泛型类的类型,TemporalQuery是一个泛型接口,里面有一个抽象方法是RqueryFrom(TemporalAccessor temporal),TemporalAccessor是Temporal的父接口,实际上也就是LocalDate,LocalDateTime相关类的顶级父接口,这个queryFrom的方法的实现逻辑就是,传入一个日期/时间对象通过自定义逻辑返回数据.
如果要计划日期距离某一个特定天数差距多少天,可以自定义类实现TemporalQuery接口并且作为参数传入到query方法中.
/** * 获取某一天距离下一个劳动节的相隔天数的实现类. */ public class UntilDayQueryImpl implements TemporalQuery<Long> { @Override public Long queryFrom(TemporalAccessor temporal) { //获取当前的年/月/日信息. LocalDate now = LocalDate.from(temporal); //封装劳动节的时间,年参数传递year,month和day是5和1. LocalDate laborDay = LocalDate.of(now.getYear(), Month.MAY, 1); //判断当前时间是否已经超过了当年的劳动节,如果超过了,则laborDay+1年. if (now.isAfter(laborDay)) { laborDay = laborDay.plusYears(1); } //通过ChronoUnit的between方法计算两个时间点的差额. return ChronoUnit.DAYS.between(now, laborDay); } }
public class Demo09 { public static void main(String[] args) { //封装LocalDate对象为当前时间. LocalDate time = LocalDate.now(); //调用time对象的query方法查询距离下一个五一劳动节还有多少天. Long l = time.query(new UntilDayQueryImpl()); System.out.println("距离下一个五一劳动节还有:" + l + "天."); } }
java.util.Date与java.time.LocalDate的转换:
对于老项目的改造,需要将Date或者Calendar转换为java.time包中相应的类的
1.使用Instant类将java.util.Date转换为java.time.LocalDate
public class Demo10 { public static void main(String[] args) { //初始化Date对象. Date d = new Date(); //将Date类对象转换为Instant类对象. Instant i = d.toInstant(); //Date类包含日期和时间信息,但是并不提供时区信息,和Instant类一样,可以通过Instant类的atZone方法添加时区信息之后进行转换. ZonedDateTime zonedDateTime = i.atZone(ZoneId.systemDefault()); //将ZonedDateTime通过toLocalDate方法转换为LocalDate对象. LocalDateTime localDateTime = zonedDateTime.toLocalDateTime(); // zonedDateTime.toLocalDate(); // zonedDateTime.toLocalTime(); System.out.println("转换之前的Date对象是:" + d); System.out.println("转换之后的LocalDate对象是:" + localDateTime); } }
2.java.sql.Date类中的转换方法使用
java.sql.Date类除了可以使用上面的转换方式,也提供直接转换为LocalDate的方法,toLocalDate.
//初始化java.sql.Date对象. java.sql.Date date = new java.sql.Date(System.currentTimeMillis()); //将java.sql.Date对象通过toLocalDate方法转换为LocalDate对象. LocalDate localDate = date.toLocalDate(); System.out.println("转换前的java.sql.Date对象是:" + date); System.out.println("转换后的LocalDate对象是:" + localDate);
3.java.sql.Timestamp类中的转换方法使用
//初始化java.sql.Timestamp对象. Timestamp t = new Timestamp(System.currentTimeMillis()); //将java.sql.Timestamp对象通过toLocalDateTime方法转换为LocalDateTime对象. LocalDateTime time = t.toLocalDateTime(); System.out.println("转换之前的Timestamp对象是:" + t); System.out.println("转换之后的LocalDateTime对象是:" + time);
日期的解析与格式化DateTimeFormatter:
SimpleDateFormat类在刚开始的讲过了是线程不安全的,所以Java8提供了新的格式化类 DateTimeFormatter.
DateTimeFormatter类提供了大量预定义格式化器,包括常量(如ISO_LOCAL_DATE),模式字母(如yyyy-MM-dd)以及本地化样式.
与SimpleDateFormat不同的是,新版本的日期/时间API的格式化与解析不需要在创建转换器对象再进行转换了,通过时间日期对象的parse/format方法可以直接进行转换.
format方法需要传入一个DateTimeFormatter对象,实际上查看DateTimeFormatter类之后,指定DateTimeFormatter中的常量即可指定格式化方法,常用的格式化方式有ISO_DATE_TIME/ISO_DATE.
public class Demo11 { public static void main(String[] args) { //对LocalDateTime进行格式化与解析,初始化LocalDateTime对象. LocalDateTime time = LocalDateTime.now(); //DateTimeFormatter类中定义了很多方式,通过常量名可以指定格式化方式. String result = time.format(DateTimeFormatter.ISO_DATE_TIME); System.out.println("ISO_DATE_TIME格式化之后的String是:" + result); String result1 = time.format(DateTimeFormatter.ISO_DATE); System.out.println("ISO_DATE格式化之后的String是:" + result1); //解析字符串的方式通过LocalDateTime类的静态方法parse方法传入需要解析的字符串即可. LocalDateTime localDateTime = LocalDateTime.parse(result); System.out.println("解析了字符串之后的LocalDateTime是:" + localDateTime); } }
除了系统的自带的方式之外,也可以通过DateTimeFormatter类提供的ofPattern方式创建自定义格式化器,格式化的写法与之前使用的SimpleDateFormat相同.
//通过通过DateTimeFormatter的ofPattern方法可以自定义格式化模式. String result2 = time.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss:SSS")); System.out.println("LocalDateTime格式化前是:" + time); System.out.println("LocalDateTime格式化后是:" + result2);