Java日期时间类及计算详解

1. Java中与日期相关的类

1.1 java.util包

类名 具体描述
Date Date对象算是JAVA中历史比较悠久的用于处理日期、时间相关的类了,但是随着版本的迭代演进,其中的众多方法都已经被弃用,所以Date更多的时候仅被用来做一个数据类型使用,用于记录对应的日期与时间信息
Calender 为了弥补Date对象在日期时间处理方法上的一些缺陷,JAVA提供了Calender抽象类来辅助实现Date相关的一些日历日期时间的处理与计算
TimeZone Timezone类提供了一些有用的方法用于获取时区的相关信息

① Date类

    @Test
    void test06(){
        Date date1 = new Date();
        // 获取当前时间后 +100 ms时间
        Date date2 = new Date(System.currentTimeMillis() + 100);
        System.out.println(date1);
        System.out.println(date1.compareTo(date2));
        System.out.println(date1.before(date2));
    }

结果:

Fri Jul 22 15:31:16 CST 2022
-1
true

② Calendar 日历类

总体来说,Date是一个设计相当糟糕的类,因此Java官方推荐尽量少用Date的构造器和方法

如果需要对日期、时间进行加减运算,或获取指定时间的年、月、日、时、分、秒信息,可使用Calendar工具类。

示例:

    @Test
    void test05(){
        Calendar calendar =  Calendar.getInstance();
        // Calendar.YEAR 表示当前年
        int year = calendar.get(Calendar.YEAR);
        // Calendar.MONTH表示月份,但是为了计算方便,是从0开始算,所以显示出来是月份 -1 的
        int month = calendar.get(Calendar.MONTH);
        // Calendar.DAY_OF_MONTH 在这个月 的这一天
        int dom = calendar.get(Calendar.DAY_OF_MONTH);
        // Calendar.DAY_OF_YEAR 在这一年 的这一天
        int doy = calendar.get(Calendar.DAY_OF_YEAR);
        // Calendar.DAY_OF_WEEK 在这一周 的这一天,从星期日当第一天从1开始算的,所以会是 +1
        int dow = calendar.get(Calendar.DAY_OF_WEEK);
        // Calendar.DAY_OF_WEEK_IN_MONTH 在这一个月 这一天在 第几周
        int dowim = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
        System.out.println(year+"年"+ month+"月");
        System.out.println(dom+"日");
        System.out.println(doy+"日");
        System.out.println(dow+"日");
        System.out.println(dowim);
    }

结果:

2022年6月20日11时8分19秒859毫秒
AM_PM: 0
HOUR: 11
DAY_OF_MONTH: 20日
DAY_OF_YEAR: 201日
DAY_OF_WEEK: 4日
DAY_OF_WEEK_IN_MONTH: 3

  • Calendar.DAY_OF_MONTH 在这个月 的这一天,但是为了计算方便,是从0开始算,所以显示出来是月份 -1 的
  • Calendar.DAY_OF_YEAR 在这一年 的这一天
  • Calendar.DAY_OF_WEEK 在这一周 的这一天,从星期日当第一天从1开始算的,所以会是 +1
  • Calendar.DAY_OF_WEEK_IN_MONTH 在这一个月 这一天在 第几周
  • Calendar.HOUR 表示今天这一天的小时(0-11),分上午和下午

具体可以看Calendar的静态属性,不需要刻意记

常用api:

Calendar类提供了大量访问、修改日期时间的方法 ,常用方法如下:

方法 描述
void add(int field, int amount) 根据日历的规则,为给定的日历字段添加或减去指定的时间量。
int get(int field) 返回指定日历字段的值。
int getActualMaximum(int field) 返回指定日历字段可能拥有的最大值。例如月,最大值为11。
int getActualMinimum(int field) 返回指定日历字段可能拥有的最小值。例如月,最小值为0。
void roll(int field, int amount) 与add()方法类似,区别在于加上 amount后超过了该字段所能表示的最大范围时,也不会向上一个字段进位。
void set(int field, int value) 将给定的日历字段设置为给定值。
void set(int year, int month, int date) 设置Calendar对象的年、月、日三个字段的值。
void set(int year, int month, int date, int hourOfDay, int minute, int second) 设置Calendar对象的年、月、日、时、分、秒6个字段的值。

上面的很多方法都需要一个int类型的field参数, field是Calendar类的类变量,如 Calendar.YEAR、Calendar.MONTH等分别代表了年、月、日、小时、分钟、秒等时间字段。**需要指出的是, Calendar.MONTH字段代表月份,月份的起始值不是1,而是O,所以要设置8月时,用7而不是8。**如上面演示的程序就示范了Calendar类的常规用法。

add和roll的区别

add

add(int field, int amount)的功能非常强大,add主要用于改变Calendar的特定字段的值。

  • 如果需要增加某字段的值,则让 amount为正数;
  • 如果需要减少某字段的值,则让 amount为负数即可。

add(int field, int amount)有如下两条规则:

  • 当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大。
  • 如果下一级字段也需要改变,那么该字段会修正到变化最小的值。
    @Test
    void test07(){
        Calendar cal1 = Calendar.getInstance();
        // 2003-8-23
        cal1.set(2003, 7, 23, 0, 0, 0);
        // 2003-8-23 => 2004-2-23
        cal1.add(Calendar.MONTH, 6);
        System.out.println(cal1.getTime());

        Calendar cal2 = Calendar.getInstance();
        // 2003-8-31
        cal2.set(2003, 7, 31, 0, 0, 0);
        // 因为进位后月份改为2月,2月没有31日,自动变成29日,若不是闰年则变成28日
        // 2003-8-31 => 2004-2-29
        cal2.add(Calendar.MONTH, 6);
        System.out.println(cal2.getTime());
    }

对于上面的例子,8-31就会变成2-29。**因为MONTH 的下一级字段是DATE,从31到29改变最小(若不是闰年则变成28日)。**所以上面2003-8-31的MONTH字段增加6后,不是变成2004-3-2,而是变成2004-2-29。

结果:

Mon Feb 23 00:00:00 CST 2004
Sun Feb 29 00:00:00 CST 2004

