当前位置: 代码网 > it编程>编程语言>Java > Java如何精确计算N个工作日后的日期

Java如何精确计算N个工作日后的日期

2026年08月08日 Java 我要评论
为什么要计算工作日后的日期?在电商、金融、oa 系统开发中,我们经常需要计算若干个工作日(跳过周末和法定节假日)之后的日期。典型场景包括:工单 sla:承诺 5 个工作日内响应。付款到期日:合同约定

为什么要计算工作日后的日期?

在电商、金融、oa 系统开发中,我们经常需要计算若干个工作日(跳过周末和法定节假日)之后的日期。典型场景包括:

  • 工单 sla:承诺 5 个工作日内响应。
  • 付款到期日:合同约定 3 个工作日内付款。
  • 定时任务调度:只允许在交易日执行。

但问题在于:

  • 周末固定(周六、日)容易处理。
  • 法定节假日每年不同,需查阅公告。
  • 调休补班(如国庆前后周六上班)让简单的“周六日休息”规则失效。

因此,数据 + 算法缺一不可。本文提供一套基于 java 8 java.time.localdate 的完整解决方案,核心算法采用“逐日递增 + 动态懒加载”策略,支持任意跨年(哪怕 workdays=300 也不怕),并附赠 datelocaldate 互转工具,完美兼容老旧系统。

核心算法思路(简单粗暴,但有效)

算法本质是“一天一天地数”,直到数满指定的工作日数量。

判定逻辑(优先级从高到低)

对于每一天,我们根据它的“日期属性”判断是否为工作日:

日期类型是否为工作日?
周一 ~ 周五默认是工作日,但如果该日期在“法定节假日黑名单”中,则不是工作日。
周六 ~ 周日默认不是工作日,但如果该日期在“调休补班白名单”中,则是工作日(即调休上班)。

算法伪代码

输入:起始日期 startdate, 工作日数 workdays
输出:目标日期

current = startdate
已累计工作日 = 0

while (已累计工作日 < workdays) {
    current = current + 1天
    获取 current 的日期属性(是否周末、是否节假日、是否调休补班)
    if (current 是工作日) {
        已累计工作日 += 1
    }
}
return current

这个算法简单、准确,且逐天递增的性能开销完全可以接受(即使 workdays=10000,也只是万次循环)。

核心优化:动态懒加载(彻底解决跨年)

很多初学者会在代码中写:

if (startdate.getmonth() == december && startdate.getday() > 20) {
    // 加载下一年的数据
}

这种写法有两大致命缺陷:

  1. 如果 workdays 较大(比如从 6 月 1 日开始计算 30 个工作日),最终日期可能跨到次年,但上述条件不触发。
  2. 如果 workdays 极大(例如 300),甚至会跨到第三年,只加载下一年显然不够。

正确做法在循环中按需加载

维护一个 map<integer, set<string>> 作为缓存,当 current 进入一个新的年份时,立即加载该年的节假日/补班数据。这样无论 workdays 多大,都能自动加载所有涉及的年份。

方案一:对接第三方节假日 api(零运维,开箱即用)

对于初创项目或不想维护数据表的场景,直接使用免费 api 是最快捷的方式。

这里以 apizero 为例(无需注册,每日 1000 次免费调用):

get https://v1.apizero.cn/api/holiday?year=2026

返回 json 示例(字段说明):

{
  "code": 200,
  "data": {
    "list": [
      {
        "holiday": "2026-10-01",           // 法定假日(单日)
        "vacation": "2026-10-01~2026-10-07", // 假期范围(或单日)
        "remark": "2026-10-10"              // 调休补班日期(周六/日上班)
      }
    ]
  }
}

maven 依赖(用于解析 json)

<dependency>
    <groupid>org.json</groupid>
    <artifactid>json</artifactid>
    <version>20230227</version>
</dependency>

完整 java 实现(包含动态懒加载)

