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

Java中的日期时间API详解:从Date、Calendar到现代时间体系
在这里插入图片描述

文章目录

在这里插入图片描述

引言:Java日期时间处理的演进之路

在Java应用程序开发中,日期和时间的处理是极其常见的需求——记录操作时间、计算时间差、格式化输出、时区转换、订单超时计算、报表统计等场景都离不开日期时间API。然而,Java的日期时间API经历了一条曲折的演进道路。

Java 1.0时代,设计者简单粗暴地推出了java.util.Date类,但它存在诸多设计缺陷。Java 1.1引入了Calendar类试图弥补,却又带来了新的复杂性。直到Java 8,吸取了Joda-Time库的精华,推出了全新的java.time包(JSR 310),才真正解决了长久以来的痛点。

本文将深入剖析Java日期时间处理的三个时代:

  • 第一代DateDateFormatSimpleDateFormat
  • 第二代CalendarGregorianCalendarTimeZone
  • 第三代LocalDateLocalTimeLocalDateTimeZonedDateTimeInstantDateTimeFormatter

我们将从源码层面剖析其设计原理,探讨线程安全问题,提供最佳实践,并给出新旧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());}// ... 其他方法}

关键点分析

  1. 核心字段fastTime:这是一个long类型的变量,存储从1970-01-01 UTC开始的毫秒数。整个Date对象本质上就是这个数字的封装。
  2. 构造方法:只有两个构造方法被保留推荐使用——无参构造(获取当前时间)和带毫秒参数的构造。其他构造方法都被@Deprecated标记,不再推荐使用。
  3. 主要方法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"

解释DatetoString()方法格式固定,不考虑不同国家和语言的日期表示习惯。

缺陷5:命名混淆

java.util.Date实际上既包含日期也包含时间,但类名却只叫"Date",容易让人误以为它只处理日期部分。

2.4 正确使用Date的建议

鉴于上述缺陷,官方建议:

  1. 只使用两个推荐的方法new Date()new Date(long)构造、getTime()setTime()before()after()compareTo()
  2. 不要使用任何@Deprecated标记的方法
  3. 将Date作为"时间戳"的载体,不直接操作其日历字段
  4. 在需要日历字段操作时,使用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.FULL0完整格式,如"2026年2月20日 星期五"
DateFormat.LONG1长格式,如"2026年2月20日"
DateFormat.MEDIUM2中等格式,如"2026-2-20"(默认风格)
DateFormat.SHORT3短格式,如"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)throwsParseException
3.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.SimpleDateFormatDateFormat的具体子类,允许通过模式字符串自定义日期时间格式。

3.2.1 构造方法
// 使用默认模式SimpleDateFormat()// 使用指定模式SimpleDateFormat(String pattern)// 使用指定模式和区域SimpleDateFormat(String pattern,Locale locale)
3.2.2 常用模式字母
字母日期/时间元素示例
yyyyy -> 2026
M月份MM -> 02, MMM -> 二月
d月份中的天数dd -> 20
E星期几EEEE -> 星期五
aAM/PM标记a -> 下午
H小时(0-23)HH -> 15
h小时(1-12)hh -> 03
m分钟mm -> 30
sss -> 45
S毫秒SSS -> 123
z时区z -> CST
ZRFC 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中定义了一个protectedCalendar对象:

// DateFormat.javapublicabstractclassDateFormatextendsFormat{protectedCalendar calendar;// 共享的Calendar实例// ...}// SimpleDateFormat.javapublicclassSimpleDateFormatextendsDateFormat{// 在format方法中会使用calendarprivateStringBufferformat(Date date,StringBuffer toAppendTo,FieldDelegate delegate){// 关键点:这里修改了calendar的状态! calendar.setTime(date);// ... 后续使用calendar进行格式化return toAppendTo;}}

问题分析

  • calendarDateFormat类的成员变量,被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());}}

关键设计解析

  1. 双重表示Calendar内部同时维护两种表示——time(毫秒数)和fields[](字段数组)。当修改字段时,isTimeSet标记设为false,表示time需要重新计算;当设置时间时,fields[]会被重新计算。
  2. 延迟计算get(int field)方法调用complete(),如果isTimeSetfalse,则调用computeTime()fields[]计算time;如果fields[]不完整,则调用computeFields()time计算fields[]。这种延迟计算提高了性能。
  3. 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的优缺点分析

优点
  1. 丰富的字段操作:提供了17个日历字段,可以精确获取和设置各个部分
  2. 强大的计算能力add()roll()方法支持灵活的日期运算
  3. 国际化支持:内置时区和Locale处理
  4. 历法扩展性:可以支持不同的历法系统