roll

roll()的规则与add()的处理规则不同—— 当被修改的字段超出它允许的范围时,上一级字段不会增大。

    @Test
    void test08(){
        Calendar cal1 = Calendar.getInstance();
        // 2003-8-23
        cal1.set(2003, 7, 23, 0, 0, 0);
        // 2003-8-23 => 2003-2-23
        cal1.roll(Calendar.MONTH, 6);
        System.out.println(cal1.getTime());

        Calendar cal2 = Calendar.getInstance();
        cal2.set(2003, 7, 31, 0, 0, 0);
        // MONTH字段“进位”后变成2,2月没有31日
        // YEAR字段不会改变,2003年2月只有28天
        // 2003-8-31 => 2003-2-28
        cal2.roll(Calendar.MONTH, 6);
        System.out.println(cal2.getTime());
    }

结果:

Sun Feb 23 00:00:00 CST 2003
Fri Feb 28 00:00:00 CST 2003

设置Calendar的容错性

调用Calendar对象的set()方法来改变指定时间字段的值时,有可能传入一个不合法的参数,例如为MONTH字段设置13,这将会导致怎样的后果呢?看如下程序:

    @Test
    void test09(){
        Calendar cal = Calendar.getInstance();
        System.out.println(cal.getTime());
        // ① 结果是Year字段+1,MONTH字段为1(2月)
        cal.set(Calendar.MONTH, 13);
        System.out.println(cal.getTime());
        // 关闭容错性
        cal.setLenient(false);
        // ② 导致运行异常
        cal.set(Calendar.MONTH, 13);
        System.out.println(cal.getTime());
    }

上面程序①②两处的代码完全相似,但它们运行的结果不一样:

  • ①处代码可以正常运行,因为设置MONTH字段的值为13,将会导致YEAR字段加1;
  • ②处代码将会导致运行时异常,因为设置的MONTH字段值超出了MONTH字段允许的范围。

关键在于程序中粗体字代码行,Calendar提供了一个setLenient()用于设置它的容错性,Calendar默认支持较好的容错性,通过 setLenient(false)可以关闭Calendar的容错性,让它进行严格的参数检查。

Calendar有两种解释日历字段的模式:lenient模式和non-lIenient模式:

  • 当Calendar 处于lenient模式时,每个时间字段可接受超出它允许范围的值;
  • 当Calendar 处于 non-lenient模式时,如果为某个时间字段设置的值超出了它允许的取值范围,程序将会抛出异常。

set

set()方法延迟修改 :set(f, value)方法将日历字段f更改为value,此外它还设置了一个内部成员变量,以指示日历字段f已经被更改。

尽管日历字段f是立即更改的,但该Calendar所代表的时间却不会立即修改,直到下次调用get()、getTime()、getTimeInMillis()、add()或roll()时才会重新计算日历的时间。

这被称为 set()方法的延迟修改,采用延迟修改的优势是多次调用set()不会触发多次不必要的计算(需要计算出一个代表实际时间的long型整数)。

    @Test
    void test10(){
        Calendar cal = Calendar.getInstance();
        // 2003-8-31
        cal.set(2003, 7, 31);
        cal.set(Calendar.MONTH, 8);

        // ① 将月份设置为9月,但是9月没有31号,如果立即修改,系统会把cal自动调整为10月1日
        // System.out.println(cal.getTime());

        // 设置DATE字段为5
        cal.set(Calendar.DATE, 5);
        // 输出结果为 2003-9-5
        System.out.println(cal.getTime());
    }

结果

Fri Sep 05 16:59:50 CST 2003

如果程序将①处代码注释起来,因为Calendar的 set()方法具有延迟修改的特性,即调用set()方法后Calendar实际上并未计算真实的日期,它只是使用内部成员变量表记录MONTH字段被修改为8,接着程序设置DATE字段值为5,程序内部再次记录DATE字段为5——就是9月5日,因此最后输出2003-9-5。

1.2 java.time包

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

Java日期时间类及计算详解_第1张图片

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

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

① LocalDate 本地日期类

LocalDate localDate = LocalDate.now();
// 也可以通过 LocalDate.of(年,月,日)去构造
System.out.println("当前日期:"+localDate.getYear()+" 年 "+localDate.getMonthValue()+" 月 "+localDate.getDayOfMonth()+"日" );

// 计算
LocalDate pluslocalDate = localDate.plusDays(1);//增加一天
LocalDate pluslocalDate = localDate.plusYears(1);//增加一年

// 对两个日期的判断,是在前、在后、或者相等。
LocalDate.isBefore(LocalDate);
LocalDate.isAfter();
LocalDate.isEqual();
//结果

② LocalTime 本地时间类

LocalDate pluslocalDate = localDate.plusDays(1);//增加一天
LocalDate pluslocalDate = localDate.plusYears(1);//增加一年

LocalDate和LocalTime 都有类似作用的api

LocalDate.plusDays(1) 增加一天

LocalTime.plusHours(1) 增加一小时 等等~

LocalTime.isBefore(LocalTime);

LocalTime.isAfter();

③ LocalDateTime 本地日期时间类

public final class LocalDateTime ...{
    private final LocalDate date;
    private final LocalTime time;
}

LocalDateTime = LocalDate + LocalTime

④ Instant 类

Instant 是瞬间,某一时刻的意思

Instant.ofEpochMilli(System.currentTimeMillis())
Instant.now()

通过Instant可以创建一个 “瞬间” 对象,ofEpochMilli()可以接受某一个“瞬间”,比如当前时间,或者是过去、将来的一个时间。 比如,通过一个“瞬间”创建一个LocalDateTime对象

LocalDateTime now = LocalDateTime.ofInstant(
    Instant.ofEpochMilli(System.currentTimeMillis()),ZoneId.systemDefault());

System.out.println("当前日期:"+now.getYear()+" 年 "+now.getMonthValue()+" 月 "+now.getDayOfMonth()+"日" );

⑤ Period 类

Period 是 时期,一段时间 的意思

Period有个between方法专门比较两个日期的

