在 Java 中优雅地操纵时间

在开发时候,发现有很多需要用到时间的地方,例如记录操作的时间、比较时间判断产品是否有效等。总而言之,时间是我们业务开发必须关注、时刻注意的点。但目前工程的代码中使用了非常多时间的工具类,一会儿用 java.util.Date 记录时间,一会用 java.time.LocalDateTime 记录时间,怎么才能在 Java 中优雅的操纵时间呢,我整理了相关的概念和工具类,希望帮助大家在代码开发的过程中对对时间的使用更加优雅。

这里先写一个结论:

  • 建议使用 java8 的时间 API,在安全性和易用性上都远高于 java.util.Date
  • 目前比较流行的封装 java API 的时间工具类大都基于 java.util.Date,建议在开发过程中根据业务需要基于 java.time.* 的方法封装工具类(文末给出了一个简单的实现)。

时间在计算机中的存储和展示

时间以整数的方式进行存储:时间在计算机中存储的本质是一个整数,称为 Epoch Time(时间戳),计算从 1970 年 1 月 1 日零点(格林威治时间/GMT+00:00)到现在所经历的秒数。

在 java 程序中,时间戳通常使用 long 表示毫秒数,通过 System.currentTimeMillis() 可以获取时间戳。时间戳对我们人来说是不易理解的,因此需要将其转换为易读的时间,例如,2024-10-7 20:21:59(实际上说的是本地时间),而同一时刻不同时区的人看到的本地时间是不一样,所以在时间展示的时候需要加上时区的信息,才能精准的找到对应的时刻。

时区与世界时间标准相关:

时区与世界时间标准相关

世界时间的标准在 1972 年发生了变化,但我们在开发程序的时候可以忽略 GMTUTC 的差异, 因为计算机的时钟在联网的时候会自动与时间服务器同步时间。 本地时间等于我们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC)之间的偏移量来定义。这个偏移量可以表示为 UTC-UTC+,后面接上偏移的小时和分钟数。 例如:GMT+08:00 或者 UTC+08:00 表示东八区,2024-10-7 20:21:59 UTC+08:00 便可以精准的定位一个时刻。

日期 API

JDK 以版本 8 为界,有两套处理日期/时间的 API。

Java 时间 API

简单的比较如下:

特性 java.util.Date java.util.Date.Calendar java.time.LocalDateTime
线程安全
时间运算
可读性 Tue Oct 08 00:11:16 CST 2024 易读性较低 ❌不易读 ✅ yyyy-MM-dd’T’HH:mm:ss
常量设计 需要对获取的年份(+1900)月份(0-11)进行处理 需要对获月份(0-11)进行处理 ✅ 不需要额外处理,符合常识
时间精度 精确到毫秒 精确到毫秒 精确到纳秒
时区 具体的时间调用 -
特性 java.text.SimpleDateFormat java.time.DateTimeFormatter
线程安全 ❌ 在多线程环境下每个线程独立维护一份 SimpleDateFormat 对象实例,或者将 SimpleDateFormat 放到 ThreadLocal ✅ 不变对象,线程安全,可以使用单例存储
使用场景 Date LocalDateTime

java.util

在 jdk8 之前,Java 使用 java.util 中的 API 对处理时间。 在获取年月日的时候,DateCalendar 需要进行不同的转换 => 规则不统一。

Date

java.util.Date 用于表示一个日期和时间的对象,其实现很简单,实际上存储了一个 long 类型的以毫秒表示的时间戳,在通过 new Date() 获取当前时间的时候,实际上是通过 System.currentTimeMillis() 获取时间戳进行赋值。

public class Date {
    long fastTime;

    public Date(long date) {
        fastTime = date;
    }

    public long getTime() {
        return fastTime;
    }
}

java.util.Date 承载的功能有限,且在利用 Date 类获取具体年/月/日的时候需要注意:getYear() 返回的年份必须加上 1900getMonth() 返回的月份是 0-11 分别表示 1-12 月,所以要加 1,而 getDate() 返回的日期范围是 1~31,又不能加 1

Calendar

Calendar 可以用于获取并设置年、月、日、时、分、秒,它和 Date 比,主要多了一个可以做简单的日期和时间运算的功能,但代码粗糙,API 不好用,性能也不好。

