news 2026/10/15 10:17:19

Java万年历源码:闰年判断与蔡勒公式实现详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java万年历源码:闰年判断与蔡勒公式实现详解

简介:这是一份面向Java初学者和课程设计学生的完整“万年历记事本”项目资料,以Swing图形界面实现万年历展示和日记记录功能,是学习日期时间处理、GUI组件布局、事件驱动编程及文件读写的综合案例。压缩包内仅含1个doc文档,大小317KB,文档将课程设计报告、核心类calendar的完整源码、运行效果截图与工作总结整合在一起,查阅和复用都很方便。目前已有184人学习/下载,资源虽小但内容完整,适合作为Java课程设计参考、期末项目模板或桌面应用开发入门练习。内容详细讲解了java.util.Calendar与java.time包的日期计算方法,JFrame、JComboBox、JTextField、JTextArea等Swing组件的组合运用,ActionListener与MouseListener事件响应,以及FileWriter/BufferedReader读写日记、闰年判断、月份天数计算等具体实现,并涉及try-catch异常处理、布局管理器和主函数入口设计。读者从中不仅能获得可直接借鉴的源代码和设计思路,还能看到作者对总体设计、功能模块、布局管理和排错过程的完整记录,无论是学习还是二次开发都能快速入手,对提升Java综合编程能力很有帮助。

1. JAVA万年历源码:一个老题目为何仍有新坑

“JAVA万年历源码”这个标题在各平台一搜一大把,但真正能扛住跨世纪边界检查的版本并不多。某开发者拿来一个现成源码,发现 1900 年到 2030 年都正常,到 2100 年 2 月却多出 29 号——问题不在代码抄没抄对,而在闰年判断和星期基准没立稳。这篇笔记不是给一个下载包,而是把万年历从日期算法、源码分层到终端与界面两种输出的完整落地方案讲清楚,适合刚学 Java 需要综合作业的同学,也适合想从旧代码里抠出日期处理经验的中级开发者。

2. 核心算法先立住:闰年、星期与每月天数的计算公式

万年历看着简单,真正决定源码质量的是三个底层计算:一年是否闰年、某天是星期几、每月有多少天。这三件事互相牵连,只要一个判断写错,表面看只是某个月多打一天,实际整个月份的星期对齐都会跟着歪掉。所以我习惯先把算法单独写成一个小工具类,不急着做界面。

2.1 闰年的正确判定:为什么四个条件里“百年”是陷阱

完整的闰年规则是:能被 400 整除的年份是闰年;能被 4 整除但不能被 100 整除的年份也是闰年;其余都不是。很多源码只写year % 4 == 0,导致 1900、2100 这种年份被错误当作闰年,2 月凭空多出一天。另一个常见误写是“能被 4 整除且能被 100 整除”也算闰年,这会把每百年的两个年份同时搞错。

