There are so many ways to calculate the difference between two dates and times in JAVA

During the spring recruitment in the first half of the year, as an interviewer, students who performed well in the interview will ask them to write a small piece of code to see. The topic is simple:

Given a date, calculate the number of days from today.

I thought that such a question was used to activate the interview atmosphere , but the result was surprising . There were very few people who could actually write it. Many people could not write an entire A4 sheet of paper, and finally they did not finish it. ..What are they doing?

First take out today's date, then calculate the values ​​of year, month, and day separately, then cut the given string to get the year, month, and day of the target, and then judge whether it is a leap year or not, and decide each The month should add 28 days or 29 days or 30 or 31 days, and finally get a number of days!

Think about the suffocating operation...

日期时间的处理, is an extremely common scenario in software development. There are also many classes and API methods related to date and time in JAVA. Here, we have comprehensively sorted out the usual coding practices, hoping to help you clarify the doorway and make it easier for you Faced with this treatment~

Classes related to datetime in JAVA

in the java.util package

class name specific description
Date The Date object is a class with a long history in JAVA for dealing with date and time, but with the iterative evolution of the version, many of its methods have been deprecated, so Date is more often only used as a The data type is used to record the corresponding date and time information
Calender In order to make up for some defects of Date object in date and time processing methods, JAVA provides Calender abstract class to assist in the processing and calculation of some calendar date and time related to Date .
TimeZone The Timezone class provides some useful methods for getting information about the time zone

in the java.time package

JAVA8之后新增了java.time包,提供了一些与日期时间有关的新实现类:

具体每个类对应的含义说明梳理如下表:

类名 含义说明
LocalDate 获取当前的日期信息,仅有简单的日期信息,不包含具体时间、不包含时区信息。
LocalTime 获取当前的时间信息,仅有简单的时间信息,不含具体的日期、时区信息。
LocalDateTime 可以看做是LocalDate和LocalTime的组合体,其同时含有日期信息与时间信息,但是依旧不包含任何时区信息。
OffsetDateTime 在LocalDateTime基础上增加了时区偏移量信息
ZonedDateTime 在OffsetDateTime基础上,增加了时区信息
ZoneOffset 时区偏移量信息, 比如+8:00或者-5:00等
ZoneId 具体的时区信息,比如Asia/Shanghai或者America/Chicago

时间间隔计算

Period与Duration类

JAVA8开始新增的java.time包中有提供DurationPeriod两个类,用于处理日期时间间隔相关的场景,两个类的区别点如下:

描述
Duration 时间间隔,用于秒级的时间间隔计算
Period 日期间隔,用于天级别的时间间隔计算,比如年月日维度的

DurationPeriod具体使用的时候还需要有一定的甄别,因为部分的方法很容易使用中被混淆,下面分别说明下。

  • Duration

Duration的最小计数单位为纳秒,其内部使用secondsnanos两个字段来进行组合计数表示duration总长度。

Duration的常用API方法梳理如下:

方法 描述
between 计算两个时间的间隔,默认是
ofXxx of开头的一系列方法,表示基于给定的值创建一个Duration实例。比如ofHours(2L),则表示创建一个Duration对象,其值为间隔2小时
plusXxx plus开头的一系列方法,用于在现有的Duration值基础上增加对应的时间长度,比如plusDays()表示追加多少天,或者plusMinutes()表示追加多少分钟
minusXxx minus开头的一系列方法,用于在现有的Duration值基础上扣减对应的时间长度,与plusXxx相反
toXxxx to开头的一系列方法,用于将当前Duration对象转换为对应单位的long型数据,比如toDays()表示将当前的时间间隔的值,转换为相差多少天,而toHours()则标识转换为相差多少小时。
getSeconds 获取当前Duration对象对应的秒数, 与toXxx方法类似,只是因为Duration使用秒作为计数单位,所以直接通过get方法即可获取到值,而toDays()是需要通过将秒数转为天数换算之后返回结果,所以提供的方法命名上会有些许差异。
getNano 获取当前Duration对应的纳秒数“零头”。注意这里与toNanos()不一样,toNanos是Duration值的纳秒单位总长度,getNano()只是获取不满1s剩余的那个零头,以纳秒表示。
isNegative 检查Duration实例是否小于0,若小于0返回true, 若大于等于0返回false
isZero 用于判断当前的时间间隔值是否为0 ,比如比较两个时间是否一致,可以通过between计算出Duration值,然后通过isZero判断是否没有差值。
withSeconds 对现有的Duration对象的nanos零头值不变的情况下,变更seconds部分的值,然后返回一个新的Duration对象
withNanos 对现有的Duration对象的seconds值不变的情况下,变更nanos部分的值,然后返回一个新的Duration对象