LocalDate startDate = LocalDateTime.ofInstant(
    Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault())
						.toLocalDate();//1601175465000是2020-9-27 10:57:45
Period p  =  Period.between(startDate,  LocalDate.now());

System.out.println("目标日期距离今天的时间差:"+p.getYears()+" 年 "+p.getMonths()+" 个月 "+p.getDays()+" 天" );

//目标日期距离今天的时间差:1 年 1 个月 1 天

查看between源码:

public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) {
    return startDateInclusive.until(endDateExclusive);
}
public Period until(ChronoLocalDate endDateExclusive) {
    LocalDate end = LocalDate.from(endDateExclusive);
    long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();  // safe
    int days = end.day - this.day;
    if (totalMonths > 0 && days < 0) {
        totalMonths--;
        LocalDate calcDate = this.plusMonths(totalMonths);
        days = (int) (end.toEpochDay() - calcDate.toEpochDay());  // safe
    } else if (totalMonths < 0 && days > 0) {
        totalMonths++;
        days -= end.lengthOfMonth();
    }
    long years = totalMonths / 12;  // safe
    int months = (int) (totalMonths % 12);  // safe
    return Period.of(Math.toIntExact(years), months, days);
}

只接受两个LocalDate对象,对时间的计算,算好之后返回Period对象

⑥ Duration 类

Duration 是期间持续时间的意思

示例代码:

LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault());
LocalDateTime start = LocalDateTime.ofInstant(Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault());
Duration duration = Duration.between(start, end);

System.out.println("开始时间到结束时间,持续了"+duration.toDays()+"天");
System.out.println("开始时间到结束时间,持续了"+duration.toHours()+"小时");
System.out.println("开始时间到结束时间,持续了"+duration.toMillis()/1000+"秒");

可以看到between也接受两个参数,LocalDateTime对象,源码是对两个时间的计算,并返回对象。

2. 时间间隔计算

2.1 Period与Duration类

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

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

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

2.1.1 Duration

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

Java日期时间类及计算详解_第2张图片

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的使用,参见如下示意:

@Test
    void durationTEst(){
        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));

    }

2.1.2 Period

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

Java日期时间类及计算详解_第3张图片

常用的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的使用,参见如下示意:

    @Test
    void periodTest(){
        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));

    }

2.2 Duration与Period的坑

Duration与Period都是用于日期之间的计算操作。

  • Duration主要用于秒、纳秒等维度的数据处理与计算。
  • Period主要用于计算年、月、日等维度的数据处理与计算

Duration的坑

先看个例子,计算两个日期相差的天数,使用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源码,可以看到注释上明确有标注着,这个方法是用于秒级的时间段间隔计算,而我们这里传入的是两个级别的数据,所以就不支持此类型运算,然后抛异常了。

Java日期时间类及计算详解_第4张图片

Period的坑

同样是计算两个日期相差的天数,再看下使用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不是一个好的思路

2.3 计算日期差

2.3.1 通过LocalDate来计算

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

Java日期时间类及计算详解_第5张图片

代码如下:

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天

2.3.2 通过时间戳来计算

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

数学逻辑计算(不推荐)

分别算出年、月、日差值,然后根据是否闰年、每月是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());
}

2.4 计算时间差

使用Hutool工具进行计算

一款超厉害的国产Java工具——Hutool。Hutool是一个Java工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类。适用于很多项目以及Web开发,并且与其他框架没有耦合性。

引入依赖:

        
        
            com.xiaoleilu
            hutool-all
            3.3.2
        

封装时间类进行计算

制作Calendar工具类计算:

基于Calendar对时间计算进行相应的封装处理,如下面两个例子,可以根据需求将相关的计算封装在一个Util工具类中

获取本周开始时间戳

/**
 * start
     * 本周开始时间戳
 */
public static Date getWeekStartTime() {
    Calendar calendar = Calendar.getInstance();
    int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1;
    if (dayOfWeek == 0){
        dayOfWeek = 7;
    }
    calendar.add(Calendar.DATE, - dayOfWeek + 1);

    calendar.set(Calendar.HOUR_OF_DAY, 0);
    //将分钟至0
    calendar.set(Calendar.MINUTE, 0);
    //将秒至0
    calendar.set(Calendar.SECOND, 0);
    //将毫秒至0
    calendar.set(Calendar.MILLISECOND, 0);
    return calendar.getTime();
}

根据日期和天数进行计算

    /**
         * 获取当前时间的月几号0点时间或第二天0时间戳(即几号的24点)
     * @param calendar 当前时间对象
     * @param day 几号, 值范围 是1 到 当前时间月天数 + 1 整数, 
         *  传入(day+1)为day号的第二天0点时间(day号的24点时间),
         *  如果值为当前时间月天数+1则结果为当前月的下个月1号0点(即当月最后一天的24点),
         *  如果当前月的天数为31天, 传入32时则为当前月的下个月1号0点(即当月最后一天的24点)
     * @return
     */
	public static Date getDayOfMonthStartOrEndTime(Calendar calendar, int day) {
	    Calendar calendarTemp = Calendar.getInstance();
	    calendarTemp.setTime(calendar.getTime());
	    int days = getDaysOfMonth(calendarTemp);
	    int limitDays = days + 1;
	    if (day > limitDays) {
	    	calendarTemp.set(Calendar.DAY_OF_MONTH, limitDays);
		} else {
			if (day >= 1) {
				calendarTemp.set(Calendar.DAY_OF_MONTH, day);
			} else {
				calendarTemp.set(Calendar.DAY_OF_MONTH, 1);
			}
		}
	    //将小时至0
	    calendarTemp.set(Calendar.HOUR_OF_DAY, 0);
	    //将分钟至0
	    calendarTemp.set(Calendar.MINUTE, 0);
	    //将秒至0
	    calendarTemp.set(Calendar.SECOND, 0);
	    //将毫秒至0
	    calendarTemp.set(Calendar.MILLISECOND, 0);

	    //获得当前月几号0点或几号的第二天0点(即几号的24点)
	    Date startTime = calendarTemp.getTime();
	   return startTime;
	}

制作Date工具类计算

