Java日期时间类及计算

Java日期时间类及计算1. Java中与日期相关的类 1.1 java.util包 类名 具体描述 Date Date对象算是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);
    }

结果

202262011819859毫秒
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为负数即可。

具体的field操作可以看:Calendar的add()方法介绍

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

  1. 当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大。
  2. 如果下一级字段也需要改变,那么该字段会修正到变化最小的值。
    @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包,提供了一些与日期时间有关的新实现类:

image-20220719143026579

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

类名 含义说明
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();
//结果
当前日期:20211027

② 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总长度。

image-20220719150424650

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来记录:

image-20220719150733431

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

image-20220719162111331

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可返回当前时间距离原点时间之间的天数,可以基于这一点,来实现计算两个日期之间相差的天数:

image-20220719163529109

代码如下:

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 计算时间差

1️⃣ 使用Hutool工具进行计算

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

工具使用说明——Hutool 指南 API

引入依赖

        <!-- https://mvnrepository.com/artifact/com.xiaoleilu/hutool-all -->
        <dependency>
            <groupId>com.xiaoleilu</groupId>
            <artifactId>hutool-all</artifactId>
            <version>3.3.2</version>
        </dependency>

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项目开发中常见的日期操作工具类封装——DateUtil

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
202207月08日 120348

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

image-20220719172300651

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

字母 使用说明
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日期时间类及计算分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/13804.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注