Calendar 对象 getTime() 可以获得 Date 对象:


import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 获取当前时间:
        Calendar c = Calendar.getInstance();
        int y = c.get(Calendar.YEAR);//返回年份不用转换
        int m = 1 + c.get(Calendar.MONTH);//返回月份需要加1
        int d = c.get(Calendar.DAY_OF_MONTH);
        int w = c.get(Calendar.DAY_OF_WEEK);//返回的
        int hh = c.get(Calendar.HOUR_OF_DAY);
        int mm = c.get(Calendar.MINUTE);
        int ss = c.get(Calendar.SECOND);
        int ms = c.get(Calendar.MILLISECOND);
        System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);
    }
}

import java.text.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 当前时间:
        Calendar c = Calendar.getInstance();
        // 清除所有:
        c.clear();
        // 设置年月日时分秒:
        c.set(2019, 10 /* 11月 */, 20, 8, 15, 0);
        // 加5天并减去2小时:
        c.add(Calendar.DAY_OF_MONTH, 5);
        c.add(Calendar.HOUR_OF_DAY, -2);
        // 显示时间:
        var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d = c.getTime();
        System.out.println(sdf.format(d));
        // 2019-11-25 6:15:00
    }
}

TimeZone

CalendarDate 相比,它提供了时区转换的功能。时区用 TimeZone 对象表示。

时区的唯一标识是以字符串表示的 ID。获取指定 TimeZone 对象也是以这个 ID 为参数获取,GMT+09:00Asia/Shanghai 都是有效的时区 ID。可以通过 TimeZone.getAvailableIDs() 获取系统支持的所有 ID


import java.text.*;
import java.util.*;

public class learnTime {
    public static void main(String[] args) {
        // 当前时间:
        Calendar c = Calendar.getInstance();
        // 清除所有字段:
        c.clear();
        // 设置为北京时区:
        c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
        // 设置年月日时分秒:
        c.set(2024, 9 /* 10月 */, 10, 8, 15, 0);
        // 显示时间:
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(sdf.format(c.getTime()));
        // 2024-10-09 20:15:00
    }
}

java.text.SimpleDateFormat

Date 使用 SimpleDateFormat 解析和格式化时间:


// SimpleDateFormat线程不安全,每次使用都要构造新的,在初始的时候定义解析的字符串格式
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 将指定字符串String解析为Date
Date date = format.parse("2024-10-07 16:10:22");

// 将Date格式化为String
String str = format.format(date);

由于 SimpleDateFormat 线程不安全,为了提升性能,可以使用 ThreadLocalCache

如下:

static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_LOCAL 
    = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);

Java.time.*

开源社区开发了一个日期库 Joda,API 清晰,性能较好,提交了 JSR-310,在 java8 中称为 JDK 基础类库。

  • 本地日期和时间:LocalDateTime(日期和时间),LocalDate(日期),LocalTime(时间)(因为没有时区,所以无法与时间戳转换);
  • 带时区的日期和时间:ZonedDateTime
  • 时刻:Instant
  • 时区:ZoneIdZoneOffset
  • 时间间隔:Duration

以及一套新的用于取代 SimpleDateFormat 的格式化类型 DateTimeFormatter

