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

字节跳动AI IDE:Trae 完全上手指南——从零安装到熟练使用,开启AI驱动开发新范式

字节跳动AI IDE:Trae 完全上手指南——从零安装到熟练使用,开启AI驱动开发新范式

目录 * 前言:当IDE进化为智能体 * 1.初识Trae * 1.1 Trae是什么? * 1.2 Trae的核心优势 * 1.3 谁适合使用Trae? * 2.安装与初始配置 * 2.1 支持的操作系统 * 2.2 下载与安装步骤 * 2.3 验证安装成功 * 3.界面导航(五分钟熟悉布局) * 3.1 核心区域功能说明 * 3.2 常用快捷键速查 * 4.核心AI功能详解 * 4.1 Chat模式:随时提问的编程助手 * 4.2 Builder模式:自然语言生成完整项目 * 4.2.1 实战案例:做一个待办事项应用 * 4.

By Ne0inhk
AI 编程新范式:一文彻底搞懂 LLM、Agent、MCP、Skill 是怎么协作的

AI 编程新范式:一文彻底搞懂 LLM、Agent、MCP、Skill 是怎么协作的

文章目录 * 一、核心结论:AI 编程进入「分工时代」 * 二、LLM 与 Agent 🔥 * 1. LLM(大语言模型) * 2. Agent(智能体) * 3. 对比 * 4. 🧠人脑 vs AI Agent 🤖 * 5. 映射图 * 三、MCP 与 Skill 🔥 * 1. MCP:神经系统协议(神经信号标准、信号如何传递)- 协议 * 2. MCP Server:肢体/器官(真正干活的执行实体)- 服务 * 3. Skill:器官的本能动作(Server本能动作)- 内置能力

By Ne0inhk
能做影视级可商业视频的AI工具,Seedance 2.0 全球首发实测

能做影视级可商业视频的AI工具,Seedance 2.0 全球首发实测

如果你是短片导演、影视团队,或者长期做内容的自媒体,一定有同感: AI 视频不是不好,而是太“难用”。 * 想复刻一个爆款运镜,结果画面乱飞 * 想做商用级视频,角色和产品每一帧都在变 * 想快点出片,却被排队、算力、复杂参数拖住 大多数 AI 视频工具的现状是: 看 Demo 很震撼,真到实操,全靠赌。 而 Seedance2.0 给我的第一感受是—— 它不是在“秀模型能力”,而是在解决真实创作流程中的控制问题,把“做视频”这件事,拉回到像 P 图一样直觉、可控。 一、模型重磅发布:Seedance2.0 到底解决了什么? Seedance2.0 是即梦最新一代视频模型,核心定位非常明确: 影视级质量 + 商业可用 + 一站式生成。

By Ne0inhk
华为昇腾310P 176T算力AI 智能计算模组规格书

华为昇腾310P 176T算力AI 智能计算模组规格书

目录产品介绍...................................................................................................................................... 31、产品简介............................................................................................................................. 32、产品特性............................................................................................................................. 33、应用领域......................................................................................

By Ne0inhk