import org.json.jsonarray;
import org.json.jsonobject;

import java.io.bufferedreader;
import java.io.inputstreamreader;
import java.net.httpurlconnection;
import java.net.url;
import java.time.localdate;
import java.time.format.datetimeformatter;
import java.util.*;

/**
 * 工作日计算器(基于第三方 api,自动跨年)
 */
public class workdaycalculatorbyapi {

    private static final string api_url = "https://v1.apizero.cn/api/holiday";
    private static final datetimeformatter fmt = datetimeformatter.ofpattern("yyyy-mm-dd");

    // 懒加载缓存:key=年份, value=该年的日期字符串集合(yyyy-mm-dd)
    private static final map<integer, set<string>> holiday_cache = new hashmap<>();
    private static final map<integer, set<string>> work_weekend_cache = new hashmap<>();

    /**
     * 计算从 startdate 开始,经过 workdays 个工作日后的日期
     *
     * @param startdate 起始日期(当天不算,从第二天开始)
     * @param workdays  需要累计的工作日数(正数)
     * @return 目标日期
     */
    public static localdate getdateafterworkdays(localdate startdate, int workdays) throws exception {
        localdate current = startdate;
        int added = 0;

        while (added < workdays) {
            current = current.plusdays(1);
            int year = current.getyear();

            // 动态懒加载:当年数据尚未加载,则调用 api 拉取并放入缓存
            if (!holiday_cache.containskey(year)) {
                holiday_cache.put(year, fetchholidays(year));
                work_weekend_cache.put(year, fetchworkweekends(year));
            }

            string datestr = current.format(fmt);
            boolean isweekend = current.getdayofweek().getvalue() >= 6; // 6=周六, 7=周日
            boolean isholiday = holiday_cache.get(year).contains(datestr);
            boolean isworkweekend = work_weekend_cache.get(year).contains(datestr);

            // 核心判定:周末仅在补班时算工作日,平日仅在不属于节假日时算工作日
            boolean isworkday = isweekend ? isworkweekend : !isholiday;

            if (isworkday) {
                added++;
            }
        }
        return current;
    }

    // ------------------- api 交互 -------------------

    /**
     * 获取指定年份的法定节假日日期集合
     */
    private static set<string> fetchholidays(int year) throws exception {
        jsonobject data = fetchapidata(year);
        set<string> holidays = new hashset<>();
        jsonarray list = data.optjsonarray("list");
        if (list == null) return holidays;

        for (int i = 0; i < list.length(); i++) {
            jsonobject item = list.getjsonobject(i);
            // 有些 api 用 holiday 字段,有些用 vacation,都取并集
            string holiday = item.optstring("holiday");
            string vacation = item.optstring("vacation");
            if (holiday != null && !holiday.isempty()) {
                holidays.addall(expanddaterange(holiday));
            }
            if (vacation != null && !vacation.isempty()) {
                holidays.addall(expanddaterange(vacation));
            }
        }
        return holidays;
    }

    /**
     * 获取指定年份的调休补班日期集合(周六日上班)
     */
    private static set<string> fetchworkweekends(int year) throws exception {
        jsonobject data = fetchapidata(year);
        set<string> workweekends = new hashset<>();
        jsonarray list = data.optjsonarray("list");
        if (list == null) return workweekends;

        for (int i = 0; i < list.length(); i++) {
            jsonobject item = list.getjsonobject(i);
            // apizero 的补班字段为 remark,可能包含多个逗号分隔的日期
            string remark = item.optstring("remark");
            if (remark != null && !remark.isempty()) {
                for (string d : remark.split(",")) {
                    string trimmed = d.trim();
                    if (!trimmed.isempty()) {
                        workweekends.add(trimmed);
                    }
                }
            }
        }
        return workweekends;
    }