Java项目开发中常见的日期操作工具类封装:代码如下(示例):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Time;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.WeekFields;
import java.util.*;
/**
 * @author hhlm
 * @description: Java日期类型相关操作类;该类负责对日期格式化转换、日期比较、日期加减、润年判断、获 
 * 取相关的日期信息等。
 *  * @date 2022年03月02日 16:24 
 */
public class DateUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(DateUtil.class);
    public static final long ONE_MINUTE_MILLISECOND = 60 * 1000L;
    public static final long ONE_HOUR_MILLISECOND = 60 * ONE_MINUTE_MILLISECOND;
    /**
     * 一天所对应的毫秒数
     */
    public static final long ONE_DAY_MILLISECOND = 24 * ONE_HOUR_MILLISECOND;
    /**
     * 一周所对应的毫秒数
     */
    public static final long ONE_WEEK_MILLISECOND = 7 * ONE_DAY_MILLISECOND;
    public static final long ONE_MONTH_MILLISECOND = 30 * ONE_DAY_MILLISECOND;
    public static final long ONE_YEAR_MILLISECOND = 365 * ONE_DAY_MILLISECOND;
    private static String defaultDatePattern = null;
    /**
     * 从配置文件中返回配置项"date.format",默认的日期格式符 (yyyy-MM-dd),
     *
     * @return a string representing the date pattern on the UI
     */
    public static synchronized String getDatePattern() {
        defaultDatePattern = "yyyy-MM-dd";
        return defaultDatePattern;
    }
    /**
     * 校验日期入参是否正确,如防止sql注入
     * @author linjx 2018-12-24
     * @param desc 入参的描述
     * @param format
     * @param dateStr notEmptyString
     * @return
     */
    public static Date validDate(String desc, String format, String dateStr) {
        LOGGER.debug("validDate.desc={}, format={}, dateStr={}", desc, format, dateStr);
        Date parse = DateUtil2.parse(dateStr, format, desc);
        /**
            format='yyyy-MM-dd',dateStr='2019-12-15' or '1'='1'是不会有ParseException的
            所以需要将parse重新格式化成字符串,和dateStr比较
         */
        AssertApp.isTrue(dateStr.equals(DateUtil2.format(parse, format)), desc + "异常");
        return parse;
    }
    /**
     * 获取日期的年份
     *
     * @return 日期的年份
     */
    public static int getYear(Date date) {
        return getCalendar(date).get(Calendar.YEAR);
    }
    /**
     * 获取日期的月份(0-11)
     *
     * @param date
     * @return 日期的月份(0-11)
     */
    public static int getMonth(Date date) {
        return getCalendar(date).get(Calendar.MONTH);
    }
    /**
     * 获取日期的一个月中的某天
     *
     * @param date
     * @return 日期的一个月中的某天(1-31)
     */
    public static int getDay(Date date) {
        return getCalendar(date).get(Calendar.DATE);
    }
    /**
     * 获取日期的一个星期中的某天
     *
     * @param date
     * @return 日期的星期中日期(1:sunday-7:SATURDAY)
     */
    public static int getWeek(Date date) {
        return getCalendar(date).get(Calendar.DAY_OF_WEEK);
    }
    /**
     * 将日期字符串按指定的格式转为Date类型
     *
     * @param strDate 待解析的日期字符串
     * @param format  日期格式
     * @return 字符串对应的日期对象
     */
    public static final Date parseDate(String strDate, String format) {
        return DateUtil2.parse(strDate, format, strDate);
    }
    /**
     * 将日期字符串按系统配置中指定默认格式(getDatePattern()返回的格式)转为Date类型
     *
     * @param strDate 待解析的日期字符串
     * @return 字符串对应的日期对象
     */
    public static Date parseDate(String strDate) {
        return parseDate(strDate, getDatePattern());
    }
    /**
     * 

检查所给的年份是否是闰年

* * @param year 年 * @return 检查结果: true - 是闰年; false - 是平年 */ public static boolean isLeapYear(int year) { if (year / 4 * 4 != year) { return false; //不能被4整除 } else if (year / 100 * 100 != year) { return true; //能被4整除,不能被100整除 } else if (year / 400 * 400 != year) { return false; //能被100整除,不能被400整除 } else { return true; //能被400整除 } } /** * 按照默认格式化样式格式化当前系统时间 * * @return 日期字符串 */ public static String getCurrentTime() { return formatDate(new Date()); } /** * 按照默认格式化样式格式化当前系统时间 * * @param format String 日期格式化标准 * @return String 日期字符串。 */ public static String getCurrentTime(String format) { return formatDate(new Date(), format); } /** * 按照指定格式化样式格式化指定的日期 * * @param date 待格式化的日期 * @param format 日期格式 * @return 日期字符串 */ public static String formatDate(Date date, String format) { if (date == null) return ""; if (format == null) format = getDatePattern(); SimpleDateFormat formatter = new SimpleDateFormat(format); return formatter.format(date); } /** * 按照默认格式化样式格式化指定的日期 * * @param date 待格式化的日期 * @return 日期字符串 */ public static String formatDate(Date date) { long offset = System.currentTimeMillis() - date.getTime(); String pos = "前"; if (offset < 0) { pos = "后"; offset = -offset; } if (offset >= ONE_YEAR_MILLISECOND) return formatDate(date, getDatePattern()); StringBuilder sb = new StringBuilder(); if (offset >= 2 * ONE_MONTH_MILLISECOND) { return sb.append((offset + ONE_MONTH_MILLISECOND / 2) / ONE_MONTH_MILLISECOND).append("个月").append(pos).toString(); } if (offset > ONE_WEEK_MILLISECOND) { return sb.append((offset + ONE_WEEK_MILLISECOND / 2) / ONE_WEEK_MILLISECOND).append("周").append(pos).toString(); } if (offset > ONE_DAY_MILLISECOND) { return sb.append((offset + ONE_DAY_MILLISECOND / 2) / ONE_DAY_MILLISECOND).append("天").append(pos).toString(); } if (offset > ONE_HOUR_MILLISECOND) { return sb.append((offset + ONE_HOUR_MILLISECOND / 2) / ONE_HOUR_MILLISECOND).append("小时").append(pos).toString(); } if (offset > ONE_MINUTE_MILLISECOND) { return sb.append((offset + ONE_MINUTE_MILLISECOND / 2) / ONE_MINUTE_MILLISECOND).append("分钟").append(pos).toString(); } return sb.append(offset / 1000).append("秒").append(pos).toString(); } /** * 将date的时间部分清零 * * @param day * @return 返回Day将时间部分清零后对应日期 */ public static Date getCleanDay(Date day) { return getCleanDay(getCalendar(day)); } /** * 设置当天最后时间 * * @param day * @return 返回当天最后时间 */ public static Date getEndDay(Date day) { Calendar c = Calendar.getInstance(); c.setTime(day); c.set(Calendar.HOUR_OF_DAY, 23); c.set(Calendar.MINUTE, 59); c.set(Calendar.SECOND, 59); c.set(Calendar.MILLISECOND, 999); return c.getTime(); } /** * 获取day对应的Calendar对象 * * @param day * @return 返回date对应的Calendar对象 */ public static Calendar getCalendar(Date day) { Calendar c = Calendar.getInstance(); if (day != null) c.setTime(day); return c; } public static Date getCleanDay(Calendar c) { c.set(Calendar.HOUR_OF_DAY, 0); c.clear(Calendar.MINUTE); c.clear(Calendar.SECOND); c.clear(Calendar.MILLISECOND); return c.getTime(); } /** * 根据year,month,day构造日期对象 * * @param year 年份(4位长格式) * @param month 月份(1-12) * @param day 天(1-31) * @return 日期对象 */ public static Date makeDate(int year, int month, int day) { Calendar c = Calendar.getInstance(); getCleanDay(c); c.set(Calendar.YEAR, year); c.set(Calendar.MONTH, month - 1); c.set(Calendar.DAY_OF_MONTH, day); return c.getTime(); } private static Date getFirstCleanDay(int datePart, Date date) { Calendar c = Calendar.getInstance(); if (date != null) c.setTime(date); c.set(datePart, 1); return getCleanDay(c); } /** * Calendar.YEAR :1则代表的是对年份操作, * Calendar.MONTH :2是对月份操作; * Calendar.DATE : 5是对日期操作; * @param datePart * @param detal * @param date * @return */ public static Date add(int datePart, int detal, Date date) { Calendar c = Calendar.getInstance(); if (date != null) c.setTime(date); c.add(datePart, detal); return c.getTime(); } /** * 日期date所在星期的第一天00:00:00对应日期对象 * * @param date * @return 日期所在星期的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfWeek(Date date) { return getFirstCleanDay(Calendar.DAY_OF_WEEK, date); } /** * 当前日期所在星期的第一天00:00:00对应日期对象 * * @return 当前日期所在星期的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfWeek() { return getFirstDayOfWeek(null); } /** * 日期date所在月份的第一天00:00:00对应日期对象 * * @param date * @return 日期所在月份的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfMonth(Date date) { return getFirstCleanDay(Calendar.DAY_OF_MONTH, date); } /** * 当前日期所在月份的第一天00:00:00对应日期对象 * * @return 当前日期所在月份的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfMonth() { return getFirstDayOfMonth(null); } /** * 日期date所在月份的最后一天23, 59, 59对应日期对象 * * @param date * @return 日期date所在月份的最后一天23, 59, 59对应日期对象 */ public static Date getLastDayOfMonth(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); int MaxDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH); cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), MaxDay, 23, 59, 59); return cal.getTime(); } /** * 日期date所在季度的第一天00:00:00对应日期对象 * * @param date * @return 日期date所在季度的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfSeason(Date date) { Date d = getFirstDayOfMonth(date); int delta = DateUtil.getMonth(d) % 3; if (delta > 0) d = DateUtil.getDateAfterMonths(d, -delta); return d; } /** * 当前日期所在季度的第一天00:00:00对应日期对象 * * @return 当前日期所在季度的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfSeason() { return getFirstDayOfMonth(null); } /** * 日期date所在年份的第一天00:00:00对应日期对象 * * @param date * @return 日期date所在年份的第一天00:00:00对应日期对象 */ public static Date getFirstDayOfYear(Date date) { return makeDate(getYear(date), 1, 1); } /** * 获取某年的第一天 * @param year * @return 日期date所在年份的第一天00:00:00对应日期对象 */ public static Date getFirstDaylOfYear(int year){ return makeDate(year, 1, 1); } /** * 当前日期年度的第一天00:00:00对应日期对象 * * @return 当前日期年度第一天00:00:00对应日期对象 */ public static Date getFirstDayOfYear() { return getFirstDayOfYear(new Date()); } /** * 当前日期年度的最后一天23:59:59对应日期对象 * * @return 当前日期年度的最后一天23:59:59对应日期对象 */ public static Date getLastDayOfYear() { return parseDate(getYear(new Date()) + "-12-31 23:59:59", "yyyy-MM-dd HH:mm:ss"); } /** * 获取某年最后一天 * @return 传入年份的最后一天23:59:59对应日期对象 */ public static Date getLastDayOfYear(int year){ return DateUtil2.parse(year + "-12-31 23:59:59", "yyyy-MM-dd HH:mm:ss", ""); } /** * 计算N周后的日期 * * @param start 开始日期 * @param weeks 可以为负,表示前N周 * @return 新的日期 */ public static Date getDateAfterWeeks(Date start, int weeks) { return getDateAfterMs(start, weeks * ONE_WEEK_MILLISECOND); } /** * 计算N月后的日期, 特殊情况:如果是'2016-1-31'一个月后是 '2017-2-28' * * @param start 开始日期 * @param months 可以为负,表示前N月 * @return 新的日期 */ public static Date getDateAfterMonths(Date start, int months) { return add(Calendar.MONTH, months, start); } /** * 计算N年后的日期, 特殊情况:如果是'2016-2-29'一年后是'2017-2-28' * * @param start 开始日期 * @param years 可以为负,表示前N年 * @return 新的日期 */ public static Date getDateAfterYears(Date start, int years) { return add(Calendar.YEAR, years, start); } /** * 计算N天后的日期 * * @param start 开始日期 * @param days 可以为负,表示前N天 * @return 新的日期 */ public static Date getDateAfterDays(Date start, int days) { return getDateAfterMs(start, days * ONE_DAY_MILLISECOND); } /** * 计算N毫秒后的日期 * * @param start 开始日期 * @param ms 可以为负,表示前N毫秒 * @return 新的日期 */ public static Date getDateAfterMs(Date start, long ms) { return new Date(start.getTime() + ms); } /** * 计算2个日期之间的间隔的周期数 * * @param start 开始日期 * @param end 结束日期 * @param msPeriod 单位周期的毫秒数 * @return 周期数 */ public static long getPeriodNum(Date start, Date end, long msPeriod) { return getIntervalMs(start, end) / msPeriod; } /** * 计算2个日期之间的毫秒数 * * @param start 开始日期 * @param end 结束日期 * @return 毫秒数 */ public static long getIntervalMs(Date start, Date end) { return end.getTime() - start.getTime(); } /** * 计算2个日期之间的天数 * * @param start 开始日期 * @param end 结束日期 * @return 天数 */ public static int getIntervalDays(Date start, Date end) { return (int) getPeriodNum(start, end, ONE_DAY_MILLISECOND); } /** * 计算2个日期之间的周数 * * @param start 开始日期 * @param end 结束日期 * @return 周数 */ public static int getIntervalWeeks(Date start, Date end) { return (int) getPeriodNum(start, end, ONE_WEEK_MILLISECOND); } /** * 比较日期前后关系 * * @param base 基准日期 * @param date 待比较的日期 * @return 如果date在base之前或相等返回true,否则返回false */ public static boolean before(Date base, Date date) { return date.before(base) || date.equals(base); } /** * 比较日期前后关系 * * @param base 基准日期 * @param date 待比较的日期 * @return 如果date在base之后或相等返回true,否则返回false */ public static boolean after(Date base, Date date) { return date.after(base) || date.equals(base); } /** * 返回对应毫秒数大的日期 * * @param date1 * @param date2 * @return 返回对应毫秒数大的日期 */ public static Date max(Date date1, Date date2) { if (date1.getTime() > date2.getTime()) return date1; else return date2; } /** * 返回对应毫秒数小的日期 * * @param date1 * @param date2 * @return 返回对应毫秒数小的日期 */ public static Date min(Date date1, Date date2) { if (date1.getTime() < date2.getTime()) return date1; else return date2; } /** * 判断date是否在指定的时期范围(start~end)内 * * @param start 时期开始日期 * @param end 时期结束日期 * @param date 待比较的日期 * @return 如果date在指定的时期范围内,返回true,否则返回false */ public static boolean inPeriod(Date start, Date end, Date date) { return (end.after(date) || end.equals(date)) && (start.before(date) || start.equals(date)); } /** * 获取当前日期是星期几
* * @param dt * @return 当前日期是星期几 */ public static String getWeekOfDate(Date dt) { String[] weekDays = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}; Calendar cal = Calendar.getInstance(); cal.setTime(dt); int w = cal.get(Calendar.DAY_OF_WEEK) - 1; if (w < 0) w = 0; return weekDays[w]; } private static final DateTimeFormatter SHORT_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter LONG_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final TemporalField CHINA_DAY_OF_WEEK = WeekFields.of(Locale.CHINA).dayOfWeek(); /** * 获取指定日期所在周的第一天 * * @param date * @return */ public static LocalDate firstDayOfWeek(TemporalAccessor date) { return LocalDate.from(date).with(CHINA_DAY_OF_WEEK, 1); } /** * 获取指定日期所在周的最后一天 * * @param date * @return */ public static LocalDate lastDayOfWeek(TemporalAccessor date) { return LocalDate.from(date).with(CHINA_DAY_OF_WEEK, 7); } /** * 获取指定日期所在月的第一天 * * @param date * @return */ public static LocalDate firstDayOfMonth(TemporalAccessor date) { return LocalDate.from(date).withDayOfMonth(1); } /** * 获取指定日期所在周的最后一天 * * @param date * @return */ public static LocalDate lastDayOfMonth(TemporalAccessor date) { return LocalDate.from(date).plusMonths(1).withDayOfMonth(1).plusDays(-1); } /** * 短日期格式 * * @param date * @return */ public static String shortString(TemporalAccessor date) { return SHORT_DATETIME_FORMATTER.format(date); } /** * 长日期格式 * * @param date * @return */ public static String longString(TemporalAccessor date) { return LONG_DATETIME_FORMATTER.format(date); } /** * 将指定定的日期转换成LocalDateTime * * @param date * @return */ public static LocalDateTime asLocalDateTime(Date date) { return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); } /** * 将指定定的日期转换成LocalDate * * @param date * @return */ public static LocalDate asLocalDate(Date date) { return asLocalDateTime(date).toLocalDate(); } /** * LocalDate 转为 Date * * @param localDate * @return */ public static Date asDate(LocalDate localDate) { return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); } /** * LocalDateTime 转为 Date * * @param localDateTime * @return */ public static Date asDate(LocalDateTime localDateTime) { return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); } /** * 按照“年-月”生成当月的工作日 * @param yearMonth [yyyy-MM] * @return */ public static List getWorkdayOfMonth(String yearMonth){ int year; int month ; List results = new ArrayList<>(); Calendar cal = Calendar.getInstance(); String[] splitStr = yearMonth.split("-"); year = Integer.parseInt(splitStr[0]); month = Integer.parseInt(splitStr[1]); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month - 1); cal.set(Calendar.DATE, 1); while(cal.get(Calendar.YEAR) == year && cal.get(Calendar.MONTH) < month){ int day = cal.get(Calendar.DAY_OF_WEEK); if(!(day == Calendar.SUNDAY || day == Calendar.SATURDAY)){ results.add((Date) cal.getTime().clone()); } cal.add(Calendar.DATE, 1); } return results; } /** * 按照年份生成一年内的工作日 * @param year * @return */ public static List getWorkdayOfYear(int year){ List results = new ArrayList<>(); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, 0); cal.set(Calendar.DATE, 1); while(cal.get(Calendar.YEAR) == year){ int day = cal.get(Calendar.DAY_OF_WEEK); if(!(day == Calendar.SUNDAY || day == Calendar.SATURDAY)){ results.add((Date) cal.getTime().clone()); } cal.add(Calendar.DATE, 1); } return results; } /** * 获取上一个月的第一天时间 */ public static Date getFirstDayOfLastMonth(){ Calendar cal = Calendar.getInstance();//获取当前日期 /* * 2018-03-31时通过测试: * 不设置cal.set(Calendar.DAY_OF_MONTH, 1)时,得到的日期是2018-02-28而不是3月 */ cal.add(Calendar.MONTH, -1); cal.set(Calendar.DAY_OF_MONTH, 1);//设置为1号,当前日期既为本月第一天 cal.set(Calendar.HOUR_OF_DAY, 0); cal.clear(Calendar.MINUTE); cal.clear(Calendar.SECOND); cal.clear(Calendar.MILLISECOND); return cal.getTime(); } /** * 获取指定日期的上月一号 * @param date * @return */ public static Date getLastMonthFirst(Date date) { Calendar c = Calendar.getInstance();//获取当前日期 c.setTime(date); c.add(Calendar.MONTH, -1); c.set(Calendar.DAY_OF_MONTH, 1);// 设置为1号 return getCleanDay(c); } /** * 获取上月月末 * @param date * @return */ public static Date getLastMonthEnd(Date date) { Calendar c = Calendar.getInstance();//获取当前日期 c.setTime(date); c.add(Calendar.MONTH, -1); int maxDay = c.getActualMaximum(Calendar.DAY_OF_MONTH); c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), maxDay, 23, 59, 59); return getCleanDay(c); } public static Date getLastMonthDay(Date date, int day) { Calendar c = Calendar.getInstance();//获取当前日期 c.setTime(date); c.add(Calendar.MONTH, -1); c.set(Calendar.DAY_OF_MONTH, day);// 设置为1号 return getCleanDay(c); } /** * 本月月末 * @param date * @return */ public static Date getMonthEnd(Date date) { Calendar c = Calendar.getInstance();//获取当前日期 c.setTime(date); int maxDay = c.getActualMaximum(Calendar.DAY_OF_MONTH); c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), maxDay, 23, 59, 59); return getCleanDay(c); } /** * 获取下月1号 * @param date * @return */ public static Date getNextMonthFirst(Date date) { Calendar c = Calendar.getInstance();//获取当前日期 c.setTime(date); c.add(Calendar.MONTH, 1); c.set(Calendar.DAY_OF_MONTH, 1);// 设置为1号 return getCleanDay(c); } /** * 获取下月 月末 * @param date * @return */ public static Date getNextMonthEnd(Date date) { Calendar c = Calendar.getInstance();//获取当前日期 c.setTime(date); c.add(Calendar.MONTH, 1); int maxDay = c.getActualMaximum(Calendar.DAY_OF_MONTH); c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), maxDay, 23, 59, 59); return getCleanDay(c); } /** * 判断两个日期之间是否为一整年 * @param start * @param end * @return */ public static boolean isOneYear(Date start, Date end) { Calendar startday = Calendar.getInstance(); Calendar endday = Calendar.getInstance(); startday.setTime(start); endday.setTime(end); if (startday.after(endday)) { return false; } long sl = startday.getTimeInMillis(); long el = endday.getTimeInMillis(); long days = ((el - sl) / (1000 * 60 * 60 * 24)); if (days == 365 || days == 366) { if (startday.get(Calendar.MONTH) <= 1) { startday.set(Calendar.MONTH, 1); int lastDay = startday.getActualMaximum(Calendar.DAY_OF_MONTH); return (lastDay == 28 && days == 365) || (lastDay == 29 && days == 366); } else { endday.set(Calendar.MONTH, 1); int lastDay = endday.getActualMaximum(Calendar.DAY_OF_MONTH); return (lastDay == 28 && days == 365) || (lastDay == 29 && days == 366); } } else { return false; } } /** * @return 上一天Date */ public static Date getPreviousDate(Date date) { Calendar c = Calendar.getInstance(); c.setTime(date); int day = c.get(Calendar.DATE); c.set(Calendar.DATE, day - 1); return c.getTime(); } /** * @return 后一天Date */ public static Date getNextDate(Date date) { Calendar c = Calendar.getInstance(); c.setTime(date); int day = c.get(Calendar.DATE); c.set(Calendar.DATE, day + 1); return c.getTime(); } /** * 判断两个时间是否在同一天 * @param date1 * @param Date2 * @return */ public static boolean inSameDay(Date date1, Date Date2) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date1); int year1 = calendar.get(Calendar.YEAR); int day1 = calendar.get(Calendar.DAY_OF_YEAR); calendar.setTime(Date2); int year2 = calendar.get(Calendar.YEAR); int day2 = calendar.get(Calendar.DAY_OF_YEAR); if ((year1 == year2) && (day1 == day2)) { return true; } return false; } /** * 传入多少分钟 * 获取两个时间的差值,有多少个小时,用于计算请了多少小时假 * 尾数不足0.5小时按0.5小时计,超过0.5小时按1小时计 * * ((int) diff / 30) 有n个半小时要转int * 最后 如果余数不为0 就要补上0.5 * return 单位:小时 */ public static double getLeaveValue(double differenceValue){ double needLeaveTs = ((int)(differenceValue / 30))*0.5; if(differenceValue % 30 != 0) needLeaveTs+= 0.5; return needLeaveTs; } /** * 传入一个日期 * 打卡开始时间在下午,开始时间与14:00上班时间请多少个小时 */ public static double getLeaveValue(Date startTime){ double hour = startTime.getHours(); double minute = startTime.getMinutes(); double needLeaveTs = ((int)(minute / 30))*0.5; if(minute % 30 != 0) needLeaveTs += 0.5; needLeaveTs += hour - 14; return needLeaveTs; } /** * 获取指定日期下午上班时间 * @param date * @return */ public static Date getGotoWorkAfternoon(Date date){ return parseDate(getYear(date) + "-"+ (getMonth(date)+1) +"-" + getDay(date)+" 14:00:00", "yyyy-MM-dd HH:mm:ss"); } /** * 根据传进来的Time和Date,获取到那天对应完成日期 * @param date * @param time * @return */ public static Date getDateByTime(Date date, Time time){ int hour = time.getHours(); int minutes = time.getMinutes(); int seconds = time.getSeconds(); String timeStr = " " + hour + ":" + minutes + ":" + seconds; return parseDate(getYear(date) + "-"+ (getMonth(date)+1) +"-" + getDay(date) + timeStr, "yyyy-MM-dd HH:mm:ss"); } /** * 获取XX:XX:XX的时间字符串 * @param t 秒 * @return [XX:XX:XX],[XX:XX] */ public static String getTimeSpanStr(int t) { StringBuilder sb = new StringBuilder(); if(t >= 3600) { int h = t / 3600; if(h < 10) sb.append("0"); sb.append(h + ":"); } if(t >= 60) { int m = t%3600/60; if(m < 10) sb.append("0"); sb.append(m + ":"); } int s = t%60; if(s < 10) sb.append("0"); sb.append(s); return sb.toString(); } /** * 根据传入日期获取目标日期 * @param date nullable 原始日期 * @param monthDiff nullable 月份偏移量 * @param dayOfMonth nullable 当月几号 * @return null if date is null */ public static Date getDiffDate(Date date, Integer monthDiff, Integer dayOfMonth) { if(date == null) return null; Calendar c = Calendar.getInstance(); c.setTime(date); if(monthDiff == null) monthDiff = 0; int month = c.get(Calendar.MONTH) + monthDiff; c.set(Calendar.MONTH, month); if(dayOfMonth != null) c.set(Calendar.DAY_OF_MONTH, dayOfMonth); return c.getTime(); } /** * 获取n天[前/后]的日期 * @return */ public static Date getDiffDay(Date date, Integer diffDay) { Calendar c = Calendar.getInstance(); c.setTime(date); c.add(Calendar.DAY_OF_YEAR, diffDay); return c.getTime(); } /** * @param d1 notNull * @param d2 notNull * @return 是否是同一个月 */ public static boolean isSameMonth(Date d1, Date d2) { Calendar c = Calendar.getInstance(); c.setTime(d1); int y1 = c.get(Calendar.YEAR); int m1 = c.get(Calendar.MONTH); c.setTime(d2); int y2 = c.get(Calendar.YEAR); int m2 = c.get(Calendar.MONTH); return y1 == y2 && m1 == m2; } /** * @param d1 notNull * @param d2 notNull * @return 是否是同一天 */ public static boolean isSameDay(Date d1, Date d2) { Calendar c = Calendar.getInstance(); c.setTime(d1); int y1 = c.get(Calendar.YEAR); int m1 = c.get(Calendar.MONTH); int day1 = c.get(Calendar.DAY_OF_MONTH); c.setTime(d2); int y2 = c.get(Calendar.YEAR); int m2 = c.get(Calendar.MONTH); int day2 = c.get(Calendar.DAY_OF_MONTH); return y1 == y2 && m1 == m2 && day1 == day2; } /** * @return d1>=d2 */ public static boolean greaterThanOrEqualTo(Date d1, Date d2) { return d1.compareTo(d2) >= 0; } /** * @return d1<=d2 */ public static boolean lessThanOrEqualTo(Date d1, Date d2) { return d1.compareTo(d2) <= 0; } /** * @return d1>d2 */ public static boolean greaterThan(Date d1, Date d2) { return d1.compareTo(d2) > 0; } /** * 获取某个月的实际最大天数, 如2016-02, 最大天数为29 */ public static int getMaximum(Date date) { Calendar c = Calendar.getInstance(); c.setTime(date); return c.getActualMaximum(Calendar.DAY_OF_MONTH); } public static Date getFirstDayOfWeek(int year, int week) { Calendar c = Calendar.getInstance(); c.set(year, Calendar.JANUARY, 1, 0, 0, 0);//定到第一天 c.add(Calendar.DATE, (week - 1) * 7);//直接add天数 c.setFirstDayOfWeek(Calendar.SUNDAY); c.setTime(c.getTime());//必须先set一次time,否则是错误的! c.set(Calendar.DAY_OF_WEEK, 1); return c.getTime(); } public static Date getHalfPastNineDateTime(Date date){ return parseDate(getYear(date) + "-"+ (getMonth(date)+1) +"-" + getDay(date)+" 9:30:00", "yyyy-MM-dd HH:mm:ss"); } public static Date getTenOClockDateTime(Date date){ return parseDate(getYear(date) + "-"+ (getMonth(date)+1) +"-" + getDay(date)+" 9:30:00", "yyyy-MM-dd HH:mm:ss")

3. 时间格式转换

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

3.1 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
  •  G 年代标志符
  •  y 年
  •  M 月
  •  d 日
  •  h 时 在上午或下午 (1~12)
  •  H 时 在一天中 (0~23)
  •  m 分
  •  s 秒
  •  S 毫秒
  •  E 星期
  •  D 一年中的第几天
  •  F 一月中第几个星期几
  •  w 一年中第几个星期
  •  W 一月中第几个星期
  •  a 上午 / 下午 标记符
  •  k 时 在一天中 (1~24)
  •  K 时 在上午或下午 (0~11)
  •  z 时区

补充说明:

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

3.2 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-19 17:19:27
转换后Date对象: Fri Jul 08 06:19:27 CST 2022

3.3 日期时间格式模板

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

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

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

Java日期时间类及计算详解_第6张图片

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

字母 使用说明
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

4. 消失的八小时问题

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

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

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

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

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

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

spring.jackson.time-zone=GMT+8

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

到此这篇关于Java日期时间类及计算详解的文章就介绍到这了,更多相关Java日期时间类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Java日期时间类及计算详解)