关于Duration的主要API的使用,参见如下示意:


public void testDuration() {
    LocalTime target = LocalTime.parse("00:02:35.700");
    // 获取当前日期,此处为了保证后续结果固定,注掉自动获取当前日期,指定固定日期
    // LocalDate today = LocalDate.now();
    LocalTime today = LocalTime.parse("12:12:25.600");
    // 输出:12:12:25.600
    System.out.println(today);
    // 输出:00:02:35.700
    System.out.println(target);
    Duration duration = Duration.between(target, today);
    // 输出:PT12H9M49.9S
    System.out.println(duration);
    // 输出:43789
    System.out.println(duration.getSeconds());
    // 输出:900000000
    System.out.println(duration.getNano());
    // 输出:729
    System.out.println(duration.toMinutes());
    // 输出:PT42H9M49.9S
    System.out.println(duration.plusHours(30L));
    // 输出:PT15.9S
    System.out.println(duration.withSeconds(15L));
}

  • Period

Period相关接口与Duration类似,其计数的最小单位是,看下Period内部时间段记录采用了年、月、日三个field来记录:

常用的API方法列举如下:

方法 描述
between 计算两个日期之间的时间间隔。注意,这里只能计算出相差几年几个月几天
ofXxx of()或者以of开头的一系列static方法,用于基于传入的参数构造出一个新的Period对象
withXxx with开头的方法,比如withYearswithMonthswithDays等方法,用于对现有的Period对象中对应的年、月、日等字段值进行修改(只修改对应的字段,比如withYears方法,只修改year,保留month和day不变),并生成一个新的Period对象
getXxx 读取Period中对应的yearmonthday字段的值。注意下,这里是仅get其中的一个字段值,而非整改Period的不同单位维度的总值。
plusXxx 对指定的字段进行追加数值操作
minusXxx 对指定的字段进行扣减数值操作
isNegative 检查Period实例是否小于0,若小于0返回true, 若大于等于0返回false
isZero 用于判断当前的时间间隔值是否为0 ,比如比较两个时间是否一致,可以通过between计算出Period值,然后通过isZero判断是否没有差值。

关于Period的主要API的使用,参见如下示意:


public void calculateDurationDays() {
    LocalDate target = LocalDate.parse("2021-07-11");
    // 获取当前日期,此处为了保证后续结果固定,注掉自动获取当前日期,指定固定日期
    // LocalDate today = LocalDate.now();
    LocalDate today = LocalDate.parse("2022-07-08");
    // 输出:2022-07-08
    System.out.println(today);
    // 输出:2021-07-11
    System.out.println(target);
    Period period = Period.between(target, today);
    // 输出:P11M27D, 表示11个月27天
    System.out.println(period);
    // 输出:0, 因为period值为11月27天,即year字段为0
    System.out.println(period.getYears());
    // 输出:11, 因为period值为11月27天,即month字段为11
    System.out.println(period.getMonths());
    // 输出:27, 因为period值为11月27天,即days字段为27
    System.out.println(period.getDays());
    // 输出:P14M27D, 因为period为11月27天,加上3月,变成14月27天
    System.out.println(period.plusMonths(3L));
    // 输出:P11M15D,因为period为11月27天,仅将days值设置为15,则变为11月15天
    System.out.println(period.withDays(15));
    // 输出:P2Y3M44D
    System.out.println(Period.of(2, 3, 44));
}

Duration与Period踩坑记

Duration与Period都是用于日期之间的计算操作。Duration主要用于秒、纳秒等维度的数据处理与计算。Period主要用于计算年、月、日等维度的数据处理与计算

先看个例子,计算两个日期相差的天数,使用Duration的时候:


public void calculateDurationDays(String targetDate) {
    LocalDate target = LocalDate.parse(targetDate);
    LocalDate today = LocalDate.now();
    System.out.println("today : " + today);
    System.out.println("target: " + target);
    long days = Duration.between(target, today).abs().toDays();
    System.out.println("相差:"  + days + "天");
}