public final class DateAlgo { // 闰年判定:400 的倍数直接是闰年; // 否则必须是 4 的倍数且不是 100 的倍数 public static boolean isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } // 返回某年某月的天数,month 范围 1-12 public static int daysInMonth(int year, int month) { switch (month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; case 4: case 6: case 9: case 11: return 30; case 2: return isLeapYear(year) ? 29 : 28; default: throw new IllegalArgumentException("month must be 1-12: " + month); } } }

这段代码把两个判断放在同一个类里,逻辑说明如下:闰年判定先看 400,再回头检查 4 和 100,顺序上不要在year % 4 == 0后直接返回 true,必须把不能被 100 整除这条带上。daysInMonth用 switch 而不是数组,好处是非法月份能抛出异常,不至于让调用方拿到一个越界的默认值。参数year和month都是 int,调用时建议先由上层做范围校验,工具类本身只负责“给定合法输入,返回正确结果”。

2.2 星期计算选用蔡勒公式:参数、边界与代码实现

求某天是星期几,常见思路是用一个已知基准日推算,比如“2000 年 1 月 1 日是星期六”,然后算出相差天数再对 7 取模。这种做法不是不行,但基准日的星期一旦记错,或者中间用Calendar类混算导致时区干扰,结果会整体偏移一天。我一般直接用蔡勒公式,它不需要依赖系统当前时间,输入年月日就能算出星期,适合写进纯算法类里。

public static int dayOfWeek(int year, int month, int day) { // 蔡勒公式只接受 3-14 月,因此把 1 月、2 月看成上一年的 13 月、14 月 if (month < 3) { month += 12; year--; } int century = year / 100; // 世纪数 int yearOfCentury = year % 100; // 世纪内年份 // 公式返回 0=星期日,1=星期一,以此类推 int week = (yearOfCentury + yearOfCentury / 4 + century / 4 - 2 * century + 26 * (month + 1) / 10 + day - 1) % 7; // Java 的 % 可能得到负数,这里统一修正到 0-6 return (week % 7 + 7) % 7; }

逻辑说明:蔡勒公式被很多实现写错,关键在于 1、2 月要做“年份倒退、月份加 12”的调整,否则计算结果跟公历对不上。代码里26 * (month + 1) / 10是公式的固定项,不要约分成2.6 * (month + 1),整数除法会丢失精度,结果星期直接乱掉。参数里century是年份前两位,yearOfCentury是后两位,例如 2024 年对应 century=20、yearOfCentury=24。最后的(week % 7 + 7) % 7是为了把 Java 负数取余修正成数学取模,否则 1 月等某些输入会返回负值。

2.3 月份天数的第二种写法:两套数组解决“闰年二月”

用 switch 写天数清晰,但有些源码会把平闰两套数组封装成查表方式,适合在后面需要频繁按月份索引的场景。我在模型层里更倾向数组策略,因为万年历要连续输出整年 12 个月,数组查询速度稳定,代码也短。

public static int daysInMonthByArray(int year, int month) { int[] commonYear = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (month < 1 || month > 12) { throw new IllegalArgumentException("month out of range: " + month); } if (month == 2 && isLeapYear(year)) { return 29; } return commonYear[month - 1]; }

参数说明:commonYear下标从 0 开始,所以访问时要减 1;这里没有单独建闰年数组,而是对 2 月做特判,两套数组的好处是逻辑直观,坏处是容易忘记“下标从 0 开始”导致差一错误。相比之下,switch 版本虽然行数多些,但胜在不会越界。真实项目中我建议保留 switch 版本作为唯一实现,数组写法只适合理解平闰年切换的思路。无论选哪种,都要在注释里写明白 month 的合法范围是 1 到 12,非法输入必须抛异常,不能悄悄返回错误天数。

3. 把源码结构拆开:模型、控制器与打印视图各管什么

不少初学者的万年历源码是把计算、打印、输入全塞进 main 方法,几百行下来根本没法调。更稳的做法是按职责拆成三层:数据模型负责保存日期和计算规则,控制器负责接收输入并调用算法,视图负责把结果打印成终端表格或渲染到界面。这样算法可以单独做单元测试,换界面也不用重写日期逻辑。

3.1 用类把“一天”封装起来:不可变模型的意义

模型层如果只传 year、month、day 三个散落的 int,调用时很容易传错参数顺序。我一般会把单个日期封装成不可变对象,让编译器帮我们检查类型,也能在调试时直接打印对象内容。

public final class CalendarDay { private final int year; private final int month; private final int day; public CalendarDay(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public int getYear() { return year; } public int getMonth() { return month; } public int getDay() { return day; } public int dayOfWeek() { return DateAlgo.dayOfWeek(year, month, day); } public boolean isSameDay(CalendarDay other) { return year == other.year && month == other.month && day == other.day; } @Override public String toString() { return year + "-" + month + "-" + day; } }

参数与设计说明:构造器里没有做合法性校验,原因是模型对象应由控制器创建,控制器知道输入范围,模型层只保证“被创建出来的对象状态不可变”。final关键字加上只读 getter,避免别处把日期改坏。dayOfWeek()直接复用第二章的蔡勒公式,业务方不用关心算法细节。isSameDay用来做边界比较,比如翻页时判断新选中的月份是否和当前是同一个月份。

3.2 控制器:把“年月”变成“当月六行七列”的数据

控制器是万年历源码里最容易写乱的部分。它要做三件事:校验年月范围、算出当月 1 号是星期几、把 1 号之前的空位和当月每天填进一个二维数组。数组我固定用 6 行 7 列,因为日历最长跨六周,空位用 0 表示,视觉上比较干净。

public class MonthData { public final int year; public final int month; public final int[][] cells; public MonthData(int year, int month) { this.year = year; this.month = month; this.cells = buildCells(year, month); } private int[][] buildCells(int year, int month) { // 表格存储 1 号前的空白与日期,空白用 0 填充 int[][] grid = new int[6][7]; int firstDay = DateAlgo.dayOfWeek(year, month, 1); // 把星期天作为 0,则 1 号应放入第 firstDay 列 int offset = firstDay; int days = DateAlgo.daysInMonth(year, month); int index = 1; for (int row = 0; row < 6; row++) { for (int col = 0; col < 7; col++) { if (row == 0 && col < offset) { grid[row][col] = 0; } else if (index <= days) { grid[row][col] = index++; } else { grid[row][col] = 0; } } } return grid; } }

逻辑说明:firstDay来自蔡勒公式,当 1 号是星期日时返回 0,正好落在第 0 列;是星期一时返回 1,落在第 1 列。这样构造出来的cells天然就是“日历表格”的形状,后面无论打印还是画界面,都直接遍历二维数组,不需要再算每行起始位置。参数方面,offset只在第一行起作用,超出当月天数后全部填 0,不过多出的 0 会在视图层被忽略。这个类有个小缺点:两个月的数据要建两个对象,但因为MonthData只保存 int 数组,内存代价可以忽略。

3.3 打印视图:按星期表头对齐的文本输出

控制台版视图要把二维数组排成一张人能看懂的表格。这里有个终端兼容问题:中文和数字宽度不一致,直接拼接字符串很容易歪掉。我的做法是统一用四字符宽输出,数字靠右,星期表头也补足到相同宽度。

public class MonthPrinter { public static void printMonth(MonthData data) { String[] headers = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; for (String h : headers) { System.out.printf("%4s", h); } System.out.println(); for (int[] row : data.cells) { for (int cell : row) { if (cell == 0) { System.out.print(" "); } else { System.out.printf("%3d ", cell); } } System.out.println(); } } }

参数说明:%4s是右对齐占四格,英文星期缩写正好四个字符;日期数字用%3d占三格加一个空格,保证数字和空白整体占四格,列与列不会挤在一起。空位输出三个空格,等于占满四格但少一个数字。这样做的代价是表头看起来有点紧凑,但胜在各种终端下列都能对齐。如果你希望表头用中文“一 二 三”,需要额外做全角宽度处理,这一章先用英文表头把原理讲透,中文对齐问题在第 5 章再单独说。

4. 让源码跑起来:控制台一键命令到 Swing 界面改造

算法和分层都就位后,第一步先在控制台把完整流程跑通,不要一上来就做界面。控制台版最适合快速验证:写一个几行的入口类,读取命令参数,调用控制器,交给打印视图。跑通后再把同一个MonthData接到 Swing 上,日期逻辑完全不用改。

4.1 控制台版最小启动类:main 函数、参数解析与异常兜底

入口类的职责是解析args,把字符串转成 int,再交给MonthData和MonthPrinter。我习惯让程序支持不传参数时打印当前月份,传两个参数时打印指定年月,这样既方便演示也方便自动化测试。

import java.time.LocalDate; public class CalendarConsole { public static void main(String[] args) { int year; int month; if (args.length >= 2) { year = Integer.parseInt(args[0]); month = Integer.parseInt(args[1]); } else { LocalDate today = LocalDate.now(); year = today.getYear(); month = today.getMonthValue(); } // 范围校验,防止月份输入 13 或年份过小导致算法异常 if (month < 1 || month > 12) { System.err.println("month must be between 1 and 12"); return; } MonthData data = new MonthData(year, month); MonthPrinter.printMonth(data); } }

逻辑说明:默认取当前月时用了java.time.LocalDate,这是标准库推荐的新日期 API,它可以拿到本地年月,不掺时区概念。手动传参时用Integer.parseInt,这里没有做 try-catch,因为演示程序里参数非法就该直接抛异常,方便定位问题;如果你要交付给别人用,可以在外层套一层 try-catch 并打印友好提示。month校验放在调用MonthData之前,因为工具类只负责处理合法输入,边界控制属于控制器的职责。测试命令类似:传“2024 7”打印 2024 年 7 月,传“2100 2”就能快速验证第二章说的闰年边界。

4.2 Swing 版整体框架:JFrame、JPanel 与日历表格渲染

控制台跑通后,做 Swing 界面就只是把二维数组搬到JTable。核心思路是自定义一个TableModel,让getValueAt直接返回MonthData.cells里的值,0 显示为空字符串。这样表格的行列结构天然对应星期一到星期日。

import javax.swing.*; import javax.swing.table.AbstractTableModel; public class CalendarTableModel extends AbstractTableModel { private final MonthData data; public CalendarTableModel(MonthData data) { this.data = data; } @Override public int getRowCount() { return 6; } @Override public int getColumnCount() { return 7; } @Override public Object getValueAt(int rowIndex, int columnIndex) { int value = data.cells[rowIndex][columnIndex]; return value == 0 ? "" : value; } @Override public String getColumnName(int column) { String[] names = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; return names[column]; } }

参数说明:getValueAt返回的是 Object,JTable 会调用toString来显示,所以日期数字直接返回 int 即可,空位返回空字符串会让格子看起来是空白。getColumnName用于表头,使用英文缩写避免全角对齐问题。这个模型类不持有界面组件,只持有MonthData,因此切换月份时只需要替换 model 里的 data 引用,再调用table.repaint(),整张表立即刷新。这是 Swing 里最值得养成的习惯:界面和数据源分离,后面无论加按钮还是加下拉框都改不动日期计算。

4.3 交互细节:月份切换与跨年滚动的体验

只显示一个月的万年历不够,至少要支持上一月、下一月和年份选择。交互层我习惯用一个JComboBox<Integer>选年份、一个JComboBox<Integer>选月份,外加两个按钮做翻月。事件监听里只更新MonthData,并让表格重新显示。

public class CalendarFrame extends JFrame { private JTable table; private CalendarTableModel model; private JComboBox<Integer> yearBox; private JComboBox<Integer> monthBox; public CalendarFrame() { setTitle("JAVA万年历"); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout()); JPanel top = new JPanel(); yearBox = new JComboBox<>(); monthBox = new JComboBox<>(); for (int y = 1900; y <= 2100; y++) { yearBox.addItem(y); } for (int m = 1; m <= 12; m++) { monthBox.addItem(m); } top.add(yearBox); top.add(monthBox); add(top, BorderLayout.NORTH); MonthData initialData = new MonthData(2024, 7); model = new CalendarTableModel(initialData); table = new JTable(model); add(new JScrollPane(table), BorderLayout.CENTER); yearBox.addActionListener(e -> updateTable()); monthBox.addActionListener(e -> updateTable()); pack(); setVisible(true); } private void updateTable() { int y = (Integer) yearBox.getSelectedItem(); int m = (Integer) monthBox.getSelectedItem(); model.setData(new MonthData(y, m)); } }

交互说明:下拉框年份限在 1900 到 2100,既覆盖绝大多数应用场景,也避开算法可能越界的极端年份。翻月按钮没有写在这里,但实现方式一样:拿到当前年月后加 1 或减 1,遇到 12 月往后翻要年份加一、月份回到 1,遇到 1 月往前翻要年份减一、月份回到 12。推荐把这段“加减月份”的逻辑写到控制器层,而不是散在按钮事件里,否则两个按钮会写两段几乎一样的代码。界面刚起步时不必做得很花哨,能让表格正确翻页、跨年、显示闰年二月,核心源码就已经合格了。

5. 万年历源码开发中的 5 个常见坑:现象、原因与修法

算法和界面都搭好后,真正的麻烦往往藏在边界条件里。下面这 5 个坑是我在评审和调试中见过次数最多的,每一条都按“现象 → 原因 → 解决”来写,你可以直接照着排查自己的代码。

5.1 星期基准错位:整张日历从星期日挪到星期一

现象:打印某个月的日历,日期数字没错,但第一格总是空在星期天,表头却从星期一开始;或者相反,1 号本该是周二,却显示在周三那一列。原因:蔡勒公式返回的 0 可能被当作星期一,而表格第一列被设计成星期天或反了。解决:先单独打印DateAlgo.dayOfWeek(2024, 7, 1)确认返回值,再检查buildCells里的offset赋值。如果表头是“Mon 到 Sun”,那么 0 代表周日时,offset 应该等于firstDay;如果 0 代表周一,offset 就要写成(firstDay + 6) % 7。我建议把所有讨论星期几的逻辑统一成“0 表示星期日”,并写进类注释。

5.2 闰年漏判 400:2100 年 2 月凭空多出 29 号

现象:程序在 2024 年显示正确,改成 2100 年 2 月后,日历表格里出现了 29 日。原因:闰年判断只写了year % 4 == 0,没有排除能被 100 整除但不能被 400 整除的年份;2100 正好落在这一类。解决:把第二章的isLeapYear原样替换进项目,并在单元测试里加一条assertFalse(DateAlgo.isLeapYear(2100))。这个坑极容易在“看起来已经写对”的代码里出现,因为 2000 年是闰年、1900 年不是闰年,而 2100 年太远,手测很少跑到。

5.3 Calendar 与 LocalDate 混用:日期悄悄偏移一天

现象:本地运行万年历一切正常,打成 jar 包放到服务器上后,某天日期总是差一天。原因:代码里既用了Calendar.getInstance()又用了LocalDate.now(),前者依赖系统默认时区,后者读取系统当前日期;服务器时区被设置为 UTC 时,两个 API 的“今天”可能不同。解决:纯日历计算不要混用两套 API,显示日期统一用LocalDate,它不携带时区信息;如果一定要用Calendar,就用Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"))显式指定时区。万年历的本质是公历日期演算,牵扯进时区只会让问题变得玄学。

5.4 闰秒与夏令时:万年历到底要不要管

现象:有同学看到系统时间是 23:59:60,就认为万年历该处理闰秒;还有人问要不要考虑夏令时调整。原因:把“时间”和“日历”混在了一起。万年历展示的是日期格子,分钟和秒的闰秒调整不会改变今天是几号;夏令时只影响某天的小时长度,也不改变“7 月 15 日”这个日期。解决:源码里不引入时间计算,只用java.time.LocalDate和自写算法,这样既避开闰秒,也不会被时区规则影响。遇到有人质疑时,可以看 2017 年等有闰秒的日期,Calendar 显示当天依然是 24 小时,日期永远是连续整数,这就是万年历的正确模型。

5.5 中文表头打歪:终端里“一”和“10”的宽度战争

现象:把星期表头改成中文“一 二 三 四 五 六 日”后,日期列对不齐,有的行向左歪两格。原因:中文字符在常见终端里占两个英文宽度,String.format("%4s", "一")只按字符数算,导致中文比数字宽出一倍。解决:要么继续用英文表头,要么在打印时手动计算显示宽度。我常用一个简单方法:先输出表头“一 二 三 四 五 六 日”,每个中文字后面补一个空格,日期数字统一用%3d输出,这样中文和数字在大多数终端都能对齐;如果还歪,就改成 Grid 布局的 Swing 界面,让表格自己处理列宽,一劳永逸。

6. 进阶验证:用两行脚本扫遍 1900 到 2100 的每一天

万年历算得对不对,不能只靠肉眼抽查几个月。最可靠的办法是拿标准库当裁判:用LocalDate遍历一段年份范围,每天比较星期,顺手校验每月天数是否一致。这样能一次性暴露闰年判断、月份天数、跨世纪边界等问题。

import java.time.LocalDate; public class CalendarValidator { public static void main(String[] args) { for (int y = 1900; y <= 2100; y++) { for (int m = 1; m <= 12; m++) { int expectDays = DateAlgo.daysInMonth(y, m); int actualDays = LocalDate.of(y, m, 1).lengthOfMonth(); if (expectDays != actualDays) { System.out.println("days mismatch: " + y + "-" + m); } for (int d = 1; d <= expectDays; d++) { int expectWeek = DateAlgo.dayOfWeek(y, m, d); int actualWeek = LocalDate.of(y, m, d).getDayOfWeek().getValue() % 7; if (expectWeek != actualWeek) { System.out.println("week mismatch: " + y + "-" + m + "-" + d); } } } } System.out.println("validation done"); } }

注意比较前做了一个换算:LocalDate.getDayOfWeek().getValue()返回 1 到 7,分别表示周一到周日,转成 0 到 6 才能和蔡勒公式的结果对齐。这个验证器还可以进一步升级为 JUnit 测试,把校验逻辑写进@Test,以后每次改算法都自动重跑,防止“修了一个坑引出另一个坑”。如果输出为空且最后打印validation done,说明 1900 到 2100 的每一天都和标准库吻合,源码的日期正确性就可以放心了。

我自己的习惯是,这种验证器永远保留在项目里,哪怕最终交付的只是一个很小的万年历 demo,也会把“能被任何标准实现验证”当作完成标准。日期计算这种东西,错一天就是彻底错,靠眼睛看永远看不过来,能自动验证才有底气说源码是好的。希望这篇笔记能帮你在万年历这个经典题目上少踩几个坑,把代码写成能放心交付、能继续扩展的样子。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 10:15:21

S32K3xx车规MCU数据手册精读:从Cortex-M7架构到量产选型避坑指南

简介&#xff1a;S32K3xxDS.pdf 是 NXP S32K3xx 系列微控制器的官方数据手册&#xff0c;面向汽车电子、工业自动化领域的嵌入式软硬件工程师及高校相关专业学习者&#xff0c;用于快速掌握该系列芯片的规格参数与选型依据。资源包仅含 1 个 PDF 文件&#xff0c;大小约 5.14MB…

作者头像 李华
网站建设 2026/10/15 10:15:07

基于ThinkPHP与Laravel的医疗健康小程序开发实践

1. 项目背景与核心价值最近在帮本地一家社区医院做数字化升级时&#xff0c;发现他们原有的挂号系统存在几个痛点&#xff1a;患者需要在上班时间打电话预约、现场排队时间长、医生号源分配不合理。于是我们决定基于微信小程序开发一套健康管理系统&#xff0c;整合挂号预约、健…

作者头像 李华
网站建设 2026/10/15 10:14:39

PCB通孔电镀铜厚均匀性优化方案

1. 通孔PCB电镀铜厚一致性的核心挑战在PCB制造过程中&#xff0c;通孔电镀铜厚的均匀性直接影响着产品的可靠性和电气性能。我经历过多次因孔内铜厚不均导致的孔壁分离或阻抗异常问题&#xff0c;这往往发生在高纵横比&#xff08;>8:1&#xff09;的通孔中。当板厚达到3mm以…

作者头像 李华