Java中的日期时间API详解:从Date、Calendar到现代时间体系

文章目录
- 引言:Java日期时间处理的演进之路
- 第一章:时间的基础概念
- 第二章:第一代日期时间API——Date
- 第三章:日期格式化——DateFormat与SimpleDateFormat
- 第四章:第二代日期时间API——Calendar
- 第五章:第三代日期时间API——java.time(JSR 310)
- 第六章:新旧API对比与转换指南
- 第七章:最佳实践与常见陷阱
- 第八章:总结与展望
- 附录:常用代码片段速查

引言:Java日期时间处理的演进之路
在Java应用程序开发中,日期和时间的处理是极其常见的需求——记录操作时间、计算时间差、格式化输出、时区转换、订单超时计算、报表统计等场景都离不开日期时间API。然而,Java的日期时间API经历了一条曲折的演进道路。
Java 1.0时代,设计者简单粗暴地推出了java.util.Date类,但它存在诸多设计缺陷。Java 1.1引入了Calendar类试图弥补,却又带来了新的复杂性。直到Java 8,吸取了Joda-Time库的精华,推出了全新的java.time包(JSR 310),才真正解决了长久以来的痛点。
本文将深入剖析Java日期时间处理的三个时代:
- 第一代:
Date、DateFormat、SimpleDateFormat - 第二代:
Calendar、GregorianCalendar、TimeZone - 第三代:
LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Instant、DateTimeFormatter等
我们将从源码层面剖析其设计原理,探讨线程安全问题,提供最佳实践,并给出新旧API的转换指南。全文预计12000字以上,力求让你彻底掌握Java日期时间处理的方方面面。
第一章:时间的基础概念
在深入Java API之前,我们需要理解计算机系统中时间的基本表示方法。
1.1 时间原点:1970-01-01 UTC
几乎所有计算机系统都采用一个共同的时间原点——1970年1月1日 00:00:00 UTC(协调世界时)。这个时间点被称为Unix纪元(Unix Epoch)。
为什么选择这个时间?这主要源于Unix操作系统的历史原因。1970年左右,Unix系统诞生,设计者选择了一个相对"干净"的时间起点。此后,几乎所有类Unix系统(包括Linux、macOS)以及Java等编程语言都沿用了这一约定。
1.2 时间表示的两种模型
计算机系统中有两种表示时间的模型:
1. 面向人类的模型
- 包含年、月、日、时、分、秒等字段
- 受时区、历法、夏令时等因素影响
- 例如:“2026年2月20日 星期五 下午3:30”
2. 面向机器的模型
- 用一个整数表示时间线上的一个点
- 通常是从时间原点开始的毫秒数或秒数
- 不受时区影响,便于计算和比较
- 例如:
1773084600000L(毫秒数)
Java中,System.currentTimeMillis()返回的就是面向机器的模型——从1970-01-01 UTC到当前时间的毫秒数。
publicclassTimeConcept{publicstaticvoidmain(String[] args){long currentTimeMillis =System.currentTimeMillis();System.out.println("当前时间戳(毫秒): "+ currentTimeMillis);// 计算某段代码的耗时long start =System.currentTimeMillis();// 模拟耗时操作try{Thread.sleep(1000);}catch(InterruptedException e){}long end =System.currentTimeMillis();System.out.println("耗时: "+(end - start)+"ms");}}1.3 时区与历法
时区(TimeZone):由于地球的自转,不同经度的地区时间不同。时区将地球划分为24个区域,每个区域相差1小时。UTC(协调世界时)是时间基准,北京时间为UTC+8。
历法(Calendar):不同文化使用不同的历法系统,如公历(GregorianCalendar)、农历、伊斯兰历等。Java主要支持公历(ISO-8601标准),但Calendar设计时考虑了扩展性,可以支持其他历法。
第二章:第一代日期时间API——Date
java.util.Date是Java中最早出现的日期时间类,自JDK 1.0起就存在。
2.1 Date类的源码剖析
查看Date类的源码(以JDK 8为例),可以看到其核心实现:
packagejava.util;publicclassDateimplementsjava.io.Serializable,Cloneable,Comparable<Date>{// 核心:存储从1970-01-01 00:00:00 GMT开始的毫秒数privatetransientlong fastTime;// 还有一些过时的方法使用的内部字段privatetransientlong cdate;// 无参构造:获取当前时间publicDate(){this(System.currentTimeMillis());}// 带参构造:根据毫秒数创建Date对象publicDate(long date){ fastTime = date;}// 已废弃的构造方法(年份从1900开始,月份从0开始)@DeprecatedpublicDate(int year,int month,int date){this(year, month, date,0,0,0);}// 获取毫秒数publiclonggetTime(){returngetTimeImpl();}// 设置毫秒数publicvoidsetTime(long time){ fastTime = time; cdate =null;}// 比较日期publicbooleanbefore(Date when){returngetMillisOf(this)<getMillisOf(when);}publicbooleanafter(Date when){returngetMillisOf(this)>getMillisOf(when);}// 转换为字符串:dow mon dd hh:mm:ss zzz yyyypublicStringtoString(){// 实际实现依赖于系统时区returntoString(ZoneId.systemDefault());}// ... 其他方法}关键点分析:
- 核心字段
fastTime:这是一个long类型的变量,存储从1970-01-01 UTC开始的毫秒数。整个Date对象本质上就是这个数字的封装。 - 构造方法:只有两个构造方法被保留推荐使用——无参构造(获取当前时间)和带毫秒参数的构造。其他构造方法都被
@Deprecated标记,不再推荐使用。 - 主要方法:
getTime()和setTime()用于获取和设置毫秒数;before()、after()、compareTo()用于日期比较;toString()用于输出。
2.2 Date类的核心方法详解
2.2.1 创建Date对象
importjava.util.Date;publicclassDateCreation{publicstaticvoidmain(String[] args){// 方式1:无参构造,表示当前时间Date now =newDate();System.out.println("当前时间: "+ now);// 方式2:带毫秒参数Date date =newDate(1773084600000L);// 2026-02-20 具体时间取决于时区System.out.println("指定毫秒数的时间: "+ date);// 方式3:不推荐!从字符串解析(已废弃)@SuppressWarnings("deprecation")Date deprecated =newDate("2026/02/20");System.out.println("已废弃方式: "+ deprecated);}}2.2.2 日期比较
importjava.util.Date;publicclassDateComparison{publicstaticvoidmain(String[] args)throwsInterruptedException{Date date1 =newDate();Thread.sleep(100);// 暂停100毫秒Date date2 =newDate();// 使用before/after方法System.out.println("date1 before date2? "+ date1.before(date2));// trueSystem.out.println("date2 after date1? "+ date2.after(date1));// true// 使用compareTo方法(实现Comparable接口)int result = date1.compareTo(date2);System.out.println("compareTo结果: "+ result);// 负数表示date1 < date2// 使用equals方法System.out.println("是否相等? "+ date1.equals(date2));// false// 直接比较毫秒数System.out.println("毫秒比较: "+(date1.getTime()< date2.getTime()));// true}}2.2.3 获取/设置毫秒数
importjava.util.Date;publicclassDateMillis{publicstaticvoidmain(String[] args){Date now =newDate();// 获取毫秒数long millis = now.getTime();System.out.println("当前毫秒数: "+ millis);// 设置新的毫秒数 now.setTime(0L);// 设置为1970-01-01 08:00:00(北京时间,因为东八区)System.out.println("重置后: "+ now);// 通过毫秒数计算时间差Date start =newDate();// 模拟操作...Date end =newDate();long elapsed = end.getTime()- start.getTime();System.out.println("耗时: "+ elapsed +"ms");}}2.3 Date类的设计缺陷(为什么被废弃)
Date类的大部分方法在JDK 1.1之后就被标记为@Deprecated,主要原因如下:
缺陷1:年份从1900年开始
importjava.util.Date;publicclassDatePitfall{publicstaticvoidmain(String[] args){// 本意是2026年2月20日@SuppressWarnings("deprecation")Date date =newDate(2026,2,20);// 年份参数是2026吗?// 实际输出:3926-03-20(年份 = 2026 + 1900,月份2表示3月)System.out.println("实际结果: "+ date);// 正确的写法应该是:Date correct =newDate(126,2,20);// 年份 = 2026 - 1900 = 126System.out.println("正确结果: "+ correct);}}解释:Date(int year, int month, int date)构造方法中,year参数表示"year - 1900",所以传入2026相当于1900+2026=3926年。这完全是反直觉的设计。
缺陷2:月份从0开始
@SuppressWarnings("deprecation")Date date =newDate(126,2,20);// 月份2实际表示3月System.out.println(date);// 输出:Fri Mar 20 00:00:00 CST 2026解释:月份常量中,0代表1月,1代表2月,…,11代表12月。这与日常习惯(1-12月)完全不符,极易导致月份错误。
缺陷3:可变性导致的线程安全问题
importjava.util.Date;publicclassDateMutableProblem{publicstaticvoidmain(String[] args){Date date =newDate();System.out.println("原始日期: "+ date);// setTime方法可以修改Date对象内部状态 date.setTime(0L);System.out.println("被修改后: "+ date);// 原始对象被改变!// 在多线程环境下,这种可变性会导致数据不一致}}解释:Date是可变的,其setTime()等方法可以修改内部fastTime字段。在多线程环境中,如果多个线程共享同一个Date实例且进行修改,会产生竞态条件。
缺陷4:国际化支持薄弱
// Date的toString()输出格式固定,不考虑LocaleDate now =newDate();System.out.println(now);// 总是输出:Fri Feb 20 15:30:45 CST 2026// 无法直接输出中文格式的"2026年2月20日 星期五 下午3:30:45"解释:Date的toString()方法格式固定,不考虑不同国家和语言的日期表示习惯。
缺陷5:命名混淆
java.util.Date实际上既包含日期也包含时间,但类名却只叫"Date",容易让人误以为它只处理日期部分。
2.4 正确使用Date的建议
鉴于上述缺陷,官方建议:
- 只使用两个推荐的方法:
new Date()和new Date(long)构造、getTime()、setTime()、before()、after()、compareTo() - 不要使用任何
@Deprecated标记的方法 - 将Date作为"时间戳"的载体,不直接操作其日历字段
- 在需要日历字段操作时,使用Calendar类
// 推荐的使用方式:仅将Date作为时间戳对象Date now =newDate();// 当前时刻long timestamp = now.getTime();// 获取毫秒数// 如果需要操作年月日,使用CalendarCalendar cal =Calendar.getInstance(); cal.setTime(now);int year = cal.get(Calendar.YEAR);// 正确获取年份第三章:日期格式化——DateFormat与SimpleDateFormat
Date类本身无法控制输出的格式,因此Java提供了专门的格式化类DateFormat及其子类SimpleDateFormat,用于在Date对象和字符串之间进行转换。
3.1 DateFormat抽象类
java.text.DateFormat是一个抽象类,用于格式化/解析日期时间。
3.1.1 预定义样式常量
DateFormat定义了四种内置的显示风格:
| 常量 | 值 | 说明 |
|---|---|---|
DateFormat.FULL | 0 | 完整格式,如"2026年2月20日 星期五" |
DateFormat.LONG | 1 | 长格式,如"2026年2月20日" |
DateFormat.MEDIUM | 2 | 中等格式,如"2026-2-20"(默认风格) |
DateFormat.SHORT | 3 | 短格式,如"26-2-20" |
3.1.2 获取实例的工厂方法
// 获取日期格式化器DateFormat.getDateInstance();// 默认风格(MEDIUM)的日期格式化DateFormat.getDateInstance(int style);// 指定风格的日期格式化DateFormat.getDateInstance(int style,Locale locale);// 指定风格和区域// 获取时间格式化器DateFormat.getTimeInstance();// 默认风格的时间格式化DateFormat.getTimeInstance(int style);// 指定风格的时间格式化// 获取日期时间格式化器DateFormat.getDateTimeInstance();// 默认风格的日期时间格式化DateFormat.getDateTimeInstance(int dateStyle,int timeStyle);// 指定风格3.1.3 核心方法
// Date -> String 格式化publicfinalStringformat(Date date)// String -> Date 解析publicDateparse(String source)throwsParseException3.1.4 使用示例
importjava.text.DateFormat;importjava.util.Date;importjava.util.Locale;publicclassDateFormatDemo{publicstaticvoidmain(String[] args){Date now =newDate();// 不同风格的日期格式化DateFormat dfFull =DateFormat.getDateInstance(DateFormat.FULL,Locale.CHINA);DateFormat dfLong =DateFormat.getDateInstance(DateFormat.LONG,Locale.CHINA);DateFormat dfMedium =DateFormat.getDateInstance(DateFormat.MEDIUM,Locale.CHINA);DateFormat dfShort =DateFormat.getDateInstance(DateFormat.SHORT,Locale.CHINA);System.out.println("FULL格式: "+ dfFull.format(now));// 2026年2月20日 星期五System.out.println("LONG格式: "+ dfLong.format(now));// 2026年2月20日System.out.println("MEDIUM格式: "+ dfMedium.format(now));// 2026-2-20System.out.println("SHORT格式: "+ dfShort.format(now));// 26-2-20// 时间格式化DateFormat tf =DateFormat.getTimeInstance(DateFormat.MEDIUM,Locale.CHINA);System.out.println("时间: "+ tf.format(now));// 15:30:45// 日期时间格式化DateFormat dtf =DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.MEDIUM,Locale.CHINA);System.out.println("日期时间: "+ dtf.format(now));// 2026年2月20日 15:30:45// 解析字符串为Datetry{String dateStr ="2026-02-20";DateFormat parser =DateFormat.getDateInstance(DateFormat.MEDIUM,Locale.CHINA);Date parsed = parser.parse(dateStr);System.out.println("解析结果: "+ parsed);}catch(Exception e){ e.printStackTrace();}}}3.2 SimpleDateFormat——更灵活的格式化器
java.text.SimpleDateFormat是DateFormat的具体子类,允许通过模式字符串自定义日期时间格式。
3.2.1 构造方法
// 使用默认模式SimpleDateFormat()// 使用指定模式SimpleDateFormat(String pattern)// 使用指定模式和区域SimpleDateFormat(String pattern,Locale locale)3.2.2 常用模式字母
| 字母 | 日期/时间元素 | 示例 |
|---|---|---|
| y | 年 | yyyy -> 2026 |
| M | 月份 | MM -> 02, MMM -> 二月 |
| d | 月份中的天数 | dd -> 20 |
| E | 星期几 | EEEE -> 星期五 |
| a | AM/PM标记 | a -> 下午 |
| H | 小时(0-23) | HH -> 15 |
| h | 小时(1-12) | hh -> 03 |
| m | 分钟 | mm -> 30 |
| s | 秒 | ss -> 45 |
| S | 毫秒 | SSS -> 123 |
| z | 时区 | z -> CST |
| Z | RFC 822时区 | Z -> +0800 |
3.2.3 基本使用示例
importjava.text.SimpleDateFormat;importjava.util.Date;publicclassSimpleDateFormatDemo{publicstaticvoidmain(String[] args){Date now =newDate();// 1. 常用格式:yyyy-MM-dd HH:mm:ssSimpleDateFormat sdf1 =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println("格式1: "+ sdf1.format(now));// 2026-02-20 15:30:45// 2. 中文格式SimpleDateFormat sdf2 =newSimpleDateFormat("yyyy年MM月dd日 EEEE HH时mm分ss秒");System.out.println("格式2: "+ sdf2.format(now));// 2026年02月20日 星期五 15时30分45秒// 3. 带毫秒SimpleDateFormat sdf3 =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");System.out.println("格式3: "+ sdf3.format(now));// 2026-02-20 15:30:45.123// 4. 12小时制带AM/PMSimpleDateFormat sdf4 =newSimpleDateFormat("yyyy-MM-dd hh:mm:ss a");System.out.println("格式4: "+ sdf4.format(now));// 2026-02-20 03:30:45 下午// 5. 只显示日期SimpleDateFormat sdf5 =newSimpleDateFormat("yyyy/MM/dd");System.out.println("格式5: "+ sdf5.format(now));// 2026/02/20// 6. 字符串解析为Datetry{String dateStr ="2026-02-20 15:30:45";SimpleDateFormat parser =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date parsed = parser.parse(dateStr);System.out.println("解析结果: "+ parsed);}catch(Exception e){ e.printStackTrace();}}}3.2.4 模式匹配规则详解
importjava.text.SimpleDateFormat;importjava.util.Date;publicclassPatternDetail{publicstaticvoidmain(String[] args){Date now =newDate();// 年份:y个数影响显示位数System.out.println("yyyy: "+newSimpleDateFormat("yyyy").format(now));// 2026System.out.println("yy: "+newSimpleDateFormat("yy").format(now));// 26// 月份:M个数影响格式System.out.println("M: "+newSimpleDateFormat("M").format(now));// 2System.out.println("MM: "+newSimpleDateFormat("MM").format(now));// 02System.out.println("MMM: "+newSimpleDateFormat("MMM").format(now));// 二月System.out.println("MMMM: "+newSimpleDateFormat("MMMM").format(now));// 二月// 日期:d个数影响格式System.out.println("d: "+newSimpleDateFormat("d").format(now));// 20System.out.println("dd: "+newSimpleDateFormat("dd").format(now));// 20// 星期:E个数影响格式System.out.println("E: "+newSimpleDateFormat("E").format(now));// 星期五System.out.println("EEEE: "+newSimpleDateFormat("EEEE").format(now));// 星期五// 小时:H(0-23) vs h(1-12)System.out.println("H: "+newSimpleDateFormat("H").format(now));// 15System.out.println("HH: "+newSimpleDateFormat("HH").format(now));// 15System.out.println("h: "+newSimpleDateFormat("h").format(now));// 3System.out.println("hh: "+newSimpleDateFormat("hh").format(now));// 03}}3.3 SimpleDateFormat的线程安全问题
核心问题:SimpleDateFormat是线程不安全的。
3.3.1 问题根源——源码分析
查看SimpleDateFormat的源码,可以发现它继承自DateFormat,而DateFormat中定义了一个protected的Calendar对象:
// DateFormat.javapublicabstractclassDateFormatextendsFormat{protectedCalendar calendar;// 共享的Calendar实例// ...}// SimpleDateFormat.javapublicclassSimpleDateFormatextendsDateFormat{// 在format方法中会使用calendarprivateStringBufferformat(Date date,StringBuffer toAppendTo,FieldDelegate delegate){// 关键点:这里修改了calendar的状态! calendar.setTime(date);// ... 后续使用calendar进行格式化return toAppendTo;}}问题分析:
calendar是DateFormat类的成员变量,被SimpleDateFormat继承- 在
format()方法中,首先调用calendar.setTime(date)修改calendar的状态 - 在多线程环境下,如果多个线程共享同一个
SimpleDateFormat实例,线程A执行calendar.setTime()后可能被暂停,线程B开始执行并再次修改calendar,导致线程A后续使用的calendar状态已被改变,产生错误结果甚至程序崩溃
3.3.2 问题复现
importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassSimpleDateFormatThreadProblem{// 共享的SimpleDateFormat实例(线程不安全)privatestaticfinalSimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");publicstaticvoidmain(String[] args){ExecutorService executor =Executors.newFixedThreadPool(10);for(int i =0; i <100; i++){finalint num = i; executor.submit(()->{try{// 多个线程同时调用format方法String dateStr = sdf.format(newDate());System.out.println("Thread "+ num +": "+ dateStr);// 可能会输出错误结果,或抛出异常}catch(Exception e){System.out.println("Thread "+ num +" exception: "+ e);}});} executor.shutdown();}}运行上述代码,可能出现:
- 日期时间错误(如月份变为0)
- 抛出
NumberFormatException等异常 - 程序挂死
3.3.3 解决方案
方案1:每次使用时创建新实例
publicclassSafeDateFormat1{publicstaticStringformatDate(Date date){// 每次方法调用都创建新实例,避免共享SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.format(date);}publicstaticDateparse(String dateStr)throwsParseException{SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.parse(dateStr);}}优点:简单可靠
缺点:频繁创建对象,有一定性能开销,但在大多数应用中可接受
方案2:使用ThreadLocal
importjava.text.SimpleDateFormat;importjava.util.Date;publicclassSafeDateFormat2{// 每个线程持有自己的SimpleDateFormat实例privatestaticfinalThreadLocal<SimpleDateFormat> DATE_FORMAT =ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd HH:mm:ss"));publicstaticStringformatDate(Date date){return DATE_FORMAT.get().format(date);}publicstaticDateparse(String dateStr)throwsException{return DATE_FORMAT.get().parse(dateStr);}// 清理(通常在web请求结束时调用)publicstaticvoidremove(){ DATE_FORMAT.remove();}}优点:线程安全,避免了重复创建的开销
缺点:需要注意在线程池环境中及时清理,防止内存泄漏
方案3:同步加锁
publicclassSafeDateFormat3{privatestaticfinalSimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");publicstaticsynchronizedStringformatDate(Date date){return sdf.format(date);}publicstaticsynchronizedDateparse(String dateStr)throwsException{return sdf.parse(dateStr);}}优点:简单
缺点:并发性能差,多个线程需要排队
方案4:使用Java 8的DateTimeFormatter(最佳方案)
importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;publicclassSafeDateFormat4{// DateTimeFormatter是不可变且线程安全的privatestaticfinalDateTimeFormatter formatter =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");publicstaticStringformatDate(LocalDateTime dateTime){return dateTime.format(formatter);}publicstaticLocalDateTimeparse(String dateStr){returnLocalDateTime.parse(dateStr, formatter);}}推荐方案:在新项目中,直接使用Java 8的DateTimeFormatter;在维护旧项目时,使用ThreadLocal包装SimpleDateFormat。
第四章:第二代日期时间API——Calendar
为了解决Date类的缺陷,JDK 1.1引入了java.util.Calendar类,它是一个抽象类,提供了更强大的日期字段操作能力。
4.1 Calendar类的设计思想
Calendar的设计目标是:
- 字段化:将日期时间分解为年、月、日、时、分、秒等独立字段
- 国际化:支持不同时区和语言环境
- 计算能力:支持日期的加减、滚动等操作
- 扩展性:可以支持不同的历法系统(如公历、农历、日本历等)
4.2 Calendar类的源码结构
publicabstractclassCalendarimplementsSerializable,Cloneable,Comparable<Calendar>{// 核心字段:存储时间毫秒数protectedlong time;// 标记time是否被设置过protectedboolean isTimeSet;// 存储各个日历字段的值(如YEAR、MONTH等)protectedint fields[];// 标记fields中哪些字段被设置过protectedboolean isSet[];// 时区privateTimeZone zone;// 17个静态常量,作为fields数组的索引publicstaticfinalint ERA =0;publicstaticfinalint YEAR =1;publicstaticfinalint MONTH =2;publicstaticfinalint WEEK_OF_YEAR =3;publicstaticfinalint WEEK_OF_MONTH =4;publicstaticfinalint DATE =5;// 同DAY_OF_MONTHpublicstaticfinalint DAY_OF_MONTH =5;publicstaticfinalint DAY_OF_YEAR =6;publicstaticfinalint DAY_OF_WEEK =7;publicstaticfinalint DAY_OF_WEEK_IN_MONTH =8;publicstaticfinalint AM_PM =9;publicstaticfinalint HOUR =10;// 12小时制(0-11)publicstaticfinalint HOUR_OF_DAY =11;// 24小时制(0-23)publicstaticfinalint MINUTE =12;publicstaticfinalint SECOND =13;publicstaticfinalint MILLISECOND =14;publicstaticfinalint ZONE_OFFSET =15;publicstaticfinalint DST_OFFSET =16;// 共17个字段,索引0-16publicstaticfinalint FIELD_COUNT =17;// 获取实例的工厂方法publicstaticCalendargetInstance(){returncreateCalendar(TimeZone.getDefault(),Locale.getDefault(Locale.Category.FORMAT));}// 抽象方法:子类实现具体的历法计算protectedabstractvoidcomputeTime();protectedabstractvoidcomputeFields();// 核心方法:获取字段值publicintget(int field){complete();// 确保fields数组是最新的returninternalGet(field);}// 设置字段值publicvoidset(int field,int value){// 如果设置了某个字段,time就不再准确,需要重新计算 isTimeSet =false; fields[field]= value; isSet[field]=true;}// 日期计算:增加/减少指定字段的值(会进位)publicabstractvoidadd(int field,int amount);// 日期滚动:增加/减少指定字段的值(不会进位)publicvoidroll(int field,int amount);// 获取对应的Date对象publicfinalDategetTime(){returnnewDate(getTimeInMillis());}// 设置时间publicfinalvoidsetTime(Date date){setTimeInMillis(date.getTime());}}关键设计解析:
- 双重表示:
Calendar内部同时维护两种表示——time(毫秒数)和fields[](字段数组)。当修改字段时,isTimeSet标记设为false,表示time需要重新计算;当设置时间时,fields[]会被重新计算。 - 延迟计算:
get(int field)方法调用complete(),如果isTimeSet为false,则调用computeTime()从fields[]计算time;如果fields[]不完整,则调用computeFields()从time计算fields[]。这种延迟计算提高了性能。 - 17个常量:这些常量作为
fields[]数组的索引,每个常量代表一个日历字段。理解这些常量是使用Calendar的基础。
4.3 获取Calendar实例
Calendar是抽象类,不能直接new。通过工厂方法获取实例:
importjava.util.Calendar;importjava.util.TimeZone;importjava.util.Locale;publicclassCalendarInstance{publicstaticvoidmain(String[] args){// 1. 默认时区和语言环境(通常使用系统默认值)Calendar cal1 =Calendar.getInstance();System.out.println("默认: "+ cal1.getTime());// 2. 指定时区Calendar cal2 =Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));System.out.println("纽约时区: "+ cal2.getTime());// 3. 指定语言环境Calendar cal3 =Calendar.getInstance(Locale.US);System.out.println("美国语言环境: "+ cal3.getTime());// 4. 同时指定时区和语言环境Calendar cal4 =Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"),Locale.CHINA );System.out.println("上海时区+中国: "+ cal4.getTime());}}getInstance()内部调用createCalendar(),默认返回GregorianCalendar(公历)实例。
4.4 Calendar核心方法详解
4.4.1 获取字段值:get(int field)
importjava.util.Calendar;publicclassCalendarGet{publicstaticvoidmain(String[] args){Calendar cal =Calendar.getInstance();// 获取各个字段的值int year = cal.get(Calendar.YEAR);int month = cal.get(Calendar.MONTH);// 注意:0代表1月!int day = cal.get(Calendar.DAY_OF_MONTH);int hour12 = cal.get(Calendar.HOUR);// 12小时制int hour24 = cal.get(Calendar.HOUR_OF_DAY);// 24小时制int minute = cal.get(Calendar.MINUTE);int second = cal.get(Calendar.SECOND);int millis = cal.get(Calendar.MILLISECOND);int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);// 1=周日, 2=周一, ..., 7=周六int dayOfYear = cal.get(Calendar.DAY_OF_YEAR);int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR);int weekOfMonth = cal.get(Calendar.WEEK_OF_MONTH);int ampm = cal.get(Calendar.AM_PM);// 0=上午, 1=下午System.out.printf("年: %d%n", year);System.out.printf("月: %d (实际月份=%d)%n", month, month +1);// 记得+1System.out.printf("日: %d%n", day);System.out.printf("24小时制: %d%n", hour24);System.out.printf("12小时制: %d %s%n", hour12, ampm ==0?"AM":"PM");System.out.printf("分: %d%n", minute);System.out.printf("秒: %d%n", second);System.out.printf("毫秒: %d%n", millis);System.out.printf("星期: %d (1=周日, 2=周一)%n", dayOfWeek);System.out.printf("一年中的第几天: %d%n", dayOfYear);System.out.printf("一年中的第几周: %d%n", weekOfYear);}}重要提醒:
Calendar.MONTH从0开始(0=一月,11=十二月)Calendar.DAY_OF_WEEK:1=周日,2=周一,…,7=周六
4.4.2 设置字段值:set(int field, int value) 和 set(int year, int month, int date)
importjava.util.Calendar;publicclassCalendarSet{publicstaticvoidmain(String[] args){Calendar cal =Calendar.getInstance();// 方法1:逐个字段设置 cal.set(Calendar.YEAR,2026); cal.set(Calendar.MONTH,1);// 1 = 二月 cal.set(Calendar.DAY_OF_MONTH,20); cal.set(Calendar.HOUR_OF_DAY,15); cal.set(Calendar.MINUTE,30); cal.set(Calendar.SECOND,45); cal.set(Calendar.MILLISECOND,123);System.out.println("设置后: "+ cal.getTime());// 方法2:一次设置年月日Calendar cal2 =Calendar.getInstance(); cal2.set(2026,1,20);// 年, 月, 日 (月从0开始)System.out.println("年月日: "+ cal2.getTime());// 方法3:一次设置年月日时分秒Calendar cal3 =Calendar.getInstance(); cal3.set(2026,1,20,15,30,45);// 年,月,日,时,分,秒System.out.println("完整设置: "+ cal3.getTime());// 注意:月份偏移问题Calendar wrong =Calendar.getInstance(); wrong.set(2026,2,20);// 本意是2026年2月20日?不对,2表示3月!System.out.println("错误设置(月份2): "+ wrong.getTime());// 实际是3月20日}}4.4.3 日期计算:add(int field, int amount)
add()方法按照日历规则,对指定字段增加/减少指定值,会进位到更高字段。
importjava.util.Calendar;importjava.text.SimpleDateFormat;publicclassCalendarAdd{publicstaticvoidmain(String[] args){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");Calendar cal =Calendar.getInstance(); cal.set(2026,1,20,23,59,59);// 2026-02-20 23:59:59System.out.println("原始时间: "+ sdf.format(cal.getTime()));// 加10秒(会进位到分钟) cal.add(Calendar.SECOND,10);System.out.println("加10秒后: "+ sdf.format(cal.getTime()));// 2026-02-21 00:00:09// 重置 cal.set(2026,1,20,23,59,59);// 加1分钟(会进位到小时) cal.add(Calendar.MINUTE,1);System.out.println("加1分钟后: "+ sdf.format(cal.getTime()));// 2026-02-21 00:00:59// 加1小时(会进位到天) cal.set(2026,1,20,23,59,59); cal.add(Calendar.HOUR_OF_DAY,1);System.out.println("加1小时后: "+ sdf.format(cal.getTime()));// 2026-02-21 00:59:59// 加1个月(会进位到年) cal.set(2026,11,20);// 2026-12-20 cal.add(Calendar.MONTH,1);System.out.println("加1个月后: "+ sdf.format(cal.getTime()));// 2027-01-20// 负数表示减去 cal.set(2026,0,1);// 2026-01-01 cal.add(Calendar.DAY_OF_MONTH,-1);System.out.println("减1天后: "+ sdf.format(cal.getTime()));// 2025-12-31}}典型应用:计算30天后、3个月前、5年后等。
4.4.4 日期滚动:roll(int field, int amount)
roll()与add()类似,但不会进位到更高字段。
importjava.util.Calendar;importjava.text.SimpleDateFormat;publicclassCalendarRoll{publicstaticvoidmain(String[] args){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");Calendar cal =Calendar.getInstance();// 示例1:月份滚动 cal.set(2026,11,20);// 2026-12-20System.out.println("原始: "+ sdf.format(cal.getTime())); cal.roll(Calendar.MONTH,1);// 月份+1,但不进位System.out.println("roll +1月: "+ sdf.format(cal.getTime()));// 2026-01-20!// 解释:12月roll 1个月,按照月份范围1-12,12+1=13,但不会进位到年,所以回到1月// 示例2:日期滚动(月份不同长度) cal.set(2026,0,31);// 2026-01-31System.out.println("原始: "+ sdf.format(cal.getTime())); cal.roll(Calendar.DAY_OF_MONTH,1);// 日期+1,但不进位System.out.println("roll +1天: "+ sdf.format(cal.getTime()));// 2026-01-01// 解释:1月31日加1天,日期范围1-31,31+1=32,不会进位到2月,而是回到1月1日// 对比add的行为 cal.set(2026,0,31); cal.add(Calendar.DAY_OF_MONTH,1);System.out.println("add +1天: "+ sdf.format(cal.getTime()));// 2026-02-01(进位)}}适用场景:当你只想在字段范围内循环(如调整日期但不想改变月份)时使用。
4.4.5 清空与设置宽松模式
importjava.util.Calendar;publicclassCalendarClearLenient{publicstaticvoidmain(String[] args){Calendar cal =Calendar.getInstance();// 1. clear():清空所有字段,设置为1970-01-01 00:00:00 cal.clear();System.out.println("clear后: "+ cal.getTime());// 2. clear(int field):清空指定字段 cal.set(2026,1,20); cal.clear(Calendar.HOUR_OF_DAY); cal.clear(Calendar.MINUTE); cal.clear(Calendar.SECOND);System.out.println("清空时间后: "+ cal.getTime());// 日期不变,时间为00:00:00// 3. setLenient:设置宽松/严格模式 cal.setLenient(true);// 默认:宽松模式,允许非法字段值自动修正 cal.set(2026,1,31);// 2月31日不存在System.out.println("宽松模式: "+ cal.getTime());// 自动修正为2026-03-03或03-02(取决于具体实现) cal.setLenient(false);// 严格模式 cal.set(2026,1,31);try{System.out.println(cal.getTime());// 抛出IllegalArgumentException}catch(IllegalArgumentException e){System.out.println("严格模式下非法日期抛出异常");}}}宽松模式:自动将非法字段值调整为合法值,如2月31日变为3月2日或3月3日(具体规则依赖于实现)。
严格模式:字段值非法时抛出异常,适合需要严格校验的场景。
4.5 Calendar的优缺点分析
优点
- 丰富的字段操作:提供了17个日历字段,可以精确获取和设置各个部分
- 强大的计算能力:
add()和roll()方法支持灵活的日期运算 - 国际化支持:内置时区和Locale处理
- 历法扩展性:可以支持不同的历法系统
缺点
- API复杂繁琐:使用时需要记住大量常量,代码冗长
- 月份偏移陷阱:月份从0开始,极易出错
- 线程不安全:内部状态可变,多线程共享需同步
- 可变性:方法调用会修改对象内部状态,不符合函数式编程思想
- 性能开销:相对重量级,频繁创建有性能问题
- 设计缺陷:某些方法行为不够直观(如
roll)
4.6 Calendar与Date的转换
importjava.util.Calendar;importjava.util.Date;publicclassCalendarDateConversion{publicstaticvoidmain(String[] args){// Calendar -> DateCalendar cal =Calendar.getInstance();Date dateFromCal = cal.getTime();System.out.println("Calendar转Date: "+ dateFromCal);// Date -> CalendarDate now =newDate();Calendar calFromDate =Calendar.getInstance(); calFromDate.setTime(now);System.out.println("Date转Calendar: "+ calFromDate.getTime());// 获取毫秒数long millisFromCal = cal.getTimeInMillis();long millisFromDate = now.getTime();System.out.println("毫秒数相等: "+(millisFromCal == millisFromDate));}}4.7 Calendar常用场景示例
场景1:计算两个日期之间的天数差
importjava.util.Calendar;importjava.util.Date;publicclassDateDiff{publicstaticintdaysBetween(Date startDate,Date endDate){Calendar startCal =Calendar.getInstance(); startCal.setTime(startDate);Calendar endCal =Calendar.getInstance(); endCal.setTime(endDate);// 重置时间到午夜,避免时分秒影响 startCal.set(Calendar.HOUR_OF_DAY,0); startCal.set(Calendar.MINUTE,0); startCal.set(Calendar.SECOND,0); startCal.set(Calendar.MILLISECOND,0); endCal.set(Calendar.HOUR_OF_DAY,0); endCal.set(Calendar.MINUTE,0); endCal.set(Calendar.SECOND,0); endCal.set(Calendar.MILLISECOND,0);long millis1 = startCal.getTimeInMillis();long millis2 = endCal.getTimeInMillis();long diff = millis2 - millis1;return(int)(diff /(24*60*60*1000));}publicstaticvoidmain(String[] args){Calendar cal1 =Calendar.getInstance(); cal1.set(2026,0,1);// 2026-01-01Calendar cal2 =Calendar.getInstance(); cal2.set(2026,11,31);// 2026-12-31int days =daysBetween(cal1.getTime(), cal2.getTime());System.out.println("2026年共有: "+(days +1)+"天");// 365天}}场景2:获取某月的第一天和最后一天
importjava.util.Calendar;publicclassMonthBoundary{publicstaticvoidmain(String[] args){Calendar cal =Calendar.getInstance(); cal.set(2026,1,15);// 2026-02-15// 第一天 cal.set(Calendar.DAY_OF_MONTH,1);System.out.println("本月第一天: "+ cal.getTime());// 最后一天 cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));System.out.println("本月最后一天: "+ cal.getTime());}}场景3:判断闰年
importjava.util.Calendar;importjava.util.GregorianCalendar;publicclassLeapYearCheck{publicstaticbooleanisLeapYear(int year){GregorianCalendar cal =newGregorianCalendar();return cal.isLeapYear(year);}publicstaticvoidmain(String[] args){System.out.println("2024是闰年? "+isLeapYear(2024));// trueSystem.out.println("2026是闰年? "+isLeapYear(2026));// falseSystem.out.println("2000是闰年? "+isLeapYear(2000));// true(世纪年规则)}}第五章:第三代日期时间API——java.time(JSR 310)
鉴于第一代Date和第二代Calendar的种种缺陷,Java 8引入了全新的日期时间API——java.time包(JSR 310),它深受Joda-Time库的启发,提供了更优雅、更安全、更强大的日期时间处理能力。
5.1 设计哲学与核心原则
Java 8日期时间API的设计遵循以下核心原则:
- 不可变性:所有核心类都是
final的,且内部状态不可变,任何修改操作都返回新对象,天然线程安全 - 清晰分离:将日期、时间、日期时间、带时区的日期时间等概念清晰分离,通过不同类表示
- 流畅API:方法命名直观,支持链式调用
- ISO标准:默认采用ISO-8601国际标准
- 线程安全:所有类都是不可变的,可在多线程环境下安全共享
5.2 核心类概览
| 类名 | 用途 | 示例 |
|---|---|---|
LocalDate | 只处理日期(年、月、日) | 2026-02-20 |
LocalTime | 只处理时间(时、分、秒、纳秒) | 15:30:45.123 |
LocalDateTime | 处理日期+时间(无时区) | 2026-02-20T15:30:45 |
ZonedDateTime | 带时区的日期时间 | 2026-02-20T15:30:45+08:00[Asia/Shanghai] |
OffsetDateTime | 带偏移量的日期时间 | 2026-02-20T15:30:45+08:00 |
OffsetTime | 带偏移量的时间 | 15:30:45+08:00 |
Instant | 时间戳(机器时间) | 2026-02-20T07:30:45Z |
Year | 年份 | 2026 |
YearMonth | 年月 | 2026-02 |
MonthDay | 月日 | –02-20 |
Period | 日期期间隔(年、月、日) | P1Y2M3D |
Duration | 时间间隔(时、分、秒、纳秒) | PT15H30M |
DateTimeFormatter | 格式化/解析(线程安全) | |
ZoneId | 时区ID | Asia/Shanghai |
ZoneOffset | 时区偏移量 | +08:00 |
5.3 LocalDate——只处理日期
LocalDate是一个不可变的日期对象,表示年-月-日,不包含时间和时区信息。
5.3.1 创建LocalDate
importjava.time.LocalDate;importjava.time.Month;importjava.time.format.DateTimeFormatter;publicclassLocalDateCreation{publicstaticvoidmain(String[] args){// 1. 当前日期LocalDate now =LocalDate.now();System.out.println("当前日期: "+ now);// 2026-02-20// 2. 指定年月日LocalDate date1 =LocalDate.of(2026,2,20);LocalDate date2 =LocalDate.of(2026,Month.FEBRUARY,20);// 使用Month枚举System.out.println("指定日期: "+ date1);// 3. 从字符串解析(ISO格式:yyyy-MM-dd)LocalDate parsed1 =LocalDate.parse("2026-02-20");System.out.println("解析ISO: "+ parsed1);// 4. 从字符串解析(自定义格式)DateTimeFormatter formatter =DateTimeFormatter.ofPattern("yyyy/MM/dd");LocalDate parsed2 =LocalDate.parse("2026/02/20", formatter);System.out.println("解析自定义: "+ parsed2);// 5. 从年日获取LocalDate fromYearDay =LocalDate.ofYearDay(2026,51);// 2026年的第51天 = 2月20日System.out.println("年日转换: "+ fromYearDay);// 6. 从纪元日获取LocalDate fromEpoch =LocalDate.ofEpochDay(20489);// 从1970-01-01开始的天数System.out.println("纪元日转换: "+ fromEpoch);}}重要:LocalDate的月份从1开始,符合人类直觉,再也不需要month+1了!
5.3.2 获取字段值
importjava.time.LocalDate;importjava.time.DayOfWeek;publicclassLocalDateGet{publicstaticvoidmain(String[] args){LocalDate date =LocalDate.of(2026,2,20);int year = date.getYear();// 2026int month = date.getMonthValue();// 2(1-12)Month monthEnum = date.getMonth();// FEBRUARYint day = date.getDayOfMonth();// 20int dayOfYear = date.getDayOfYear();// 51DayOfWeek dayOfWeek = date.getDayOfWeek();// FRIDAYSystem.out.printf("年: %d%n", year);System.out.printf("月: %d (%s)%n", month, monthEnum);System.out.printf("日: %d%n", day);System.out.printf("一年中的第几天: %d%n", dayOfYear);System.out.printf("星期: %s%n", dayOfWeek);// 检查某些特征System.out.println("是否闰年? "+ date.isLeapYear());// falseSystem.out.println("本月长度: "+ date.lengthOfMonth());// 28(2026年2月)System.out.println("本年长度: "+ date.lengthOfYear());// 365}}5.3.3 日期运算(加减)
LocalDate的运算返回新的LocalDate对象,原对象不变。
importjava.time.LocalDate;importjava.time.temporal.ChronoUnit;publicclassLocalDatePlusMinus{publicstaticvoidmain(String[] args){LocalDate date =LocalDate.of(2026,2,20);System.out.println("原始日期: "+ date);// 加天数LocalDate plusDays = date.plusDays(10);System.out.println("加10天: "+ plusDays);// 2026-03-02// 加周数LocalDate plusWeeks = date.plusWeeks(2);System.out.println("加2周: "+ plusWeeks);// 2026-03-06// 加月数LocalDate plusMonths = date.plusMonths(1);System.out.println("加1月: "+ plusMonths);// 2026-03-20// 加年数LocalDate plusYears = date.plusYears(5);System.out.println("加5年: "+ plusYears);// 2031-02-20// 使用通用方法(ChronoUnit枚举)LocalDate plus = date.plus(3,ChronoUnit.WEEKS);System.out.println("加3周(ChronoUnit): "+ plus);// 2026-03-13// 减法LocalDate minusDays = date.minusDays(5);System.out.println("减5天: "+ minusDays);// 2026-02-15// 链式调用LocalDate result = date.plusYears(1).plusMonths(2).minusDays(3);System.out.println("链式运算: "+ result);// 2027-04-17}}5.3.4 日期比较
importjava.time.LocalDate;publicclassLocalDateCompare{publicstaticvoidmain(String[] args){LocalDate date1 =LocalDate.of(2026,2,20);LocalDate date2 =LocalDate.of(2026,3,15);LocalDate date3 =LocalDate.of(2026,2,20);// 比较方法System.out.println("date1 before date2? "+ date1.isBefore(date2));// trueSystem.out.println("date1 after date2? "+ date1.isAfter(date2));// falseSystem.out.println("date1 equals date3? "+ date1.equals(date3));// true// compareTo方法(实现Comparable)int cmp = date1.compareTo(date2);System.out.println("compareTo结果: "+ cmp);// 负数(-1或更小)// 与当前日期比较LocalDate now =LocalDate.now();System.out.println("date1是否早于今天? "+ date1.isBefore(now));}}5.4 LocalTime——只处理时间
LocalTime表示时间(时、分、秒、纳秒),不包含日期和时区。
importjava.time.LocalTime;importjava.time.format.DateTimeFormatter;importjava.time.temporal.ChronoUnit;publicclassLocalTimeDemo{publicstaticvoidmain(String[] args){// 1. 创建LocalTime now =LocalTime.now();System.out.println("当前时间: "+ now);// 15:30:45.123LocalTime time1 =LocalTime.of(15,30);// 15:30LocalTime time2 =LocalTime.of(15,30,45);// 15:30:45LocalTime time3 =LocalTime.of(15,30,45,123456789);// 15:30:45.123456789LocalTime parsed =LocalTime.parse("15:30:45");LocalTime parsedCustom =LocalTime.parse("15-30-45",DateTimeFormatter.ofPattern("HH-mm-ss"));// 2. 获取字段System.out.println("小时: "+ time2.getHour());// 15System.out.println("分钟: "+ time2.getMinute());// 30System.out.println("秒: "+ time2.getSecond());// 45System.out.println("纳秒: "+ time2.getNano());// 0// 3. 运算LocalTime plusHours = time2.plusHours(2);LocalTime plusMinutes = time2.plusMinutes(30);LocalTime plusSeconds = time2.plus(30,ChronoUnit.SECONDS);LocalTime minus = time2.minusHours(1);System.out.println("加2小时: "+ plusHours);// 17:30:45System.out.println("加30分: "+ plusMinutes);// 16:00:45System.out.println("减1小时: "+ minus);// 14:30:45// 4. 比较LocalTime timeA =LocalTime.of(10,0);LocalTime timeB =LocalTime.of(14,0);System.out.println("timeA before timeB? "+ timeA.isBefore(timeB));// true}}5.5 LocalDateTime——日期+时间(无时区)
LocalDateTime组合了LocalDate和LocalTime,表示不带时区的完整日期时间。
importjava.time.LocalDate;importjava.time.LocalTime;importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;importjava.time.temporal.ChronoUnit;publicclassLocalDateTimeDemo{publicstaticvoidmain(String[] args){// 1. 创建LocalDateTime now =LocalDateTime.now();System.out.println("当前日期时间: "+ now);// 2026-02-20T15:30:45.123// 从年月日时分秒LocalDateTime dt1 =LocalDateTime.of(2026,2,20,15,30);LocalDateTime dt2 =LocalDateTime.of(2026,2,20,15,30,45);LocalDateTime dt3 =LocalDateTime.of(2026,2,20,15,30,45,123456789);// 组合LocalDate和LocalTimeLocalDate date =LocalDate.of(2026,2,20);LocalTime time =LocalTime.of(15,30,45);LocalDateTime dt4 =LocalDateTime.of(date, time);// 从字符串解析LocalDateTime parsed =LocalDateTime.parse("2026-02-20T15:30:45");DateTimeFormatter formatter =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");LocalDateTime parsedCustom =LocalDateTime.parse("2026-02-20 15:30:45", formatter);System.out.println("解析结果: "+ parsedCustom);// 2. 获取各部分LocalDate datePart = dt4.toLocalDate();LocalTime timePart = dt4.toLocalTime();System.out.println("日期部分: "+ datePart);System.out.println("时间部分: "+ timePart);// 获取字段System.out.println("年: "+ dt4.getYear());System.out.println("月: "+ dt4.getMonthValue());System.out.println("日: "+ dt4.getDayOfMonth());System.out.println("时: "+ dt4.getHour());// 3. 运算LocalDateTime tomorrow = dt4.plusDays(1);LocalDateTime nextWeek = dt4.plusWeeks(1);LocalDateTime nextMonth = dt4.plusMonths(1);LocalDateTime nextYear = dt4.plusYears(1);LocalDateTime plusHours = dt4.plusHours(2);LocalDateTime minusMinutes = dt4.minusMinutes(30);System.out.println("明天此刻: "+ tomorrow);System.out.println("加2小时: "+ plusHours);// 链式调用LocalDateTime result = dt4.plusYears(1).plusMonths(2).minusDays(3).plusHours(4);System.out.println("链式结果: "+ result);// 4. 使用TemporalAdjusters(更复杂的调整)// 例如:获取当月的最后一天LocalDateTime lastDayOfMonth = dt4.with(java.time.temporal.TemporalAdjusters.lastDayOfMonth());System.out.println("当月最后一天: "+ lastDayOfMonth);}}5.6 Instant——机器时间(时间戳)
Instant表示时间线上的一个瞬时点,从1970-01-01T00:00:00Z开始计算的秒数和纳秒数,是面向机器的表示。
importjava.time.Instant;importjava.time.ZoneId;importjava.time.ZonedDateTime;importjava.util.Date;publicclassInstantDemo{publicstaticvoidmain(String[] args){// 1. 获取当前Instant(UTC时间)Instant now =Instant.now();System.out.println("当前Instant: "+ now);// 2026-02-20T07:30:45.123Z(Z表示UTC)// 2. 从时间戳创建Instant fromEpochMilli =Instant.ofEpochMilli(1773084600000L);// 毫秒Instant fromEpochSecond =Instant.ofEpochSecond(1773084600L);// 秒Instant fromEpochSecondWithNano =Instant.ofEpochSecond(1773084600L,123456789);// 秒+纳秒System.out.println("毫秒创建: "+ fromEpochMilli);// 3. 获取时间戳long epochSecond = now.getEpochSecond();// 秒long epochMilli = now.toEpochMilli();// 毫秒int nano = now.getNano();// 纳秒System.out.println("秒: "+ epochSecond);System.out.println("毫秒: "+ epochMilli);System.out.println("纳秒: "+ nano);// 4. 运算Instant plusSeconds = now.plusSeconds(3600);// 加1小时Instant minusMillis = now.minusMillis(5000);// 减5秒// 5. 比较Instant earlier =Instant.now().minusSeconds(10);Instant later =Instant.now().plusSeconds(10);System.out.println("earlier before later? "+ earlier.isBefore(later));// 6. 与Date转换Date date =Date.from(now);// Instant -> DateInstant instantFromDate = date.toInstant();// Date -> InstantSystem.out.println("Date转回Instant: "+ instantFromDate);// 7. 转换为带时区的日期时间ZonedDateTime beijingTime = now.atZone(ZoneId.of("Asia/Shanghai"));System.out.println("北京时间: "+ beijingTime);}}关键理解:
Instant总是UTC时间,不附带时区信息- 适合用于时间戳记录、计算时间差、跨时区传输等场景
- 与
Date可以互相转换,是连接新旧API的桥梁
5.7 ZonedDateTime——带时区的日期时间
ZonedDateTime包含日期、时间和时区信息,解决了跨时区应用的需求。
importjava.time.ZoneId;importjava.time.ZonedDateTime;importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;publicclassZonedDateTimeDemo{publicstaticvoidmain(String[] args){// 1. 获取当前时区的日期时间ZonedDateTime now =ZonedDateTime.now();System.out.println("当前时区时间: "+ now);// 2026-02-20T15:30:45.123+08:00[Asia/Shanghai]// 2. 指定时区ZonedDateTime nyNow =ZonedDateTime.now(ZoneId.of("America/New_York"));System.out.println("纽约时间: "+ nyNow);// 3. 从LocalDateTime加时区LocalDateTime localDateTime =LocalDateTime.of(2026,2,20,15,30);ZonedDateTime zoned1 = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));ZonedDateTime zoned2 =ZonedDateTime.of(localDateTime,ZoneId.of("Asia/Shanghai"));// 4. 直接指定ZonedDateTime zoned3 =ZonedDateTime.of(2026,2,20,15,30,45,0,ZoneId.of("Asia/Shanghai"));// 5. 时区转换ZonedDateTime beijing =ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));ZonedDateTime london = beijing.withZoneSameInstant(ZoneId.of("Europe/London"));System.out.println("北京时间: "+ beijing);System.out.println("纽约时间: "+ newYork);System.out.println("伦敦时间: "+ london);// 6. 获取时区信息ZoneId zone = beijing.getZone();String zoneId = zone.getId();// "Asia/Shanghai"System.out.println("时区ID: "+ zoneId);// 7. 偏移量java.time.ZoneOffset offset = beijing.getOffset();System.out.println("偏移量: "+ offset);// +08:00// 8. 格式化输出DateTimeFormatter formatter =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz");System.out.println("格式化: "+ beijing.format(formatter));// 2026-02-20 15:30:45 中国标准时间}}5.8 Period与Duration——时间间隔
Period用于日期之间的间隔(年、月、日),Duration用于时间之间的间隔(时、分、秒、纳秒)。
importjava.time.*;importjava.time.temporal.ChronoUnit;publicclassPeriodDurationDemo{publicstaticvoidmain(String[] args){// ========== Period(日期间隔)==========LocalDate startDate =LocalDate.of(2020,1,1);LocalDate endDate =LocalDate.of(2026,2,20);Period period =Period.between(startDate, endDate);System.out.println("日期差: "+ period);// P6Y1M19D(6年1个月19天)System.out.println("年差: "+ period.getYears());System.out.println("月差: "+ period.getMonths());System.out.println("日差: "+ period.getDays());// 创建PeriodPeriod ofYears =Period.ofYears(5);// 5年Period ofMonths =Period.ofMonths(3);// 3个月Period ofWeeks =Period.ofWeeks(2);// 2周(即14天)Period ofDays =Period.ofDays(10);// 10天Period custom =Period.of(2,6,15);// 2年6个月15天// 应用PeriodLocalDate newDate = startDate.plus(period);System.out.println("加上间隔后: "+ newDate);// 2026-02-20// ========== Duration(时间间隔)==========LocalTime startTime =LocalTime.of(10,0,0);LocalTime endTime =LocalTime.of(15,30,45);Duration duration =Duration.between(startTime, endTime);System.out.println("时间差: "+ duration);// PT5H30M45SSystem.out.println("小时差: "+ duration.toHours());// 5System.out.println("分钟差: "+ duration.toMinutes());// 330System.out.println("秒差: "+ duration.getSeconds());// 19845// 创建DurationDuration ofHours =Duration.ofHours(3);// 3小时Duration ofMinutes =Duration.ofMinutes(45);// 45分钟Duration ofSeconds =Duration.ofSeconds(120);// 120秒Duration ofMillis =Duration.ofMillis(5000);// 5秒Duration ofNanos =Duration.ofNanos(1000000);// 1毫秒Duration of =Duration.of(2,ChronoUnit.HOURS);// 2小时// 应用DurationLocalTime newTime = startTime.plus(duration);System.out.println("加上间隔后: "+ newTime);// 15:30:45// 更精确的计算(考虑纳秒)Instant startInstant =Instant.now();// 模拟操作Instant endInstant =Instant.now().plusSeconds(3);Duration elapsed =Duration.between(startInstant, endInstant);System.out.println("耗时(秒): "+ elapsed.getSeconds());System.out.println("耗时(毫秒): "+ elapsed.toMillis());}}5.9 DateTimeFormatter——线程安全的格式化器
DateTimeFormatter是Java 8提供的格式化类,取代了SimpleDateFormat,并且是线程安全的。
importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;importjava.time.format.FormatStyle;importjava.util.Locale;publicclassDateTimeFormatterDemo{publicstaticvoidmain(String[] args){LocalDateTime now =LocalDateTime.now();// ========== 1. 预定义格式化器 ==========DateTimeFormatter isoDate =DateTimeFormatter.ISO_DATE;DateTimeFormatter isoDateTime =DateTimeFormatter.ISO_DATE_TIME;System.out.println("ISO日期: "+ now.format(isoDate));// 2026-02-20System.out.println("ISO日期时间: "+ now.format(isoDateTime));// 2026-02-20T15:30:45.123// ========== 2. 本地化格式 ==========DateTimeFormatter fullDate =DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);DateTimeFormatter longDateTime =DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG,FormatStyle.MEDIUM);System.out.println("本地化FULL: "+ now.format(fullDate));// 2026年2月20日 星期五System.out.println("本地化LONG/MEDIUM: "+ now.format(longDateTime));// 2026年2月20日 15:30:45// 指定LocaleDateTimeFormatter usFormatter =DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.US);System.out.println("美国格式: "+ now.format(usFormatter));// Friday, February 20, 2026// ========== 3. 自定义模式(最常用)==========DateTimeFormatter pattern1 =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");DateTimeFormatter pattern2 =DateTimeFormatter.ofPattern("yyyy年MM月dd日 EEEE HH时mm分ss秒");DateTimeFormatter pattern3 =DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss a");System.out.println("模式1: "+ now.format(pattern1));System.out.println("模式2: "+ now.format(pattern2));System.out.println("模式3: "+ now.format(pattern3));// ========== 4. 解析字符串 ==========String dateStr ="2026-02-20 15:30:45";DateTimeFormatter parser =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");LocalDateTime parsed =LocalDateTime.parse(dateStr, parser);System.out.println("解析结果: "+ parsed);// ========== 5. 线程安全演示 ==========// 可以在多个线程中共享同一个formatter实例DateTimeFormatter sharedFormatter =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");// 创建多个线程使用同一个formatterfor(int i =0; i <10; i++){newThread(()->{String result = sharedFormatter.format(LocalDateTime.now());System.out.println(Thread.currentThread().getName()+": "+ result);}).start();}// 不会出现线程安全问题}}5.10 时区处理——ZoneId与ZoneOffset
importjava.time.ZoneId;importjava.time.ZoneOffset;importjava.time.ZonedDateTime;importjava.util.Set;publicclassZoneDemo{publicstaticvoidmain(String[] args){// ========== ZoneId(时区ID)==========// 1. 系统默认时区ZoneId defaultZone =ZoneId.systemDefault();System.out.println("默认时区: "+ defaultZone);// Asia/Shanghai// 2. 通过ID获取ZoneId shanghai =ZoneId.of("Asia/Shanghai");ZoneId newYork =ZoneId.of("America/New_York");ZoneId utc =ZoneId.of("UTC");// 3. 所有可用时区Set<String> zoneIds =ZoneId.getAvailableZoneIds();System.out.println("总时区数: "+ zoneIds.size());// 约600个// 打印前10个 zoneIds.stream().limit(10).forEach(System.out::println);// ========== ZoneOffset(偏移量)==========ZoneOffset offset1 =ZoneOffset.of("+08:00");ZoneOffset offset2 =ZoneOffset.ofHours(8);ZoneOffset offset3 =ZoneOffset.ofHoursMinutes(8,30);ZoneOffset utcOffset =ZoneOffset.UTC;// 零偏移System.out.println("偏移量+8: "+ offset1);// +08:00// 使用偏移量创建ZonedDateTimeZonedDateTime zonedWithOffset =ZonedDateTime.now(offset1);System.out.println("偏移量时间: "+ zonedWithOffset);// 时区转换示例:东京时间转纽约时间ZonedDateTime tokyoTime =ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));ZonedDateTime nyTime = tokyoTime.withZoneSameInstant(ZoneId.of("America/New_York"));System.out.println("东京时间: "+ tokyoTime);System.out.println("对应纽约时间: "+ nyTime);}}5.11 日期时间调整——TemporalAdjusters
TemporalAdjusters提供了许多常用的日期调整工具。
importjava.time.DayOfWeek;importjava.time.LocalDate;importjava.time.temporal.TemporalAdjusters;publicclassTemporalAdjustersDemo{publicstaticvoidmain(String[] args){LocalDate date =LocalDate.of(2026,2,20);// 星期五// 下一个/上一个/本月第一个/最后一个LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY));LocalDate previousSunday = date.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY));LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());LocalDate firstDayOfNextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth());LocalDate firstDayOfYear = date.with(TemporalAdjusters.firstDayOfYear());LocalDate lastDayOfYear = date.with(TemporalAdjusters.lastDayOfYear());System.out.println("当前日期: "+ date);System.out.println("下周一: "+ nextMonday);System.out.println("上周日: "+ previousSunday);System.out.println("本月第一天: "+ firstDayOfMonth);System.out.println("本月最后一天: "+ lastDayOfMonth);// 本月第几个星期几LocalDate thirdFriday = date.with(TemporalAdjusters.dayOfWeekInMonth(3,DayOfWeek.FRIDAY));System.out.println("本月第三个星期五: "+ thirdFriday);// 下个月的今天(如果存在)LocalDate nextMonthSameDay = date.plusMonths(1);System.out.println("下个月今天: "+ nextMonthSameDay);}}第六章:新旧API对比与转换指南
6.1 三代API对比总结
| 维度 | 第一代 (Date/DateFormat) | 第二代 (Calendar) | 第三代 (java.time) |
|---|---|---|---|
| 核心类 | Date, SimpleDateFormat | Calendar, GregorianCalendar | LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant |
| 不可变性 | 可变 | 可变 | 不可变 |
| 线程安全 | 不安全 | 不安全 | 安全 |
| 月份偏移 | 0-11 | 0-11 | 1-12 |
| 年份偏移 | year-1900 | 正常 | 正常 |
| API设计 | 混乱 | 繁琐 | 清晰流畅 |
| 时区支持 | 弱 | 有 | 强大 |
| 性能 | 一般 | 较差(重量级) | 优秀 |
| 可读性 | 差 | 差 | 好 |
6.2 何时使用旧API,何时使用新API
使用旧API的场景(仅限于维护遗留代码):
- 正在维护使用JDK 7及以下版本的项目
- 与某些旧框架集成(如Hibernate早期版本)
- 需要与遗留数据库交互(部分JDBC驱动仍使用
java.sql.Date/Timestamp)
强烈推荐使用新API的场景(所有新项目):
- JDK 8及以上版本的新开发
- 需要复杂的日期计算
- 涉及时区转换
- 多线程环境下的日期处理
- 希望代码更清晰、更易维护
6.3 新旧API转换工具类
在实际开发中,经常需要在新旧API之间转换(例如,数据库操作可能返回java.sql.Date)。下面是一个完整的转换工具类:
importjava.time.*;importjava.util.Date;importjava.util.Calendar;importjava.util.GregorianCalendar;/** * 新旧日期时间API转换工具类 */publicclassDateTimeConversionUtil{// ========== java.util.Date <-> java.time ==========/** * Date -> Instant */publicstaticInstanttoInstant(Date date){return date ==null?null: date.toInstant();}/** * Instant -> Date */publicstaticDatetoDate(Instant instant){return instant ==null?null:Date.from(instant);}/** * Date -> LocalDate(系统默认时区) */publicstaticLocalDatetoLocalDate(Date date){if(date ==null)returnnull;return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();}/** * LocalDate -> Date(系统默认时区) */publicstaticDatetoDate(LocalDate localDate){if(localDate ==null)returnnull;returnDate.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());}/** * Date -> LocalDateTime(系统默认时区) */publicstaticLocalDateTimetoLocalDateTime(Date date){if(date ==null)returnnull;return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();}/** * LocalDateTime -> Date(系统默认时区) */publicstaticDatetoDate(LocalDateTime localDateTime){if(localDateTime ==null)returnnull;returnDate.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());}/** * Date -> ZonedDateTime(系统默认时区) */publicstaticZonedDateTimetoZonedDateTime(Date date){if(date ==null)returnnull;return date.toInstant().atZone(ZoneId.systemDefault());}/** * ZonedDateTime -> Date */publicstaticDatetoDate(ZonedDateTime zonedDateTime){if(zonedDateTime ==null)returnnull;returnDate.from(zonedDateTime.toInstant());}// ========== java.util.Calendar <-> java.time ==========/** * Calendar -> Instant */publicstaticInstanttoInstant(Calendar calendar){if(calendar ==null)returnnull;return calendar.toInstant();}/** * Instant -> GregorianCalendar */publicstaticGregorianCalendartoCalendar(Instant instant){if(instant ==null)returnnull;returnGregorianCalendar.from(instant.atZone(ZoneId.systemDefault()));}/** * Calendar -> ZonedDateTime */publicstaticZonedDateTimetoZonedDateTime(Calendar calendar){if(calendar ==null)returnnull;returnZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());}/** * ZonedDateTime -> GregorianCalendar */publicstaticGregorianCalendartoCalendar(ZonedDateTime zonedDateTime){if(zonedDateTime ==null)returnnull;returnGregorianCalendar.from(zonedDateTime);}/** * Calendar -> LocalDateTime */publicstaticLocalDateTimetoLocalDateTime(Calendar calendar){if(calendar ==null)returnnull;returnLocalDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());}/** * LocalDateTime -> GregorianCalendar(指定时区) */publicstaticGregorianCalendartoCalendar(LocalDateTime localDateTime,ZoneId zoneId){if(localDateTime ==null|| zoneId ==null)returnnull;returnGregorianCalendar.from(localDateTime.atZone(zoneId));}// ========== java.sql 相关 ==========/** * java.sql.Date -> LocalDate */publicstaticLocalDatetoLocalDate(java.sql.Date sqlDate){return sqlDate ==null?null: sqlDate.toLocalDate();}/** * LocalDate -> java.sql.Date */publicstaticjava.sql.DatetoSqlDate(LocalDate localDate){return localDate ==null?null:java.sql.Date.valueOf(localDate);}/** * java.sql.Timestamp -> LocalDateTime */publicstaticLocalDateTimetoLocalDateTime(java.sql.Timestamp timestamp){return timestamp ==null?null: timestamp.toLocalDateTime();}/** * LocalDateTime -> java.sql.Timestamp */publicstaticjava.sql.TimestamptoTimestamp(LocalDateTime localDateTime){return localDateTime ==null?null:java.sql.Timestamp.valueOf(localDateTime);}/** * java.sql.Time -> LocalTime */publicstaticLocalTimetoLocalTime(java.sql.Time sqlTime){return sqlTime ==null?null: sqlTime.toLocalTime();}/** * LocalTime -> java.sql.Time */publicstaticjava.sql.TimetoSqlTime(LocalTime localTime){return localTime ==null?null:java.sql.Time.valueOf(localTime);}// ========== 使用示例 ==========publicstaticvoidmain(String[] args){// Date <-> LocalDateTimeDate now =newDate();LocalDateTime ldt =toLocalDateTime(now);Date backToDate =toDate(ldt);System.out.println("原始Date: "+ now);System.out.println("转LocalDateTime: "+ ldt);System.out.println("转回Date: "+ backToDate);System.out.println("是否相等: "+ now.equals(backToDate));// Calendar <-> ZonedDateTimeCalendar calendar =Calendar.getInstance();ZonedDateTime zdt =toZonedDateTime(calendar);GregorianCalendar backToCal =toCalendar(zdt);System.out.println("原始Calendar: "+ calendar.getTime());System.out.println("转ZonedDateTime: "+ zdt);System.out.println("转回Calendar: "+ backToCal.getTime());}}第七章:最佳实践与常见陷阱
7.1 开发中应该选择哪个API?
决策树:
- 项目JDK版本 ≥ 8? → 使用
java.time包 - 需要与遗留代码/库交互? → 在边界处转换,核心逻辑仍用
java.time - 需要数据库操作? → 使用
java.time类型与JPA 2.2+(支持LocalDate等) - 需要高性能、线程安全的格式化? → 使用
DateTimeFormatter - 维护JDK 7及以下项目? → 只能使用
Calendar+SimpleDateFormat(注意线程安全)
7.2 常见陷阱与解决方案
陷阱1:月份从0开始(Calendar/Date)
// 错误Calendar cal =Calendar.getInstance(); cal.set(2026,2,20);// 以为是2月20日,实际是3月20日// 正确 cal.set(2026,Calendar.FEBRUARY,20);// 使用Calendar常量// 或者 cal.set(2026,1,20);// 月份0=1月,1=2月// 最佳:使用Java 8LocalDate date =LocalDate.of(2026,2,20);// 直接使用2陷阱2:SimpleDateFormat线程不安全
// 错误privatestaticfinalSimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");// 多个线程共用,导致数据错乱// 正确privatestaticfinalThreadLocal<SimpleDateFormat> sdfHolder =ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd"));// 最佳privatestaticfinalDateTimeFormatter formatter =DateTimeFormatter.ofPattern("yyyy-MM-dd");陷阱3:时区混淆
// 错误:认为LocalDateTime有时区LocalDateTime now =LocalDateTime.now();// 实际上只是系统默认时区的本地时间,不包含时区信息// 需要时区时应使用ZonedDateTimeZonedDateTime zonedNow =ZonedDateTime.now();// 跨时区转换正确做法ZonedDateTime beijing =ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));陷阱4:时间精度丢失
// Date只能精确到毫秒Date date =newDate();long millis = date.getTime();// 毫秒// Instant可以精确到纳秒Instant instant =Instant.now();long seconds = instant.getEpochSecond();int nanos = instant.getNano();// 纳秒部分// 互相转换时可能丢失精度Instant nanoInstant =Instant.now();Date dateFromInstant =Date.from(nanoInstant);// 纳秒部分被截断为毫秒陷阱5:Period与Duration混淆
// Period用于日期(年、月、日)LocalDate start =LocalDate.of(2026,1,1);LocalDate end =LocalDate.of(2026,2,20);Period period =Period.between(start, end);System.out.println(period.getDays());// 19?不对,是19天,但月份部分也是1个月// Duration用于时间(时、分、秒)LocalTime startTime =LocalTime.of(10,0);LocalTime endTime =LocalTime.of(15,30);Duration duration =Duration.between(startTime, endTime);System.out.println(duration.toMinutes());// 330分钟// 不要用Period计算时间差,用Duration7.3 性能考虑
java.time性能优于Calendar:Calendar内部有复杂的字段计算和同步开销DateTimeFormatter重用:由于线程安全,可以定义为static final常量重用- 避免频繁创建
Instant/LocalDateTime:除非必要,否则使用now()获取当前时间即可 - 大量日期计算时考虑使用
java.time:API设计更高效
7.4 代码示例:业务场景实战
场景1:计算年龄
importjava.time.LocalDate;importjava.time.Period;publicclassAgeCalculator{publicstaticintcalculateAge(LocalDate birthDate){LocalDate today =LocalDate.now();returnPeriod.between(birthDate, today).getYears();}publicstaticvoidmain(String[] args){LocalDate birth =LocalDate.of(1990,5,15);int age =calculateAge(birth);System.out.println("年龄: "+ age);// 根据当前日期计算}}场景2:订单超时判断(30分钟未支付取消)
importjava.time.LocalDateTime;importjava.time.temporal.ChronoUnit;publicclassOrderTimeout{publicstaticbooleanisTimeout(LocalDateTime orderTime,int timeoutMinutes){LocalDateTime now =LocalDateTime.now();long minutesElapsed =ChronoUnit.MINUTES.between(orderTime, now);return minutesElapsed >= timeoutMinutes;}publicstaticvoidmain(String[] args){LocalDateTime orderTime =LocalDateTime.now().minusMinutes(25);boolean timeout =isTimeout(orderTime,30);System.out.println("订单是否超时: "+ timeout);// false orderTime =LocalDateTime.now().minusMinutes(35); timeout =isTimeout(orderTime,30);System.out.println("订单是否超时: "+ timeout);// true}}场景3:获取某月的所有周末
importjava.time.DayOfWeek;importjava.time.LocalDate;importjava.time.YearMonth;importjava.util.ArrayList;importjava.util.List;publicclassWeekendsInMonth{publicstaticList<LocalDate>getWeekends(int year,int month){List<LocalDate> weekends =newArrayList<>();YearMonth yearMonth =YearMonth.of(year, month);LocalDate firstOfMonth = yearMonth.atDay(1);LocalDate lastOfMonth = yearMonth.atEndOfMonth();LocalDate date = firstOfMonth;while(!date.isAfter(lastOfMonth)){DayOfWeek dow = date.getDayOfWeek();if(dow ==DayOfWeek.SATURDAY || dow ==DayOfWeek.SUNDAY){ weekends.add(date);} date = date.plusDays(1);}return weekends;}publicstaticvoidmain(String[] args){List<LocalDate> weekends =getWeekends(2026,2);System.out.println("2026年2月周末:"); weekends.forEach(System.out::println);}}场景4:国际化日期显示
importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;importjava.time.format.FormatStyle;importjava.util.Locale;publicclassI18nDateDemo{publicstaticvoidmain(String[] args){LocalDateTime now =LocalDateTime.now();// 中文显示DateTimeFormatter chineseFormatter =DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM).withLocale(Locale.CHINA);System.out.println("中文: "+ now.format(chineseFormatter));// 英文显示DateTimeFormatter usFormatter =DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM).withLocale(Locale.US);System.out.println("英文: "+ now.format(usFormatter));// 日文显示DateTimeFormatter japanFormatter =DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM).withLocale(Locale.JAPAN);System.out.println("日文: "+ now.format(japanFormatter));}}第八章:总结与展望
8.1 三代API的演进之路回顾
- 第一代(Date/DateFormat):JDK 1.0诞生,设计简陋,存在年份偏移、月份从0开始、线程不安全等问题,大部分方法已废弃
- 第二代(Calendar):JDK 1.1引入,试图弥补Date的缺陷,但API复杂、月份偏移问题依旧、线程不安全,且性能较差
- 第三代(java.time):JDK 8引入,基于JSR 310,设计优雅,不可变、线程安全、API清晰、月份从1开始,是处理日期时间的首选
8.2 核心要点总结
| 核心类 | 用途 | 关键特性 |
|---|---|---|
Date | 表示时间戳(已过时) | 可变、线程不安全、月份0-11 |
Calendar | 日历字段操作(已过时) | 可变、线程不安全、月份0-11、API繁琐 |
LocalDate | 日期(无时间) | 不可变、线程安全、月份1-12 |
LocalTime | 时间(无日期) | 不可变、线程安全 |
LocalDateTime | 日期+时间(无时区) | 不可变、线程安全 |
ZonedDateTime | 带时区的日期时间 | 不可变、线程安全、时区转换 |
Instant | 时间戳(机器时间) | 不可变、线程安全、UTC |
DateTimeFormatter | 格式化/解析 | 线程安全、取代SimpleDateFormat |
Period/Duration | 时间间隔 | 不可变、线程安全 |
8.3 未来展望
随着Java的持续发展,日期时间API也在不断完善:
- JDK 9+:对
java.time包进行了细微优化和bug修复 - 未来版本:可能会引入更多便捷的日期时间操作方法,与
Record、Pattern Matching等新特性更好地集成
8.4 给开发者的建议
- 新项目一律使用
java.time包,告别旧API的烦恼 - 维护旧项目时,在边界层(如Controller、DAO)进行新旧API转换,核心业务逻辑尽量使用
java.time - 注意线程安全,
SimpleDateFormat在多线程环境下必须采取保护措施 - 理解时区概念,区分本地时间、UTC时间、带时区时间
- 善用
DateTimeFormatter,它是线程安全的,可以定义为常量复用 - 阅读官方文档和
java.time包的Javadoc,掌握更多高级特性(如TemporalQuery、TemporalAdjuster等)
附录:常用代码片段速查
获取当前时间
// 旧Date now =newDate();Calendar cal =Calendar.getInstance();// 新LocalDate today =LocalDate.now();LocalTime nowTime =LocalTime.now();LocalDateTime nowDateTime =LocalDateTime.now();Instant instant =Instant.now();创建指定日期
// 旧Calendar cal =Calendar.getInstance(); cal.set(2026,Calendar.FEBRUARY,20);// 注意月份常量// 新LocalDate date =LocalDate.of(2026,2,20);LocalDateTime dt =LocalDateTime.of(2026,2,20,15,30,45);日期加减
// 旧 cal.add(Calendar.DAY_OF_MONTH,5); cal.add(Calendar.MONTH,-2);// 新LocalDate newDate = date.plusDays(5).minusMonths(2);格式化
// 旧(注意线程安全问题)SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");String formatted = sdf.format(newDate());// 新DateTimeFormatter dtf =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formatted =LocalDateTime.now().format(dtf);解析字符串
// 旧SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");Date date = sdf.parse("2026-02-20");// 新DateTimeFormatter dtf =DateTimeFormatter.ofPattern("yyyy-MM-dd");LocalDate date =LocalDate.parse("2026-02-20", dtf);计算两个日期的天数差
// 旧(繁琐)long days =(date2.getTime()- date1.getTime())/(24*60*60*1000);// 新long days =ChronoUnit.DAYS.between(date1, date2);时区转换
// 旧(麻烦)TimeZone tz =TimeZone.getTimeZone("America/New_York");Calendar cal =Calendar.getInstance(tz);// 新ZonedDateTime nyTime =ZonedDateTime.now(ZoneId.of("America/New_York"));ZonedDateTime shanghaiTime = nyTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));