    /**
     * 调用 api 获取原始 json 数据
     */
    private static jsonobject fetchapidata(int year) throws exception {
        string urlstr = api_url + "?year=" + year;
        httpurlconnection conn = (httpurlconnection) new url(urlstr).openconnection();
        conn.setrequestmethod("get");
        conn.setconnecttimeout(5000);
        conn.setreadtimeout(5000);

        int code = conn.getresponsecode();
        if (code != 200) {
            throw new runtimeexception("api 调用失败,http 状态码:" + code);
        }

        try (bufferedreader reader = new bufferedreader(
                new inputstreamreader(conn.getinputstream(), "utf-8"))) {
            stringbuilder sb = new stringbuilder();
            string line;
            while ((line = reader.readline()) != null) {
                sb.append(line);
            }
            jsonobject json = new jsonobject(sb.tostring());
            if (json.optint("code") != 200) {
                throw new runtimeexception("api 返回错误:" + json.optstring("message"));
            }
            return json.getjsonobject("data");
        } finally {
            conn.disconnect();
        }
    }

    /**
     * 展开日期范围,例如 "2026-10-01~2026-10-07" 拆成每一天
     */
    private static set<string> expanddaterange(string range) {
        set<string> dates = new hashset<>();
        if (range == null || range.isempty()) return dates;

        if (range.contains("~")) {
            string[] parts = range.split("~");
            if (parts.length == 2) {
                try {
                    localdate start = localdate.parse(parts[0].trim(), fmt);
                    localdate end = localdate.parse(parts[1].trim(), fmt);
                    while (!start.isafter(end)) {
                        dates.add(start.format(fmt));
                        start = start.plusdays(1);
                    }
                } catch (exception ignored) {
                    // 格式异常则忽略
                }
            }
        } else {
            dates.add(range);
        }
        return dates;
    }

    // ---------- 测试 ----------
    public static void main(string[] args) throws exception {
        localdate start = localdate.of(2026, 6, 1);
        int workdays = 30;
        localdate result = getdateafterworkdays(start, workdays);
        system.out.println("从 " + start + " 起 " + workdays + " 个工作日后:" + result);
        // 无论跨多少年,都会自动加载对应年份的数据
    }
}

注意workdays=300 虽然理论上可行,但现实中国家只公布未来一年的节假日安排,如果跨年太远(例如 2026 年要跨到 2027 年底),api 可能没有数据,此时需要人工介入或使用数据库方案。

方案二:自建数据库表(企业级推荐)

对于金融、政务等对数据主 权和稳定性要求极高的场景,建议将节假日数据存储在自己的数据库中,每年根据公告手动维护。

mysql 表结构设计

create table `holiday` (
    `id` int primary key auto_increment,
    `date` date not null comment '日期(yyyy-mm-dd)',
    `is_work_day` tinyint(1) not null default 0 comment '0=休息日(法定节假日),1=工作日(调休补班)',
    `year` int not null comment '年份(便于索引)',
    `description` varchar(100) default null comment '描述,例如“国庆节”、“调休补班”',
    unique key `uk_date` (`date`),
    key `idx_year` (`year`)
) engine=innodb default charset=utf8mb4;

示例数据(2026 年部分)

dateis_work_daydescription
2026-01-010元旦
2026-01-020元旦调休
2026-01-030元旦调休
2026-02-150春节
2026-02-160春节调休
2026-10-010国庆节
2026-10-020国庆调休
2026-10-070国庆调休
2026-10-101调休补班(周六上班)

特别说明

  • 对于普通的周一至周五,无需插入任何记录,系统默认它们为工作日。
  • 仅需插入两类“例外”:法定节假日is_work_day=0)和调休补班is_work_day=1,通常是周六日)。

java 访问数据库(只写逻辑,不写具体连接代码)

在 service 层,我们可以这样设计(省略 jdbc 细节,只展示核心流程):

public class workdaycalculatorbydb {

    // 同样使用懒加载缓存
    private static final map<integer, set<string>> holiday_cache = new hashmap<>();
    private static final map<integer, set<string>> work_weekend_cache = new hashmap<>();