运行后会报错:


today : 2022-07-07
target: 2022-07-11
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
	at java.time.LocalDate.until(LocalDate.java:1614)
	at java.time.Duration.between(Duration.java:475)
	at com.veezean.demo5.DateService.calculateDurationDays(DateService.java:24)

点击看下Duration.between源码,可以看到注释上明确有标注着,这个方法是用于秒级的时间段间隔计算,而我们这里传入的是两个级别的数据,所以就不支持此类型运算,然后抛异常了。

再看下使用Period的实现:


public void calculateDurationDays(String targetDate) {
    LocalDate target = LocalDate.parse(targetDate);
    LocalDate today = LocalDate.now();
    System.out.println("today : " + today);
    System.out.println("target: " + target);
    // 注意,此处写法错误!这里容易踩坑:
    long days = Math.abs(Period.between(target, today).getDays());
    System.out.println("相差:"  + days + "天");
}

执行结果:

today : 2022-07-07
target: 2021-07-07
相差:0天

执行是不报错,但是结果明显是错误的。这是因为getDays()并不会将Period值换算为天数,而是单独计算年、月、日,此处只是返回天数这个单独的值。

再看下面的写法:


public void calculateDurationDays(String targetDate) {
    LocalDate target = LocalDate.parse(targetDate);
    LocalDate today = LocalDate.now();
    System.out.println("today : " + today);
    System.out.println("target: " + target);
    Period between = Period.between(target, today);
    System.out.println("相差:"
            + Math.abs(between.getYears()) + "年"
            + Math.abs(between.getMonths()) + "月"
            + Math.abs(between.getDays()) + "天");
}

结果为:


today : 2022-07-07
target: 2021-07-11
相差:0年11月26天

所以说,如果想要计算两个日期之间相差的绝对天数,用Period不是一个好的思路

计算日期差

  • 通过LocalDate来计算

LocalDate中的toEpocDay可返回当前时间距离原点时间之间的天数,可以基于这一点,来实现计算两个日期之间相差的天数:

代码如下:


public void calculateDurationDays(String targetDate) {
    LocalDate target = LocalDate.parse(targetDate);
    LocalDate today = LocalDate.now();
    System.out.println("today : " + today);
    System.out.println("target: " + target);
    long days = Math.abs(target.toEpochDay() - today.toEpochDay());
    System.out.println("相差:" + days + "天");
}

结果为:


today : 2022-07-07
target: 2021-07-11
相差:361天

  • 通过时间戳来计算

如果是使用的Date对象,则可以通过将Date日期转换为毫秒时间戳的方式相减然后将毫秒数转为天数的方式来得到结果。需要注意的是通过毫秒数计算日期天数的差值时,需要屏蔽掉时分秒带来的误差影响


public void calculateDaysGap(Date start, Date end) {
    final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
    // 此处要注意,去掉时分秒的差值影响,此处采用先换算为天再相减的方式
    long gapDays = Math.abs(end.getTime()/ONE_DAY_MILLIS - start.getTime()/ONE_DAY_MILLIS);
    System.out.println(gapDays);
}

输出结果:


today : 2022-07-08
target: 2021-07-11
相差:362天

  • 数学逻辑计算

分别算出年、月、日差值,然后根据是否闰年、每月是30还是31天等计数逻辑,纯数学硬怼方式计算。

不推荐、代码略...

计算接口处理耗时

在一些性能优化的场景中,我们需要获取到方法处理的执行耗时,很多人都是这么写的:


public void doSomething() {
    // 记录开始时间戳
    long startMillis = System.currentTimeMillis();
    // do something ...
    
    // 计算结束时间戳
    long endMillis = System.currentTimeMillis();
    
    // 计算相差的毫秒数
    System.out.println(endMillis - startMillis);
}

当然啦,如果你使用的是JDK8+的版本,你还可以这么写:


public void doSomething() {
    // 记录开始时间戳
    Instant start = Instant.now();
    // do something ...

    // 计算结束时间戳
    Instant end = Instant.now();

    // 计算相差的毫秒数
    System.out.println(Duration.between(start, end).toMillis());
}

时间格式转换

项目中,时间格式转换是一个非常典型的日期处理操作,可能会涉及到将一个字符串日期转换为JAVA对象,或者是将一个JAVA日期对象转换为指定格式的字符串日期时间。

