简介这是一份面向Java开发者的日期计算实战资料聚焦「计算两个日期之间的天数并排除节假日与周末」这一常见业务需求适合需要实现请假天数、工作日统计等功能的初中级开发者参考。资源以PDF形式呈现压缩包内共1个文件体积约39KB内容围绕Calendar类、DAY_OF_WEEK判断周末、数据库查询节假日、try-catch异常处理以及循环累加算法展开并给出calLeaveDays方法的完整代码示例支持请假半天计0.5天的double类型结果。读者可从中掌握日期遍历、周末与节假日双重过滤、数据库交互及异常捕获的完整实现思路同时了解节假日表设计与查询方式便于直接迁移到考勤、工时统计等场景。目前已有3401人学习下载可作为日期计算类功能的实用参考。1. 算工作日天数这件事为什么总在验收前夜翻车排期系统里最容易被质疑的一个数字就是「两个日期之间到底有几个工作日」。产品经理拍脑袋说 15 个工作日能上线你写了个ChronoUnit.DAYS.between()一减得到 21 天交付日期直接对不上。更麻烦的是遇上春节、国庆这种调休周末可能变工作日工作日也可能变休息日纯靠DayOfWeek判断必然翻车。这篇讲的就是用 Java 计算两个日期之间的天数并且排除周末和节假日。核心不是 API 怎么调而是「节假日数据从哪来、怎么存、怎么查、怎么保证跨年不出错」。适合正在做排期、考勤、SLA 计时、工单时效统计的后端同学也适合被业务方追着问「为什么少算了一天」的开发者。下面按最小可跑通的路径一步步来参数、边界、坑都摊开讲。2. 先定口径再写代码工作日、节假日、调休到底怎么算2.1 三种常见口径选错了后面全白干在动手之前必须和业务方确认一件事你说的「工作日」是哪种口径定义典型场景实现难度纯周末排除周一至周五算工作日不管法定节假日内部粗略排期低周末 法定节假日排除周六周日和法定节假日考勤、合同工期中周末 节假日 调休在上一档基础上把调休上班的周末算回工作日精确 SLA、工资结算高绝大多数「算出来对不上」的问题根源都是口径没对齐。业务方嘴上说「排除节假日」心里想的是第三档你按第二档写春节前后必然差出好几天。我的习惯是先把口径写进接口文档再写代码。2.2 为什么不用 DayOfWeek 硬编码新手最容易写出这样的代码// 反例只判断周末遇到调休直接错 public static long countWorkDays(LocalDate start, LocalDate end) { long count 0; LocalDate cur start; while (!cur.isAfter(end)) { DayOfWeek dow cur.getDayOfWeek(); if (dow ! DayOfWeek.SATURDAY dow ! DayOfWeek.SUNDAY) { count; } cur cur.plusDays(1); } return count; }这段代码逻辑没错但它只解决了「纯周末排除」。一旦有调休比如某个周日被调整为上班它会把这天算成休息结果偏少。所以正确做法是周末判断只作为默认规则真正的判定权交给一张可配置的节假日表。表里明确标记每一天是「休息」还是「上班」代码只查表不猜。2.3 节假日数据从哪来三种落地方式对比节假日数据是这类需求的黑匣子来源决定了维护成本。硬编码枚举把每年的节假日写死在代码里。优点是零依赖、查询快缺点是每年要改代码重新发版跨年就失效。配置文件 / 数据库表把日期和类型存成2025-01-01,REST这样的记录。优点是改数据不用发版缺点是要有人维护且需要处理「今年数据还没发布」的空窗期。第三方接口调用外部日历服务。优点是省心缺点是引入网络依赖接口挂了业务就挂还要考虑限流和缓存。我一般选第二种数据库表 本地缓存。数据量极小一年也就 30 条左右查询用Set缓存到内存性能没有压力维护也灵活。下面按这个方案展开。3. 用 Java 把工作日计算跑通从建表到最小可运行代码3.1 建一张节假日表字段别贪多表结构越简单越不容易出错我一般只留四个字段CREATE TABLE holiday_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, cal_date DATE NOT NULL COMMENT 日期, day_type TINYINT NOT NULL COMMENT 1休息 2上班(调休), remark VARCHAR(64) COMMENT 备注如春节, UNIQUE KEY uk_cal_date (cal_date) );day_type用 1 和 2 而不是布尔值是为了以后扩展比如半天班。cal_date加唯一索引防止同一天被录入两次导致判定冲突。注意这里只存「特殊日子」普通周末不用存代码里用默认规则兜底这样数据量最小。3.2 核心计算逻辑一次遍历 集合查表先定义好类型枚举和查询接口再写计算。下面是可以直接抄的最小实现import java.time.DayOfWeek; import java.time.LocalDate; import java.util.Set; public class WorkDayCalculator { // 休息日集合法定节假日 调休放假的日期 private final SetLocalDate restDays; // 上班日集合被调休成工作日的周末 private final SetLocalDate workDays; public WorkDayCalculator(SetLocalDate restDays, SetLocalDate workDays) { this.restDays restDays; this.workDays workDays; } /** * 计算 [start, end] 闭区间内的工作日天数 * 口径排除周末和法定节假日但调休上班的周末计入 */ public long count(LocalDate start, LocalDate end) { if (start null || end null) { throw new IllegalArgumentException(日期不能为空); } if (start.isAfter(end)) { // 允许反向传参交换后计算避免调用方踩坑 LocalDate tmp start; start end; end tmp; } long count 0; LocalDate cur start; while (!cur.isAfter(end)) { if (isWorkDay(cur)) { count; } cur cur.plusDays(1); } return count; } private boolean isWorkDay(LocalDate date) { // 优先级调休上班 法定休息 默认周末规则 if (workDays.contains(date)) { return true; } if (restDays.contains(date)) { return false; } DayOfWeek dow date.getDayOfWeek(); return dow ! DayOfWeek.SATURDAY dow ! DayOfWeek.SUNDAY; } }逻辑说明isWorkDay的判定顺序是关键。调休上班的优先级最高因为一个日期可能既是周末又被标记为上班其次是法定休息最后才落到默认的周末规则。这个顺序如果写反调休就会失效。参数说明restDays和workDays用Set而不是List因为判定是逐天进行的HashSet的contains是 O(1)跨年计算几千天也不会有性能问题。如果数据量真的很大比如算十年可以把循环换成按周跳跃但一般业务用不上。3.3 从数据库加载数据并缓存计算逻辑不碰数据库数据由外部注入这样单元测试不用起容器。加载部分单独写import java.time.LocalDate; import java.util.Set; import java.util.stream.Collectors; public class HolidayRepository { // 假设用 JdbcTemplate 查询实际替换成你的 DAO private final JdbcTemplate jdbcTemplate; public HolidayRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public WorkDayCalculator loadCalculator(int year) { // 只加载目标年份及相邻年份避免跨年计算时缺数据 String sql SELECT cal_date, day_type FROM holiday_config WHERE YEAR(cal_date) BETWEEN ? AND ?; var rows jdbcTemplate.queryForList(sql, year - 1, year 1); SetLocalDate restDays rows.stream() .filter(r - ((Number) r.get(day_type)).intValue() 1) .map(r - ((java.sql.Date) r.get(cal_date)).toLocalDate()) .collect(Collectors.toSet()); SetLocalDate workDays rows.stream() .filter(r - ((Number) r.get(day_type)).intValue() 2) .map(r - ((java.sql.Date) r.get(cal_date)).toLocalDate()) .collect(Collectors.toSet()); return new WorkDayCalculator(restDays, workDays); } }逻辑说明查询范围取year-1到year1是因为跨年计算比如 12 月 20 日到次年 1 月 10 日会同时用到两年的数据。如果只查目标年跨年时就会漏掉另一年的节假日这是血泪经验里最常见的一类 bug。参数说明year传业务发生的年份即可。生产环境建议把WorkDayCalculator按年缓存起来比如用ConcurrentHashMapInteger, WorkDayCalculator避免每次计算都查库。缓存失效策略很简单节假日数据变更时清空对应年份的缓存。3.4 一个能直接跑的验证用例写完别急着交付先用几个边界日期验证public class WorkDayCalculatorTest { public static void main(String[] args) { SetLocalDate rest Set.of( LocalDate.of(2025, 1, 1), // 元旦 LocalDate.of(2025, 1, 28) // 假设春节 ); SetLocalDate work Set.of( LocalDate.of(2025, 1, 26) // 假设调休上班的周日 ); WorkDayCalculator calc new WorkDayCalculator(rest, work); // 2025-01-01 到 2025-01-07元旦休息1/26 不在区间内 System.out.println(calc.count( LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 7))); // 预期 4 // 跨区间验证调休1/26 是周日但上班 System.out.println(calc.count( LocalDate.of(2025, 1, 25), LocalDate.of(2025, 1, 27))); // 预期 21/26 上班 1/27 周一 } }逻辑说明第一个用例验证法定节假日被正确排除第二个用例验证调休上班的周末被正确计入。这两个方向都覆盖到基本逻辑就稳了。参数说明测试数据用Set.of构造注意Set.of不允许重复元素如果同一天既在rest又在work会抛异常——这其实是个好事能提前暴露数据录入冲突。4. 避坑指南工作日计算最容易踩的五个坑4.1 坑一跨年计算只加载了单年数据现象12 月底算到次年 1 月初结果比预期多算或少算几天。原因查询节假日时只按起始年份过滤次年的元旦没被加载被当成普通工作日。解决加载范围扩到year-1到year1或者干脆按日期区间查询不按年份。4.2 坑二调休优先级写反现象某个周日明明调休上班结果还是被算成休息。原因isWorkDay里先判断了周末规则再判断调休集合导致调休被覆盖。解决把workDays.contains(date)放在最前面保证调休优先级最高。4.3 坑三把「区间」当成左闭右开现象算出来的天数总是比业务方少一天。原因业务方说的「1 号到 7 号」通常包含 7 号而代码里用了cur.isBefore(end)而不是!cur.isAfter(end)。解决明确闭区间语义并在接口注释里写清楚。如果业务要左闭右开单独提供重载方法别让调用方猜。4.4 坑四节假日数据没发布就上线现象年初算工时发现全年节假日都没排除。原因当年的节假日安排还没公布数据库里是空的代码只能按默认周末规则算。解决加一个兜底策略——如果目标年份数据为空记录告警日志并返回「按周末规则计算」的结果同时在接口返回里标注dataCompletefalse让业务方知道这个数字是估算的。4.5 坑五用毫秒数做日期差现象算出来的天数偶尔差一天尤其在夏令时切换的地区。原因用(endMillis - startMillis) / 86400000计算遇到时区偏移会截断。解决全程用LocalDate它是无时区的纯日期不受夏令时影响。如果输入是Date先转成LocalDate再算。5. 进阶把工作日计算做成可复用的服务5.1 用缓存把查询压到内存前面每次计算都遍历日期如果 QPS 高可以把「某年某月的工作日天数」预计算成前缀和。做法是对一年 365 天维护一个数组prefix[i]表示从 1 月 1 日到第 i 天的累计工作日数那么任意区间[a, b]的天数就是prefix[b] - prefix[a-1]。这样单次查询是 O(1)代价是每年数据变更时重建一次数组。对于排期系统这种读多写少的场景非常划算。5.2 处理「半天班」和「特殊工时」有些公司周五下午不上班或者某些节日前一天只上半天。这时候day_type可以扩展成小数权重全天班记 1.0半天班记 0.5休息记 0。计算时累加权重而不是计数。改动很小但能覆盖更多真实场景。注意权重方案要和业务方确认清楚否则又会出现「为什么是 0.5 不是 1」的扯皮。5.3 验证方法用已知年份做回归上线前拿过去一年的真实节假日数据跑一遍和人工核对的结果对比。重点看三个位置春节前后、国庆前后、以及任何有调休的周末。如果这三个位置都对得上基本可以放心。我一般会写一个回归测试把过去三年的数据固化进去每次改计算逻辑都跑一遍防止改 A 坏 B。5.4 一个具体技巧用接口返回口径说明最后分享一个减少沟通成本的习惯在计算接口的返回体里带上口径说明。public class WorkDayResult { private long workDays; // 工作日天数 private String rule; // 口径描述如 排除周末及法定节假日含调休 private boolean dataComplete; // 节假日数据是否完整 // getter/setter 省略 }这样业务方拿到数字的同时也知道这个数字是怎么来的。遇到争议时直接看rule字段不用再翻代码。这个习惯帮我省掉了无数次「你这个数怎么算的」的追问。希望帮到你。本文还有配套的精品资源点击获取