为什么要计算工作日后的日期?
在电商、金融、oa 系统开发中,我们经常需要计算若干个工作日(跳过周末和法定节假日)之后的日期。典型场景包括:
- 工单 sla:承诺 5 个工作日内响应。
- 付款到期日:合同约定 3 个工作日内付款。
- 定时任务调度:只允许在交易日执行。
但问题在于:
- 周末固定(周六、日)容易处理。
- 法定节假日每年不同,需查阅公告。
- 调休补班(如国庆前后周六上班)让简单的“周六日休息”规则失效。
因此,数据 + 算法缺一不可。本文提供一套基于 java 8 java.time.localdate 的完整解决方案,核心算法采用“逐日递增 + 动态懒加载”策略,支持任意跨年(哪怕 workdays=300 也不怕),并附赠 date ↔ localdate 互转工具,完美兼容老旧系统。
核心算法思路(简单粗暴,但有效)
算法本质是“一天一天地数”,直到数满指定的工作日数量。
判定逻辑(优先级从高到低)
对于每一天,我们根据它的“日期属性”判断是否为工作日:
| 日期类型 | 是否为工作日? |
|---|---|
| 周一 ~ 周五 | 默认是工作日,但如果该日期在“法定节假日黑名单”中,则不是工作日。 |
| 周六 ~ 周日 | 默认不是工作日,但如果该日期在“调休补班白名单”中,则是工作日(即调休上班)。 |
算法伪代码
输入:起始日期 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) {
// 加载下一年的数据
}
这种写法有两大致命缺陷:
- 如果
workdays较大(比如从 6 月 1 日开始计算 30 个工作日),最终日期可能跨到次年,但上述条件不触发。 - 如果
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 年部分)
| date | is_work_day | description |
|---|---|---|
| 2026-01-01 | 0 | 元旦 |
| 2026-01-02 | 0 | 元旦调休 |
| 2026-01-03 | 0 | 元旦调休 |
| 2026-02-15 | 0 | 春节 |
| 2026-02-16 | 0 | 春节调休 |
| 2026-10-01 | 0 | 国庆节 |
| 2026-10-02 | 0 | 国庆调休 |
| 2026-10-07 | 0 | 国庆调休 |
| 2026-10-10 | 1 | 调休补班(周六上班) |
特别说明:
- 对于普通的周一至周五,无需插入任何记录,系统默认它们为工作日。
- 仅需插入两类“例外”:法定节假日(
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工作日日期计算的资料请关注代码网其它相关文章!
发表评论