LocalDate/LocalTime/LocalDateTime

  • 默认严格按照 ISO 8601 规定日期和时间格式进行打印(日期和时间的分隔符是 T)。

    • 日期:yyyy-MM-dd; 时间 HH:mm:ss
    • 日期和时间:yyyy-MM-dd'T'HH:mm:ss
  • 可以解析简单格式获取类型:

    LocalDateTime localDayTime=LocalDateTime.of(2024, 10, 07, 8, 15, 0);
    LocalDate localDay=LocalDate.of(2024, 10, 07); 
    LocalTime localTime=LocalTime.parse("08:15:07");
    
  • 有对日期和时间进行加减的非常简单的链式调用,通过 plusXxx()/minusXxx() 对时间进行变换:

    public class learnTime {
        public static void main(String[] args) {
            LocalDateTime dt = LocalDateTime.of(2024, 10, 10, 20, 30, 59);
            System.out.println(dt);
            // 加5天减3小时:2024-10-10T20:30:59
            LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
            System.out.println(dt2); // 2024-10-15T17:30:59
            // 减1月:
            LocalDateTime dt3 = dt2.minusMonths(1); //2024-09-15T17:30:59
            System.out.println(dt3); // 2019-09-30T17:30:59
        }
    }
    
  • 对日期和时间进行调整使用 withXxx(),例如将月份调整为 9月: dataLocalTime.withMonth(9)

  • 复杂的操作:获取特殊时间

    • withTemporalAdjusters 配合使用找到特殊时间(当月的第一天)。

      public class Main {
          public static void main(String[] args) {
              LocalDateTime now = LocalDateTime.now();
      
              // 获取本月第一天0:00时刻:
              System.out.println("当月第一天0:00时刻"+now.withDayOfMonth(1).atStartOfDay());
              //获取当月第一天
              System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));
              //获取下月第一天
              System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth()));
              //获取明年第一天
              System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear()));
              //获取本年第一天
              System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));
              //获取当月最后一天
              System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth()));
              //获取本年最后一天
              System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear()));
              //获取当月第三周星期五
              System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY)));
              //获取上周一
              System.out.println("上周一:"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));
              //获取下周日
              System.out.println("下周日:"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));
      
          }
      }
      
  • 比较可以使用 isBefore()isAfter()

Duration 和 Period

  • Duration: 基于时间值(Instant/LocalDateTime),表示两个时刻时间的时间间隔,适合处理较短的时间,需要更高的精确性。
    • 使用 between() 方法比较两个瞬间的差;
    • 使用 getSeconds()getNanosecends() 方法获取时间单元的值;
    • 获得具体的粒度的间隔:ofDays()ofHours()ofMillis()ofMinutes()ofNanos()ofSeconds()
    • 通过文本创建 Duration 对象,格式为 “PnDTnHnMn.nS”,Duration.parse("P1DT1H10M10.5S")
    • 使用 toDays()toHours()toMillis()toMinutes() 方法把 Duration 对象可以转成其他时间单元;
    • 通过 plusX()minusX() 方法增加或减少 Duration 对象,其中 X 表示 days, hours, millis, minutes, nanosseconds
  • Period 基于日期值,表示一段时间的年、月、日:
    • 使用 between() 方法比较两个日期的差;
    • 使用 getYears()getMonhs()getDays() 方法获取具体粒度差距(返回的类型是 int);
    • 通过文本创建 Period 对象,格式为 “PnYnMnD”:Period.parse("P2Y3M5D")
    • 可以通过 plusX()minusX() 方法进行增加或减少,其中 X 表示日期单元;

ZonedDateTime

ZonedDateTimeLocalDateTimeZoneId

  • ZonedDateTime 带时区时间的常见方法:

    • now():获取当前时区的ZonedDateTime对象。
    • now(ZoneId zone):获取指定时区的 ZonedDateTime 对象。
    • getYeargetMonthValuegetDayOfMonth 等:获取年月日、时分秒、纳秒等。
    • withXxx(时间):修改时间系列的方法。
    • minusXxx(时间):减少时间系列的方法。
    • plusXxx(时间):增加时间系列的方法。
  • 时区转换

    import java.time.*;
    
    public class Main {
        public static void main(String[] args) {
            // 以中国时区获取当前时间:
            ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
            // 转换为纽约时间:
            ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
            System.out.println(zbj);
            System.out.println(zny);
        }
    }
    

ZoneId

时区类,功能和 java.util.TimeZone 类似。

ZoneId 支持两种类型格式初始化,一种是时区偏移的格式(基于 UTC/Greenwich 时),一种是地域时区的格式(eg:Europe/Paris)。ZoneId 是抽象类,具体的逻辑实现由来子类完成,ZoneOffset 处理时区偏移类型的格式,ZoneRegion 处理基于地域时区的格式:

  • getAvailableZoneIds():获取Java中支持的所有时区。
  • systemDefault():获取系统默认时区。
  • of(String zoneId):获取一个指定时区。
格式 描述 示例
Z, GMT, UTC, UT 格林尼治标准时间,和中国相差8个小时 ZoneId.of("Z");
+h +hh +hh:mm -hh:mm +hhmm -hhmm +hh:mm:ss -hh:mm:ss +hhmmss -hhmmss 表示从格林尼治标准时间偏移时间,中国用+8表示 ZoneId.of("+8");
前缀:UTC+, UTC-, GMT+, GMT-, UT+ UT-, 后缀:-h +hh +hh:mm -hh:mm… 表示从格林尼治标准时间偏移时间 ZoneId.of("UTC+8");
Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, Africa/Nairobi, America/Marigot… 地区表示法,这些ID必须包含在getAvailableZoneIds集合中,否则会抛出异常 ZoneId.of("Asia/Shanghai");