缺点
  1. API复杂繁琐:使用时需要记住大量常量,代码冗长
  2. 月份偏移陷阱:月份从0开始,极易出错
  3. 线程不安全:内部状态可变,多线程共享需同步
  4. 可变性:方法调用会修改对象内部状态,不符合函数式编程思想
  5. 性能开销:相对重量级,频繁创建有性能问题
  6. 设计缺陷:某些方法行为不够直观(如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的设计遵循以下核心原则:

  1. 不可变性:所有核心类都是final的,且内部状态不可变,任何修改操作都返回新对象,天然线程安全
  2. 清晰分离:将日期、时间、日期时间、带时区的日期时间等概念清晰分离,通过不同类表示
  3. 流畅API:方法命名直观,支持链式调用
  4. ISO标准:默认采用ISO-8601国际标准
  5. 线程安全:所有类都是不可变的,可在多线程环境下安全共享

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时区IDAsia/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组合了LocalDateLocalTime,表示不带时区的完整日期时间。

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, SimpleDateFormatCalendar, GregorianCalendarLocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant
不可变性可变可变不可变
线程安全不安全不安全安全
月份偏移0-110-111-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?

决策树

  1. 项目JDK版本 ≥ 8? → 使用 java.time
  2. 需要与遗留代码/库交互? → 在边界处转换,核心逻辑仍用 java.time
  3. 需要数据库操作? → 使用 java.time 类型与JPA 2.2+(支持LocalDate等)
  4. 需要高性能、线程安全的格式化? → 使用 DateTimeFormatter
  5. 维护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计算时间差,用Duration

7.3 性能考虑

  1. java.time 性能优于 CalendarCalendar内部有复杂的字段计算和同步开销
  2. DateTimeFormatter 重用:由于线程安全,可以定义为static final常量重用
  3. 避免频繁创建 Instant/LocalDateTime:除非必要,否则使用now()获取当前时间即可
  4. 大量日期计算时考虑使用 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的演进之路回顾

  1. 第一代(Date/DateFormat):JDK 1.0诞生,设计简陋,存在年份偏移、月份从0开始、线程不安全等问题,大部分方法已废弃
  2. 第二代(Calendar):JDK 1.1引入,试图弥补Date的缺陷,但API复杂、月份偏移问题依旧、线程不安全,且性能较差
  3. 第三代(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修复
  • 未来版本:可能会引入更多便捷的日期时间操作方法,与RecordPattern Matching等新特性更好地集成

8.4 给开发者的建议

  1. 新项目一律使用 java.time,告别旧API的烦恼
  2. 维护旧项目时,在边界层(如Controller、DAO)进行新旧API转换,核心业务逻辑尽量使用java.time
  3. 注意线程安全SimpleDateFormat在多线程环境下必须采取保护措施
  4. 理解时区概念,区分本地时间、UTC时间、带时区时间
  5. 善用 DateTimeFormatter,它是线程安全的,可以定义为常量复用
  6. 阅读官方文档java.time包的Javadoc,掌握更多高级特性(如TemporalQueryTemporalAdjuster等)

附录:常用代码片段速查

获取当前时间

// 旧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"));

Read more

OpenClaw接入模型并基于WebUI完成智能操作

OpenClaw接入自定义模型并基于WebUI完成智能操作 背景介绍 OpenClaw(原 Clawdbot)是一个开源的 AI 代理框架,支持通过配置文件或 GUI 界面进行灵活配置。安装 OpenClaw 后,用户可以通过修改工作目录下的配置文件 openclaw.json 来接入不同的 LLM 模型提供商。 OpenClaw 支持众多主流模型提供商,包括 OpenAI、Anthropic、Moonshot AI(Kimi)、OpenRouter、Vercel AI Gateway、Amazon Bedrock 等。完整的提供商目录可参考官方文档 模型提供商快速入门。 要使用自定义的提供商,需要通过 models.providers 配置进行设置。这种方式允许用户接入官方支持列表之外的其他兼容 OpenAI API 或 Anthropic 格式的模型服务。 接入配置说明 核心配置参数解析

By Ne0inhk
openclaw新手入门指南:一文看懂环境搭建、模型配置与 WebUI 远程访问

openclaw新手入门指南:一文看懂环境搭建、模型配置与 WebUI 远程访问

目录 * 1. 基础设施层:OpenClaw 运行环境的初始化 * 2. 算力与模型层:蓝耘 MaaS 平台的接入配置 * 2.1 协议适配与 JSON 配置 * 3. 编排层:OpenClaw 初始化与 Onboarding 流程 * 3.1 模式选择与基础设置 * 3.2 模型提供商与应用集成策略 * 3.3 技能库(Skills)装载与服务启动 * 4. 网络架构与网关(Gateway)配置 * 4.1 网关暴露与安全策略 * 4.2 Web UI 远程访问与设备配对(Device Pairing) * 5. 高级模型编排与 JSON 配置深度解析

By Ne0inhk
一文彻底搞清楚数据结构之栈与队列:从定义到实战解析

一文彻底搞清楚数据结构之栈与队列:从定义到实战解析

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》《数据结构与算法初阶》 ✨逆境不吐心中苦,顺境不忘来时路!🎬 博主简介: 前言:前面我通过介绍了一些链表的算法题的解题思路和双向链表的相关知识,相信你一定有所感悟和体会!至此关于链表的内容小编就告一段落了,接下来我要介绍一种特殊的线性表—>栈和队列.它们又有什么作用和用途呢?废话不多说,下面跟着小编的节奏🎵一起学习吧! 目录 * 1.栈的定义 * 2.栈的结构 * 3.多栈共享空间 * 4.栈的实现 * 5.队列的定义 * 6.队列的结构 * 7.队列的实现 * 8.栈和队列的算法题 * 9.循环队列的定义 * 10.循环队列的算法题 1.栈的定义 栈是一种只允许在表的一端进行插入和删除的线性表.通常将表中允许进行插入、删除操作的一端称为栈顶,另一端称为栈底.栈的插入操作称为进栈或入栈,删除操作为出栈或退栈.当栈中没有元素时称为空栈.由于插入和删除操作都是在栈顶中进行,

By Ne0inhk
LeetCode算法日记 - Day 5: 长度最小的子数组、无重复字符的最长子串

LeetCode算法日记 - Day 5: 长度最小的子数组、无重复字符的最长子串

目录 1. 长度最小的子数组 1.1 题目解析 1.2 解法 1.3 代码实现 2. 无重复字符的最长子串 2.1 题目解析 2.2 解法 2.3 代码实现 1. 长度最小的子数组 209. 长度最小的子数组 - 力扣(LeetCode) 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 示例 1: 输入:

By Ne0inhk