    public static localdate getdateafterworkdays(localdate startdate, int workdays) {
        localdate current = startdate;
        int added = 0;

        while (added < workdays) {
            current = current.plusdays(1);
            int year = current.getyear();

            // 如果缓存中没有该年的数据,则从数据库加载
            if (!holiday_cache.containskey(year)) {
                holiday_cache.put(year, loadholidaysfromdb(year));
                work_weekend_cache.put(year, loadworkweekendsfromdb(year));
            }

            string datestr = current.format(datetimeformatter.iso_local_date);
            boolean isweekend = current.getdayofweek().getvalue() >= 6;
            boolean isholiday = holiday_cache.get(year).contains(datestr);
            boolean isworkweekend = work_weekend_cache.get(year).contains(datestr);

            if (isweekend ? isworkweekend : !isholiday) {
                added++;
            }
        }
        return current;
    }

    // 从数据库加载指定年份的节假日(is_work_day=0)
    private static set<string> loadholidaysfromdb(int year) {
        // 执行 sql: select date from holiday where year = ? and is_work_day = 0
        // 将结果集转为 set<string> 并返回
        return new hashset<>(); // 伪代码
    }

    // 从数据库加载指定年份的调休补班(is_work_day=1)
    private static set<string> loadworkweekendsfromdb(int year) {
        // 执行 sql: select date from holiday where year = ? and is_work_day = 1
        return new hashset<>(); // 伪代码
    }
}

使用建议

  • 每年 11~12 月公布次年节假日安排后,通过数据导入工具(如 excel 导入或脚本)将数据插入 holiday 表。
  • 应用启动时可将全量数据加载到内存缓存中,避免每次计算都查询数据库(数据量极小,每年不到 50 条)。

date ↔ localdate 互转工具(兼容老旧系统)

如果你的项目仍在使用 java.util.date,可以封装以下工具类无缝集成:

import java.time.instant;
import java.time.localdate;
import java.time.zoneid;
import java.util.date;

public class dateconverter {

    /**
     * java.util.date → java.time.localdate
     * 使用系统默认时区,如需指定时区可传入 zoneid.of("asia/shanghai")
     */
    public static localdate tolocaldate(date date) {
        return instant.ofepochmilli(date.gettime())
                .atzone(zoneid.systemdefault())
                .tolocaldate();
    }

    /**
     * java.time.localdate → java.util.date
     */
    public static date todate(localdate localdate) {
        return date.from(localdate.atstartofday(zoneid.systemdefault()).toinstant());
    }
}

使用示例

date oldstart = new date();
localdate startlocal = dateconverter.tolocaldate(oldstart);
localdate resultlocal = workdaycalculatorbyapi.getdateafterworkdays(startlocal, 5);
date resultdate = dateconverter.todate(resultlocal);

总结与最佳实践

关键点建议
跨年处理务必使用本文的动态懒加载,在循环中按需加载年份数据,杜绝硬编码判断。
数据源选择互联网项目可用 api(零维护),企业内网或对数据可控性要求高的项目必须用数据库。
缓存策略节假日数据极少,建议使用 hashmap 做 jvm 内存缓存,避免重复调用 api 或查询数据库。
数据更新api 方案无需维护;数据库方案需每年根据公告导入数据,并注意调休补班(标记 is_work_day=1)。
年份超限问题workdays 过大(如 300),可能跨到未发布数据的未来年份。此时 api 可能返回空数据,数据库也可能无记录。建议业务上限制最大计算范围,或返回错误提示。
性能逐日循环在 workdays 为几百时性能无影响,若达到数万次循环也仅需毫秒级,无需过度优化。

希望这篇博客能帮助你彻底解决“工作日计算”这一经典业务难题。 

以上就是java如何精确计算n个工作日后的日期的详细内容,更多关于java工作日日期计算的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com