Instant

时间线上的某个时刻/时间戳

通过获取 Instant 的对象可以拿到此刻的时间,该时间由两部分组成:从 1970-01-01 00:00:00 开始走到此刻的总秒数+不够 1 秒的纳秒数。

  • 作用:可以用来记录代码的执行时间,或用于记录用户操作某个事件的时间点。
  • 传统的 Date 类,只能精确到毫秒,并且是可变对象。
  • 新增的 Instant 类,可以精确到纳秒,并且是不可变对象,推荐用 Instant 代替 Date
//1、创建Instant的对象,获取此刻时间信息
Instant now = Instant.now(); //不可变对象
//2、获取总秒数
long second = now.getEpochSecond();
system.out.println(second) ;
//3、不够1秒的纳秒数
int nano = now.getNano();
system.out.println(nano) ;

system.out.println(now);
//可以进行加减法 
Instant instant = now.plusNanos(111);//将纳秒加111

// Instant对象的作用:做代码的性能分析,或者记录用户的操作时间点
Instant now1 = Instant.now();
//代码执行...
Instant now2 = Instant.now();
//用这两个时间点相减就可以知道这段代码运行了多少时间

DateTimeFormatter

使用方式,传入格式化字符串,可以指定 local


import java.time.*;
import java.time.format.*;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
        System.out.println(formatter.format(zdt));

        DateTimeFormatter zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
        System.out.println(zhFormatter.format(zdt));

        DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
        System.out.println(usFormatter.format(zdt));

        //2024-10-08T00:25 GMT+08:00
        //2024 十月 08 星期二 00:25
        //Tue, October/08/2024 00:25
    }
}

转换

LocalTimeTimeDate 的相互转换

LocalDateTime 不包括时区,而 Date 代表一个具体的时间瞬间,精度为毫秒。

为了从 LocalDateTime 转换到 Date 需要提供时区。


// LocalDateTime 转换为 Date
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
Date date = Date.from(zonedDateTime.toInstant());
// Date 转换为 LocalDateTime
Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();

数据库映射变化

  • java.util.Date 和数据库映射

    <arg column="gmt_create" jdbcType="TIMESTAMP" javaType="java.util.Date"/>
    
  • java.time.* 和数据库映射

    <arg column="gmt_create" jdbcType="TIMESTAMP" javaType="java.time.LocalDateTime"/>
    
    • mybatis 3.5.0 以后已经支持,有 LocalDateTimeTypeHandler 等类型处理器支持,不需要额外操作。

    • 比较老的 mybatis 版本可能会报错,需要添加相关的依赖。

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-typehandlers-jsr310</artifactId>
          <version>1.0.2</version>
      </dependency>
      

Mybatis 中和时间相关的 jdbcType 和 javaTypetypeHandler 的对照关系如下:

TypeHandler Java类型 JDBC类型
DateTypeHandler java.util.Date TIMESTAMP
DateOnlyTypeHandler java.util.Date DATE
TimeOnlyTypeHandler java.util.Date TIME
InstantTypeHandler java.time.Instant TIMESTAMP
LocalDateTimeTypeHandler java.time.LocalDateTime TIMESTAMP
LocalDateTypeHandler java.time.LocalDate DATE
LocalTimeTypeHandler java.time.LocalTime TIME
OffsetDateTimeTypeHandler java.time.OffsetDateTime TIMESTAMP
OffsetTimeTypeHandler java.time.OffsetTime TIME
ZonedDateTimeTypeHandler java.time.ZonedDateTime TIMESTAMP
YearTypeHandler java.time.Year INTEGER
MonthTypeHandler java.time.Month INTEGER
YearMonthTypeHandler java.time.YearMonth VARCHAR 或 LONGVARCHAR
JapaneseDateTypeHandler java.time.chrono.JapaneseDate DATE

操作时间相关的工具

