简介:面向初学者与开发者的Java万年历程序代码,涵盖阳历与阴历转换、传统节日标注、日历表格展示与点击监听等核心功能。程序将日期计算、界面渲染和用户交互分层设计,代码结构清晰,便于阅读和二次开发,且支持任意年份查询,兼顾性能与准确性。资源包共10个文件,以java源文件、class字节码文件为主,另含工程配置文件,整体仅18KB,适合直接导入或对照学习。已有1357人学习下载,其中Lunar.java与Clock.java分别实现农历计算和时钟辅助,MainFrame.java负责主界面与事件监听,配套class文件便于运行验证。通过研读该项目,可掌握java.time日期操作、农历算法设计、节日判断逻辑、Swing界面布局及监听器应用等实用技能,也可作为课程设计或面试准备的参考实现。
1. JAVA万年历代码:拿到手先搞清它到底解决了什么
如果你只是随手搜“java万年历 阳历+阴历”,大概率会翻到一堆只做阳历、阴历靠查表硬编码、节气算错一整天还没人管的半成品。我最早帮某开发者的桌面小工具找日历模块时,就是被这种代码坑过一整天:农历二月跑着跑着就跳到三月,最后才发现是闰月数据没处理。这份“JAVA万年历程序代码(阳历+阴历+节日)”的价值在于,它把阳历、农历、节气、节日揉进了一套可运行的Java项目里,不是那种只贴三五个方法的碎片代码。哪怕你只是想要一个能算农历的类,也可以直接抽出核心算法改一改。
适合谁呢?一是做Java桌面应用但不想折腾日历算法的同学,二是需要给管理系统加个日期面板的前端转后端开发者,三是想拿现成代码练手、理解农历推算原理的新手。它不是让你背概念的教程,而是一份能直接跑起来、能改参数、能拆模块的工程代码。
2. 初始运行与项目结构:先让代码跑起来再谈算法
2.1 拿到代码包先看目录,别急着点运行
拆这份资源的第一步不是找Main方法,而是把压缩包解开后先看目录结构。常见做法是,这类万年历项目会分成三个核心包:date(日期计算核心)、lunar(农历与节气)、ui(控制台或Swing界面)。你可以搜一下项目里是否有README.md或doc目录,有些版本会附带自述文件,写明JDK版本和运行方式。
我一般会先用命令行扫一眼文件清单,确认入口类是谁,再决定是直接跑main还是写测试类调接口。清单大概会是下面这样:
| 文件/目录 | 作用 | 说明 |
|---|---|---|
src/date/SolarDate.java | 阳历日期对象封装 | 含年、月、日、星期计算 |
src/lunar/LunarCalendar.java | 农历推算主类 | 公历转农历、闰月处理 |
src/lunar/LunarData.java | 农历数据表 | 存放1900-2100年二进制年数据 |
src/festival/FestivalUtil.java | 节日与节气查询 | 返回法定节日、传统节日、节气名 |
src/Main.java | 控制台入口 | 演示调用示例 |
注意,这里的包名和类名可能与你拿到的压缩包略有出入,有的版本会把数据表放在resources目录下。核心思路是一个类负责数据、一个类负责推算、一个类负责对外输出,如果你拿到的版本结构完全不同,反而要多留个心眼,可能代码被改过结构。
2.2 环境准备与启动命令:JDK版本和中文乱码是两个门槛
跑Java项目,第一步是确认JDK版本。这份代码看似老派,但绝大多数实现都基于JDK 8的语法,个别版本可能用到了java.time包。建议直接用JDK 8或JDK 11试,不要一上来上JDK 17或21——不是不能跑,而是某些老代码用了Calendar的废弃方法,在新JDK下会有警告但不影响运行。
编译运行的方式有两种。如果项目带了build.xml(Ant)或pom.xml(Maven),那直接按对应构建工具来;如果只是纯源码目录,我一般直接命令行操作:
# 进入源码根目录 cd java-lunar-calendar # 编译所有Java源文件到out目录 javac -encoding UTF-8 -d out src/date/*.java src/lunar/*.java src/festival/*.java src/Main.java # 运行主类 java -Dfile.encoding=UTF-8 -cp out Main逻辑说明:-encoding UTF-8指定源文件编码,-d out指定编译输出目录,最后-cp out指定类路径并运行Main类。这里的Main是控制台入口,会打印当天的阳历日期、农历日期和节日名。
参数说明:如果你拿到的源码里中文注释编译报错,十有八九是操作系统的默认编码不是UTF-8。Windows下用chcp 65001切到UTF-8代码页再编译,Linux/macOS下直接加-encoding UTF-8就行。
2.3 控制台输出的调用姿势
跑通之后,别急着看结果准不准,先看它输出了什么字段。通常Main类里会这样写:
public static void main(String[] args) { // 获取当天日期 LocalDate today = LocalDate.now(); // 转为万年历核心对象 SolarDate solar = new SolarDate(today.getYear(), today.getMonthValue(), today.getDayOfMonth()); // 输出阳历信息 System.out.println("阳历:" + solar.getYear() + "-" + solar.getMonth() + "-" + solar.getDay()); System.out.println("星期:" + solar.getWeekName()); // 转农历并输出 LunarCalendar lunar = LunarCalendar.fromSolar(solar); System.out.println("农历:" + lunar.getLunarDayName() + " " + lunar.getLunarMonthName()); // 查节日和节气 FestivalUtil festival = new FestivalUtil(); System.out.println("节日/节气:" + festival.getFestivalOrTerm(solar, lunar)); }逻辑说明:这段代码把“今天”拆成阳历对象,再转农历对象,最后统一查节日。这里的fromSolar是静态工厂方法,内部通过查表计算农历偏移量。
参数说明:getLunarDayName()返回的是带“初几/廿几”的字符串,getLunarMonthName()返回“正月/二月/冬月”这类名称。getFestivalOrTerm返回空字符串时表示当天不是节日也不是节气,这时可以不做特殊显示。初学者最容易改错的地方是直接new多个LunarCalendar对象——这个类内部有状态,建议一个日期对应一个实例,不要复用。
3. 阳历模块拆解:闰年、星期与日期格式的三件套
3.1 闰年判断不能只记“四年一闰”
很多人写日历代码,闰年判断就写year % 4 == 0,然后就被2100年这种整百年份打脸。这个万年历项目里必然有一个静态方法处理它,而且按规则应该是这样:
/** * 判断是否为闰年:四年一闰,百年不闰,四百年再闰 */ public static boolean isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }逻辑说明:公历闰年的判定规则是“能被4整除但不能被100整除,或者能被400整除”。如果不加year % 100 != 0这个条件,公元1900年就会被误判为闰年,导致二月的天数算错,进而影响整个日期偏移量。
参数说明:该方法只接受一个year参数,传负数或0时逻辑仍然成立但无实际意义——因为项目数据表一般从1900年开始,负年份是查不到的。我在实际项目里通常会在调用处先校验年份范围。
3.2 星期计算:蔡勒公式还是Calendar类
星期计算的实现方式会直接影响代码对JDK版本的敏感度。老版本代码喜欢用java.util.Calendar,而新版本则偏好蔡勒公式或者java.time.LocalDate.getDayOfWeek()。这份资源里大概率是一个私有方法:
/** * 蔡勒公式计算星期,只适用于格里高利历 * 返回0=星期日,1=星期一,...,6=星期六 */ private static int zellerWeek(int year, int month, int day) { if (month < 3) { month += 12; year--; } int century = year / 100; int yearOfCentury = year % 100; int week = (yearOfCentury + yearOfCentury / 4 + century / 4 - 2 * century + (26 * (month + 1)) / 10 + day - 1) % 7; // 修正负数 return (week + 7) % 7; }逻辑说明:蔡勒公式是计算公历星期的经典公式,注意month < 3时要把月份当成上一年的13或14月处理,相当于把1月、2月归到前一年,这是公式要求,不是bug。
参数说明:week在Java的取余运算里可能是负数,所以最后要+ 7再取余。如果你在代码里看到week < 0 ? week + 7 : week的写法,效果一样。调用这个方法的类只需要传入年月日三个整数,不依赖任何外部状态。
3.3 日期格式化的隐藏细节:月份补零与星期中文映射
阳历模块的输出会涉及两个格式化问题:数字补零和星期中文映射。比如2024-3-7是你想要的,2024-03-07是日历面板需要的。项目里通常会有这样的代码:
/** * 格式化日期:YYYY-MM-DD */ public static String formatDate(int year, int month, int day) { return String.format("%04d-%02d-%02d", year, month, day); } /** * 星期数字转中文 */ public static String weekName(int weekIndex) { String[] names = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}; return names[weekIndex]; }逻辑说明:String.format的%02d就是不足两位补0,%04d就是年份不足四位补0。weekIndex即上一节zellerWeek返回的0-6整数。
参数说明:注意如果你入参的year是负数,%04d会把负号也算进长度里,输出类似-001-02-03,但这在万年历场景下不会出现,不用纠结。
这一章看着基础,但阳历模块是整个项目的“地基”。阴历换算最终要把结果映射回阳历日期,如果阳历这边都算不对,后面全是白搭。我在实际拆包时会把isLeapYear和zellerWeek抽出来单独测试,用2024年2月(闰年)和1900年2月(平年)各验一遍,过了再继续往下看。
4. 阴历与节日模块的算法细节:数据表、闰月与节气边界
4.1 农历年数据表是核心,看不懂位运算就看不懂日历
农历没有固定的数学公式可以套,通用的实现方案是“查表法”:把1900年到2100年每一年的农历信息压缩成一个整数或字符串,存在数组里。这份资源里的LunarData.java大概率就是干这个的。一张典型的数据表大概长这样:
/** * 农历年数据表,每项存储当年农历信息 * 具体位含义:0x0f 表示闰月月份,其他位表示每月大小(30天=大,29天=小) */ public class LunarData { // 为了可读性,这里只展示1900年的数据格式 private static final int[] LUNAR_INFO = { 0x04bd8, // 1900 0x04ae0, // 1901 0x0a570, // 1902 0x054d5, // 1903 0x0d260 // 1904 // ... 更多年份数据 }; /** * 获取农历某年某月是大月(30天)还是小月(29天) * @param year 公历年份,取值范围 1900-2100 * @param month 农历月份 * @return true=大月,false=小月 */ public static boolean isBigMonth(int year, int month) { int offset = year - 1900; int info = LUNAR_INFO[offset]; // 数据位从低到高,第month位表示第month月的大小 return ((info >> month) & 0x01) == 1; } }逻辑说明:每个int低4位存储闰月月份(0表示没有闰月),其余位从低到高依次代表正月到腊月是大月还是小月。>> month右移后再& 0x01,就是取出指定月份的位标记。
参数说明:LUNAR_INFO的索引从1900年开始,所以取数时必须year - 1900。如果传了2025年,对应的数据项就是LUNAR_INFO[125]。问题在于很多简化版代码只存到2050年,超出范围就报数组越界,这是最常见的运行时错误之一。
4.2 农历转换流程:从“正月初一”到“今天”的偏移量计算
拿到数据表之后,农历换算的核心思路是:先算出1900年1月31日(公认的农历正月初一)到你要求的那天相距多少天,再逐年减去每年农历天数,最后余下的天数在当前农历年内数月份。代码骨架如下:
/** * 公历日期转农历日期 */ public static LunarDate fromSolar(SolarDate solar) { // 以1900年1月31日为基准,计算偏移天数 long offset = calcOffsetDays(solar); int year = 1900; int lunarYear = year; int leapMonth = 0; while (offset >= LunarData.yearDays(lunarYear)) { offset -= LunarData.yearDays(lunarYear); lunarYear++; leapMonth = LunarData.leapMonth(lunarYear); } // 当前农历年的总天数,其中yearDays会依据闰月修正 int lunarMonth = 1; boolean leap = false; while (offset > 0) { int monthDays = LunarData.monthDays(lunarYear, lunarMonth, leap); if (offset < monthDays) { break; } offset -= monthDays; lunarMonth++; // 如果这个月是闰月,恢复非闰状态 if (leapMonth == lunarMonth) { leap = !leap; } } return new LunarDate(lunarYear, lunarMonth, (int) offset + 1, leap); }逻辑说明:两个while循环分别完成“跨年”和“跨月”两步。外层循环每减一次,就是减掉一整年的农历天数,内层循环每减一次,就是减掉一个农历月的天数。leapMonth则是当年闰月的月份编号,内层判断里leapMonth == lunarMonth说明当前处理到闰月了。
参数说明:calcOffsetDays是公历日期到基准日期的间隔天数,具体实现可以用LocalDate.toEpochDay()相减,也可以手工算。这个方法专为1900年之后设计,基准日之前的日期不在数据表覆盖范围内,这决定了该万年历不能查1900年以前的农历。
4.3 节日与节气的两套查询逻辑
节目查询分两类:固定公历节日(元旦、劳动节)和农历节日(春节、中秋),处理方式完全不同。固定节日直接比对月日,农历节日则是在转成农历后比对农历月日。核心实现大概是:
/** * 查询指定日期的节日或节气 * @return 节日名,没有则返回空字符串 */ public static String getFestivalOrTerm(SolarDate solar, LunarDate lunar) { // 第一类:固定公历节日 if (solar.getMonth() == 1 && solar.getDay() == 1) return "元旦"; if (solar.getMonth() == 5 && solar.getDay() == 1) return "劳动节"; // 第二类:固定农历节日 if (lunar.getMonth() == 1 && lunar.getDay() == 1) return "春节"; if (lunar.getMonth() == 5 && lunar.getDay() == 5) return "端午节"; if (lunar.getMonth() == 8 && lunar.getDay() == 15) return "中秋节"; // 第三类:节气(简化处理,详见下文) return getTerm(solar); }逻辑说明:字符串比对本身没有难度,难点在节气。节气是根据太阳在黄道上的位置划分的,每15度一个节气,共24个。精确计算需要天文算法,但工程上通常用一个近似公式:以某个基准年份的节气时刻为参照,逐年累积偏移量。
参数说明:节气返回的是一个字符串,且优先级高于节日。比如清明节当天既是节日又是节气,项目里的做法通常是直接返回“清明节”三个字,而不是额外再返回一个“清明”。如果你发现输出的节日和节气会同时出现两个字符串,那是优先级设计没做好。
4.4 节气算法的边界:为什么“差一天”是常态
很多开发者跑完节气查询后,对着日历App一比对,发现2024年的立春差了一天,就开始怀疑代码写错了。其实节气计算没那么简单,常见实现是直接用公式推算:
/** * 近似计算节气日,基于1900年的节气时刻表 */ private static String getTerm(SolarDate solar) { // 节气表:每月两个节气,先定义每个节气的基准日期 int[] termInfo = {6, 21, 6, 21, 6, 21, 6, 21, 6, 21, 6, 22}; int month = solar.getMonth(); int day = solar.getDay(); // 按下半月(>=20日)和上半月分别匹配 if (day >= termInfo[month - 1] && day <= termInfo[month - 1] + 1) { return TERM_NAMES[month * 2 - 2]; } return ""; }逻辑说明:这个简化版的思路是每个月的两个节气大致固定在6日和21日前后,具体日期由termInfo数组指定。这样实现能在大多数年份与标准历书匹配,但个别年份会偏差一到两天。
参数说明:termInfo数组代表每个月两个节气的基准日,如1月是6日和21日。如果你追求精确到小时级的节气时刻,这种代码是不够的,但如果你只是做桌面挂历显示,这个精度完全够用。我做过的项目中,用户投诉节气差一天的概率远低于闰月错位——后者才是最伤脑筋的。
5. 避坑清单:我翻过车也帮人排过雷的几个典型问题
5.1 农历闰月错位:二十几号直接跳到下个月
现象:某开发者在集成这段万年历代码后,发现2023年3月22日之后,农历直接显示闰二月初三,而标准日历显示的是三月初三。日期整体看起来提前了一个月。
原因:这类万年历代码处理闰月的逻辑里,通常用一个leapMonth标志位。如果数据表里2023年(0x0d460这类数据)的闰月字段解析不对,或者leap状态没有在闰月结束后重置,后面的月序就会整体错位。
解决:先单独查LunarData.leapMonth(2023)返回什么,正常情况下2023年应该返回2(闰二月)。如果返回了0或其它值,检查数据表定义是否包含了2023年,很多版本的数据表只写到2000年或2050年,缺少的年份返回默认值0,自然就没有闰月。
5.2 节气差一天:看着是算法问题,实际是数据表问题
现象:程序里2024年的小寒显示1月6日,而某日历App显示1月6日,另一款App显示1月5日。
原因:节气的准确时刻是每年起伏的,简化公式在个别年份会产生一天的漂移。多数情况下不是代码bug,而是“近似计算”和“精确历书”的天文差异。
解决:如果你只需要显示不参与逻辑计算,直接接受误差就好;如果你要拿节气做排班或农事提醒,建议换成精确计算,或者引入一份“节气校正表”。校正表可以做成year -> month -> day的映射,覆盖你需要的时间范围。
5.3 数组越界:查1900年之前的日期直接崩溃
现象:输入“1899年12月1日”,程序直接抛出ArrayIndexOutOfBoundsException。
原因:数据表从1900年开始,year - 1900得到负数,索引访问越界。
解决:在入口方法处加大范围校验:
if (year < 1900 || year > 2100) { throw new IllegalArgumentException("年份超出数据表范围: " + year); }这样至少是报错清晰,而不是让用户看一堆调用栈。进一步的做法是做一个年份范围提示,告诉用户只支持1900-2100年。
5.4 控制台中文乱码:不是代码问题,是编码问题
现象:运行后节日显示“�������”,农历月份显示一大串问号。
原因:Windows控制台默认GBK,而源码编译用的是UTF-8,运行时输出流的编码不一致。
解决:运行时加参数-Dfile.encoding=UTF-8,或者在代码里强制指定输出流编码:
System.setProperty("file.encoding", "UTF-8");另一种土办法是把运行结果重定向到文本文件再打开,文件可以自动识别编码。
5.5 星期结果错一天:月份边界条件没测
现象:2024年3月1日,程序显示星期五,而标准答案是星期五——但如果输入2024年2月29日,显示星期三,标准答案是星期四。
原因:蔡勒公式在二月的边界上容易受闰年影响,如果month < 3的月份归年逻辑处理不对,或者公式里century计算使用了错误的向下取整,就会差一天。
解决:用几个已知日期做回归验证,我用的固定测试用例是:2024年2月29日(星期四)、2000年3月1日(星期三)、1999年12月31日(星期五)。如果这三个都对,星期的正确率基本有保障。
6. 进阶玩法:用关键日期做回归验算,再把它嵌进Swing界面
如果代码能跑通、能查日期,这只是起点。我拿到任何日历类资源后,第一件事永远是拿几个“锚点日期”去验算。所谓锚点日期,就是那些在万年历上被反复验证过的特殊日期。项目实测中,我用过这么几组:
| 公历日期 | 期望农历 | 期望节气 |
|---|---|---|
| 2024年2月10日 | 正月初一(春节) | 无 |
| 2023年2月4日 | 正月十四 | 立春 |
| 2023年3月22日 | 闰二月初一 | 无 |
| 2024年4月4日 | 二月廿六 | 清明 |
把这几组数据输入到Main.java里跑一遍,如果全部匹配,再谈集成到UI都不迟。
接着可以做的第二步,是把控制台调用改成Swing界面的数据源。常见做法是新建一个CalendarPanel类,调用LunarCalendar.fromSolar()获取农历信息,再调用FestivalUtil.getFestivalOrTerm()获取节日文本。界面上放一个网格布局,每月刷新一次日期数据,就能把一个控制台程序改成桌面日历。这里有一个值得注意的细节:Swing的JTable渲染日期时,每个cell对应一个SolarDate实例,所以你需要保证每个日期的对象都是新建的,不要用静态变量缓存,否则切月份时数据会串。
最后一步是做异常兜底:当用户输入的年份超过数据表范围时,面板要给出提示而不是白屏。我不会在这类资源上追求“支持任意年份的农历转换”,因为数据表法决定了它的边界就摆在那里。如果你确实需要更早年份,可以考虑换用算法推算实现,但那已经不是“这份资源能解决的事”了。
从那次被闰月坑到之后,我每次拿到日历类代码都强制走一遍“锚点日期验算、越界校验、编码检查”三件套,不到五分钟就能判断一份代码能不能用。希望帮到你。
本文还有配套的精品资源,点击获取