SimpleDataFormat实现

在JAVA8之前,通常会使用SimpleDateFormat类来处理日期与字符串之间的相互转换:


public void testDateFormatter() {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 日期转字符串
    String format = simpleDateFormat.format(new Date());
    System.out.println("当前时间:" + format);
   
    try {
        // 字符串转日期
        Date parseDate = simpleDateFormat.parse("2022-07-08 06:19:27");
        System.out.println("转换后Date对象: " + parseDate);
        // 按照指定的时区进行转换,可以对比下前面转换后的结果,会发现不一样
        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+5:00"));
        parseDate = simpleDateFormat.parse("2022-07-08 06:19:27");
        System.out.println("指定时区转换后Date对象: " + parseDate);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

输出结果如下:


当前时间:2022-07-08 06:25:31
转换后Date对象: Fri Jul 08 06:19:27 CST 2022
指定时区转换后Date对象: Fri Jul 08 09:19:27 CST 2022

补充说明:

SimpleDateFormat对象是非线程安全的,所以项目中在封装为工具方法使用的时候需要特别留意,最好结合ThreadLocal来适应在多线程场景的正确使用。 JAVA8之后,推荐使用DateTimeFormat替代SimpleDateFormat。

DataTimeFormatter实现

JAVA8开始提供DataTimeFormatter作为新的用于日期与字符串之间转换的类,它很好的解决了SimpleDateFormat多线程的弊端,也可以更方便的与java.time中心的日期时间相关类的集成调用。


public void testDateFormatter() {
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    LocalDateTime localDateTime = LocalDateTime.now();
    // 格式化为字符串
    String format = localDateTime.format(dateTimeFormatter);
    System.out.println("当前时间:" + format);
    // 字符串转Date
    LocalDateTime parse = LocalDateTime.parse("2022-07-08 06:19:27", dateTimeFormatter);
    Date date = Date.from(parse.atZone(ZoneId.systemDefault()).toInstant());
    System.out.println("转换后Date对象: " + date);
}


输出结果:


当前时间:2022-07-08 18:37:46
转换后Date对象: Fri Jul 08 06:19:27 CST 2022

日期时间格式模板

对于计算机而言,时间处理的时候按照基于时间原点的数字进行处理即可,但是转为人类方便识别的场景显示时,经常会需要转换为不同的日期时间显示格式,比如:


2022-07-08 12:02:34
2022/07/08 12:02:34.238
2022年07月08日 12点03分48秒

在JAVA中,为了方便各种格式转换,提供了基于时间模板进行转换的实现能力:

时间格式模板中的字幕含义说明如下:

字母 使用说明
yyyy 4位数的年份
yy 显示2位数的年份,比如2022年,则显示为22年
MM 显示2位数的月份,不满2位数的,前面补0,比如7月份显示07月
M 月份,不满2位的月份不会补0
dd 天, 如果1位数的天数,则补0
d 天,不满2位数字的,不补0
HH 24小时制的时间显示,小时数,两位数,不满2位数字的前面补0
H 24小时制的时间显示,小时数,不满2位数字的不补0
hh 12小时制的时间显示,小时数,两位数,不满2位数字的前面补0
ss 秒数,不满2位的前面补0
s 秒数,不满2位的不补0
SSS 毫秒数
z 时区名称,比如北京时间东八区,则显示CST
Z 时区偏移信息,比如北京时间东八区,则显示+0800

消失的8小时问题

日期字符串存入DB后差8小时

在后端与数据库交互的时候,可能会遇到一个问题,就是往DB中存储了一个时间字段之后,后面再查询的时候,就会发现时间数值差了8个小时,这个需要在DB的连接信息中指定下时区信息:


spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai

界面时间与后台时间差8小时

在有一些前后端交互的项目中,可能会遇到一个问题,就是前端选择并保存了一个时间信息,再查询的时候就会发现与设置的时间差了8个小时,这个其实就是后端时区转换设置的问题。

SpringBoot的配置文件中,需要指定时间字符串转换的时区信息:


spring.jackson.time-zone=GMT+8

这样从接口json中传递过来的时间信息,jackson框架可以根据对应时区转换为正确的Date数据进行处理。


我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。


我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

Guess you like

Origin juejin.im/post/7117957247052808199