有一些对基础的API进行了封装便于我们在开发中有效的处理时间。

  • 蚂蚁时间工具类:com.iwallet.biz.common.util.DateUtil
    • 基于 java.Util.Date,提供了广泛的日期/时间处理方法,可满足绝大部分需求。
  • org.apache.commons.lang3.time
    • 包括多种基于 java.util.Date 封装的工具类,提供了很多方便操作日期和时间的算法。

目前暂时没有发现基于 java.time* 封装的公共的时间工具类。

在很多情况下,因为已有的工具类不能满足当下的业务需求,工程内部需要自己实现类似 DateUtil 的工具类,建议基于 java.time* 实现相关的工具类。


import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class DateUtils {

    // 获取当前日期
    public static LocalDate getCurrentDate() {
        return LocalDate.now();
    }

    // 获取当前时间
    public static LocalTime getCurrentTime() {
        return LocalTime.now();
    }

    // 获取当前日期时间
    public static LocalDateTime getCurrentDateTime() {
        return LocalDateTime.now();
    }

    // 格式化日期为字符串
    public static String formatLocalDate(LocalDate date, String pattern) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        return date.format(formatter);
    }

    // 解析字符串为LocalDate
    public static LocalDate parseLocalDate(String dateStr, String pattern) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        return LocalDate.parse(dateStr, formatter);
    }

    // 增加指定天数
    public static LocalDate addDays(LocalDate date, long days) {
        return date.plusDays(days);
    }

    // 减少指定天数
    public static LocalDate minusDays(LocalDate date, long days) {
        return date.minusDays(days);
    }

    // 计算两个日期之间的天数差
    public static long getDaysBetween(LocalDate startDate, LocalDate endDate) {
        return ChronoUnit.DAYS.between(startDate, endDate);
    }

    // 获取指定日期所在月份的第一天
    public static LocalDate getFirstDayOfMonth(LocalDate date) {
        return date.withDayOfMonth(1);
    }

    // 获取指定日期所在月份的最后一天
    public static LocalDate getLastDayOfMonth(LocalDate date) {
        return date.withDayOfMonth(date.lengthOfMonth());
    }

    // 判断两个日期是否相等
    public static boolean isSameDate(LocalDate date1, LocalDate date2) {
        return date1.isEqual(date2);
    }

    // 判断日期是否在指定范围内
    public static boolean isDateInRange(LocalDate date, LocalDate startDate, LocalDate endDate) {
        return date.isAfter(startDate) && date.isBefore(endDate);
    }

    // 获取指定日期的星期几
    public static DayOfWeek getDayOfWeek(LocalDate date) {
        return date.getDayOfWeek();
    }

    // 判断是否为闰年
    public static boolean isLeapYear(int year) {
        return Year.of(year).isLeap();
    }

    // 获取指定月份的天数
    public static int getDaysInMonth(int year, int month) {
        return YearMonth.of(year, month).lengthOfMonth();
    }

    // 获取指定日期的年份
    public static int getYear(LocalDate date) {
        return date.getYear();
    }

    // 获取指定日期的月份
    public static int getMonth(LocalDate date) {
        return date.getMonthValue();
    }

    // 获取指定日期的天数
    public static int getDayOfMonth(LocalDate date) {
        return date.getDayOfMonth();
    }

    // 获取指定日期的小时数
    public static int getHour(LocalDateTime dateTime) {
        return dateTime.getHour();
    }

    // 获取指定日期的分钟数
    public static int getMinute(LocalDateTime dateTime) {
        return dateTime.getMinute();
    }

    // 获取指定日期的秒数
    public static int getSecond(LocalDateTime dateTime) {
        return dateTime.getSecond();
    }

    // 判断指定日期是否在当前日期之前
    public static boolean isBefore(LocalDate date) {
        return date.isBefore(LocalDate.now());
    }

    // 判断指定日期是否在当前日期之后
    public static boolean isAfter(LocalDate date) {
        return date.isAfter(LocalDate.now());
    }

    // 判断指定日期是否在当前日期之前或相等
    public static boolean isBeforeOrEqual(LocalDate date) {
        return date.isBefore(LocalDate.now()) || date.isEqual(LocalDate.now());
    }

    // 判断指定日期是否在当前日期之后或相等
    public static boolean isAfterOrEqual(LocalDate date) {
        return date.isAfter(LocalDate.now()) || date.isEqual(LocalDate.now());
    }

    // 获取指定日期的年龄
    public static int getAge(LocalDate birthDate) {
        LocalDate currentDate = LocalDate.now();
        return Period.between(birthDate, currentDate).getYears();
    }

    // 获取指定日期的季度
    public static int getQuarter(LocalDate date) {
        return (date.getMonthValue() - 1) / 3 + 1;
    }

    // 获取指定日期的下一个工作日
    public static LocalDate getNextWorkingDay(LocalDate date) {
        do {
            date = date.plusDays(1);
        } while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY);
        return date;
    }

    // 获取指定日期的上一个工作日
    public static LocalDate getPreviousWorkingDay(LocalDate date) {
        do {
            date = date.minusDays(1);
        } while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY);
        return date;
    }

    // 获取指定日期所在周的第一天(周一)
    public static LocalDate getFirstDayOfWeek(LocalDate date) {
        return date.with(DayOfWeek.MONDAY);
    }

    // 获取指定日期所在周的最后一天(周日)
    public static LocalDate getLastDayOfWeek(LocalDate date) {
        return date.with(DayOfWeek.SUNDAY);
    }

    // 获取指定日期所在年的第一天
    public static LocalDate getFirstDayOfYear(LocalDate date) {
        return date.withDayOfYear(1);
    }

    // 获取指定日期所在年的最后一天
    public static LocalDate getLastDayOfYear(LocalDate date) {
        return date.withDayOfYear(date.lengthOfYear());
    }

    // 获取指定日期所在季度的第一天
    public static LocalDate getFirstDayOfQuarter(LocalDate date) {
        int month = (date.getMonthValue() - 1) / 3 * 3 + 1;
        return LocalDate.of(date.getYear(), month, 1);
    }

    // 获取指定日期所在季度的最后一天
    public static LocalDate getLastDayOfQuarter(LocalDate date) {
        int month = (date.getMonthValue() - 1) / 3 * 3 + 3;
        return LocalDate.of(date.getYear(), month, Month.of(month).maxLength());
    }

    // 判断指定日期是否为工作日(周一至周五)
    public static boolean isWeekday(LocalDate date) {
        return date.getDayOfWeek() != DayOfWeek.SATURDAY && date.getDayOfWeek() != DayOfWeek.SUNDAY;
    }

    // 判断指定日期是否为周末(周六或周日)
    public static boolean isWeekend(LocalDate date) {
        return date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
    }

    // 获取指定日期所在月份的工作日天数
    public static int getWeekdayCountOfMonth(LocalDate date) {
        int weekdayCount = 0;
        LocalDate firstDayOfMonth = getFirstDayOfMonth(date);
        LocalDate lastDayOfMonth = getLastDayOfMonth(date);

        while (!firstDayOfMonth.isAfter(lastDayOfMonth)) {
            if (isWeekday(firstDayOfMonth)) {
                weekdayCount++;
            }
            firstDayOfMonth = firstDayOfMonth.plusDays(1);
        }

        return weekdayCount;
    }

    // 获取指定日期所在月份的周末天数
    public static int getWeekendCountOfMonth(LocalDate date) {
        int weekendCount = 0;
        LocalDate firstDayOfMonth = getFirstDayOfMonth(date);
        LocalDate lastDayOfMonth = getLastDayOfMonth(date);

        while (!firstDayOfMonth.isAfter(lastDayOfMonth)) {
            if (isWeekend(firstDayOfMonth)) {
                weekendCount++;
            }
            firstDayOfMonth = firstDayOfMonth.plusDays(1);
        }

        return weekendCount;
    }

    // 获取指定日期所在年份的工作日天数
    public static int getWeekdayCountOfYear(LocalDate date) {
        int weekdayCount = 0;
        LocalDate firstDayOfYear = getFirstDayOfYear(date);
        LocalDate lastDayOfYear = getLastDayOfYear(date);

        while (!firstDayOfYear.isAfter(lastDayOfYear)) {
            if (isWeekday(firstDayOfYear)) {
                weekdayCount++;
            }
            firstDayOfYear = firstDayOfYear.plusDays(1);
        }

        return weekdayCount;
    }

}

Ref:https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247542060&idx=1&sn=ebde870557f2f3002dacef8a43e04bfd