Java时间处理详解:从Date、Calendar到java.time新API

8 阅读18分钟

Java时间处理详解:从Date、Calendar到java.time新API

时间处理是Java开发中最基础、最高频的场景之一——用户注册时间记录、订单超时判断、定时任务调度、日志时间戳生成等,几乎所有Java应用都离不开时间操作。但Java的时间API经历了多轮迭代,从早期的Date、Calendar,到Java 8推出的java.time包新API,不同API的用法、特点差异较大,新手容易混淆,甚至踩坑。

本文将系统梳理Java中所有常用的时间相关类,从旧API的缺陷、新API的优势,到具体用法、实战案例、避坑技巧,层层递进,帮你彻底掌握Java时间处理,无论是维护遗留系统(用Date/Calendar),还是开发新项目(用java.time),都能从容应对。

一、Java时间API的发展历程:从混乱到规范

Java的时间API发展主要分为三个阶段,理解这个历程,能更好地明白不同API的设计初衷和适用场景,避免盲目使用:

  1. JDK 1.0 阶段:推出java.util.Date类,作为最早的时间处理类,仅能表示一个时间点(精确到毫秒),API设计简陋,存在诸多缺陷;

  2. JDK 1.1 阶段:推出java.util.Calendarjava.text.SimpleDateFormat,解决Date类操作繁琐的问题,支持时间字段(年、月、日)的获取和计算,但仍有设计缺陷和线程安全问题;

  3. Java 8(JDK 1.8)阶段:推出java.time包(JSR 310规范),包含LocalDate、LocalTime、LocalDateTime等核心类,彻底解决旧API的缺陷,实现了不可变性、线程安全,API设计更直观、规范,是目前推荐的首选方案。

核心结论:新项目开发优先使用Java 8新API;维护遗留系统需掌握Date、Calendar,同时建议逐步迁移到新API。

二、旧时代API:Date与Calendar(遗留系统必备)

虽然Java 8新API已成为主流,但很多遗留系统仍在使用Date和Calendar,掌握它们的用法和缺陷,是维护旧系统的关键。

2.1 java.util.Date类:最早的时间类

Date类的核心作用是「表示一个具体的时间点」,内部存储的是自1970年1月1日00:00:00 GMT(格林尼治标准时间)以来的毫秒数(即时间戳),本身不具备时间字段(年、月、日)的操作能力,需配合SimpleDateFormat格式化,或Calendar进行字段操作。

2.1.1 Date类核心用法

import java.util.Date;

public class DateDemo {
    public static void main(String[] args) {
        // 1. 创建当前时间的Date对象(默认系统时区)
        Date now = new Date();
        System.out.println("当前时间:" + now); // 输出格式:Sat Apr 13 09:58:00 CST 2026

        // 2. 根据时间戳创建Date对象(毫秒级,1000L=1秒)
        long timestamp = 1712995080000L; // 对应2026-04-13 09:58:00
        Date specificDate = new Date(timestamp);
        System.out.println("指定时间戳的时间:" + specificDate);

        // 3. 核心方法:获取时间戳(毫秒)
        long currentTimestamp = now.getTime();
        System.out.println("当前时间戳(毫秒):" + currentTimestamp);

        // 4. 时间比较(before、after、equals)
        Date pastDate = new Date(timestamp - 86400000L); // 前一天
        System.out.println("now在pastDate之后:" + now.after(pastDate)); // true
        System.out.println("now在pastDate之前:" + now.before(pastDate)); // false
        System.out.println("两个时间相等:" + now.equals(specificDate)); // 取决于时间戳是否一致
    }
}

2.1.2 Date类的致命缺陷(必记)

Date类的设计存在诸多不合理之处,这也是它被逐渐淘汰的核心原因:

  • 可变性:Date对象是可变的,调用setTime()等方法可直接修改其表示的时间,多线程环境下存在线程安全问题;

  • API混乱:很多方法已被废弃(如getYear()、getMonth()),且返回值设计不合理(如getYear()返回的是“年份-1900”,getMonth()返回0-11代表1-12月);

  • 无明确时区概念:Date本身不存储时区信息,默认依赖系统时区,容易导致时区混淆;

  • 功能单一:仅能表示时间点,无法直接处理日期、时间的字段(如获取当月天数、加减月份),需依赖其他类。

2.2 java.util.Calendar类:Date的增强版

Calendar是抽象类,不能直接实例化,需通过Calendar.getInstance()获取子类(默认是GregorianCalendar,公历)实例。它的核心作用是「处理时间字段」,解决了Date类无法操作年、月、日等字段的问题,支持时间计算、字段设置等操作。

2.2.1 Calendar类核心用法

import java.util.Calendar;

public class CalendarDemo {
    public static void main(String[] args) {
        // 1. 获取Calendar实例(默认系统时区、当前时间)
        Calendar cal = Calendar.getInstance();

        // 2. 获取时间字段(核心用法,需注意字段的取值范围)
        int year = cal.get(Calendar.YEAR); // 年份(4位,如2026)
        int month = cal.get(Calendar.MONTH) + 1; // 月份:0-11,需+1才是实际月份
        int day = cal.get(Calendar.DAY_OF_MONTH); // 当月日期(1-31)
        int hour = cal.get(Calendar.HOUR_OF_DAY); // 24小时制小时(0-23)
        int minute = cal.get(Calendar.MINUTE); // 分钟(0-59)
        int second = cal.get(Calendar.SECOND); // 秒(0-59)
        int week = cal.get(Calendar.DAY_OF_WEEK); // 星期:1-7(1=周日,2=周一...7=周六)

        System.out.printf("当前时间:%d年%d月%d日 %02d:%02d:%02d,星期%s%n",
                year, month, day, hour, minute, second,
                week == 1 ? "日" : (week - 1) + "");

        // 3. 设置时间字段(修改Calendar对象)
        cal.set(Calendar.YEAR, 2025); // 设置年份为2025
        cal.set(Calendar.MONTH, 11); // 设置月份为12月(0-11)
        cal.set(Calendar.DAY_OF_MONTH, 31); // 设置日期为31日
        System.out.println("设置后的时间:" + cal.getTime()); // 转换为Date对象输出

        // 4. 时间计算(加减操作)
        cal.add(Calendar.DAY_OF_MONTH, 1); // 日期加1天(跨年/跨月会自动调整)
        cal.add(Calendar.MONTH, -1); // 月份减1个月
        System.out.println("计算后的时间:" + cal.getTime());

        // 5. 转换为Date对象
        java.util.Date date = cal.getTime();
        System.out.println("Calendar转Date:" + date);
    }
}

2.2.2 Calendar类的缺陷(仍需注意)

Calendar虽然解决了Date类的部分问题,但依然存在不足,无法满足现代开发的需求:

  • 线程不安全:与Date类一样,Calendar对象是可变的,多线程共享实例会导致并发问题;

  • API设计繁琐:字段操作需通过常量(如Calendar.YEAR),且月份、星期的取值范围不直观,容易踩坑;

  • 时区处理复杂:时区相关操作繁琐,容易出现时区偏移错误;

  • 不可链式调用:时间计算、字段设置需多次调用方法,代码可读性差。

2.3 java.text.SimpleDateFormat:时间格式化工具

Date和Calendar本身不支持自定义格式的字符串转换,需配合SimpleDateFormat类,实现「Date对象 ↔ 字符串」的双向转换。但SimpleDateFormat存在严重的线程安全问题,是开发中常见的坑点。

2.3.1 核心用法:格式化与解析

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatDemo {
    public static void main(String[] args) throws ParseException {
        // 1. 定义格式化模板(区分大小写,不可写错)
        // yyyy:4位年份,MM:2位月份,dd:2位日期,HH:24小时制,hh:12小时制,mm:分钟,ss:秒
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // 2. Date对象 → 字符串(格式化)
        Date now = new Date();
        String timeStr = sdf.format(now);
        System.out.println("格式化后的时间:" + timeStr); // 输出:2026-04-13 09:58:00

        // 3. 字符串 → Date对象(解析)
        String dateStr = "2025-12-31 23:59:59";
        Date date = sdf.parse(dateStr);
        System.out.println("解析后的Date对象:" + date);

        // 4. 其他常用模板
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        System.out.println("中文格式:" + sdf2.format(now));
    }
}

2.3.2 致命坑点:线程不安全

SimpleDateFormat是非线程安全的,其内部存在共享的Calendar实例,多线程并发调用format()或parse()方法时,会导致格式错乱、解析异常甚至数据污染。

避坑方案(二选一):

  • 每次使用时新建SimpleDateFormat实例(轻量,适合低并发场景);

  • 使用ThreadLocal缓存SimpleDateFormat实例(推荐,适合高并发场景),避免多线程共享。

// 高并发场景推荐写法:ThreadLocal缓存SimpleDateFormat
import java.text.SimpleDateFormat;
import java.util.concurrent.ThreadLocalRandom;

public class SafeSimpleDateFormat {
    // ThreadLocal保证每个线程有独立的SimpleDateFormat实例
    private static final ThreadLocal<SimpleDateFormat> SDF_THREAD_LOCAL = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    public static String format(Date date) {
        return SDF_THREAD_LOCAL.get().format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return SDF_THREAD_LOCAL.get().parse(dateStr);
    }
}

三、新时代API:java.time包(Java 8+首选)

Java 8推出的java.time包(也称为JSR 310),彻底解决了旧API的所有缺陷,遵循「不可变性、线程安全、API直观」的设计原则,涵盖了日期、时间、时区、持续时间等所有时间处理场景,是目前Java时间处理的首选方案。

核心类关系:java.time包的核心类都基于Temporal接口,主要分为三类——日期(LocalDate)、时间(LocalTime)、日期时间(LocalDateTime),此外还有Instant(时间戳)、Duration(持续时间)、Period(周期)等辅助类。

3.1 核心类1:LocalDate(仅处理日期,无时间)

LocalDate专门表示「无时间的日期」(如2026-04-13),仅包含年、月、日三个字段,适合只关注日期的场景(如生日、订单日期)。它是不可变对象,线程安全,所有修改操作都会返回新的LocalDate实例,不会修改原对象。

3.1.1 核心用法(创建+字段操作+计算)

import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.TemporalAdjusters;

public class LocalDateDemo {
    public static void main(String[] args) {
        // 1. 创建LocalDate对象(3种常用方式)
        LocalDate today = LocalDate.now(); // 当前日期(系统时区)
        LocalDate birthday = LocalDate.of(1990, Month.JUNE, 15); // 指定日期(Month枚举,避免踩坑)
        LocalDate customDate = LocalDate.of(1990, 6, 15); // 指定日期(数字月份,1-12,直观)
        LocalDate parseDate = LocalDate.parse("2026-04-13"); // 从ISO格式字符串解析(yyyy-MM-dd)

        System.out.println("当前日期:" + today); // 输出:2026-04-13
        System.out.println("指定日期:" + birthday); // 输出:1990-06-15

        // 2. 获取日期字段(直观,无偏移)
        int year = today.getYear(); // 2026
        Month month = today.getMonth(); // APRIL(枚举)
        int monthValue = today.getMonthValue(); // 4(1-12,无需修正)
        int day = today.getDayOfMonth(); // 13
        int dayOfWeek = today.getDayOfWeek().getValue(); // 6(1=周一,7=周日,符合日常习惯)
        int daysInMonth = today.lengthOfMonth(); // 30(当月天数)
        boolean isLeapYear = today.isLeapYear(); // false(是否闰年)

        System.out.printf("年份:%d,月份:%d,日期:%d,星期:%d,当月天数:%d,是否闰年:%b%n",
                year, monthValue, day, dayOfWeek, daysInMonth, isLeapYear);

        // 3. 日期计算(返回新对象,原对象不变)
        LocalDate tomorrow = today.plusDays(1); // 加1天
        LocalDate nextMonth = today.plusMonths(1); // 加1个月
        LocalDate lastYear = today.minusYears(1); // 减1年
        LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth()); // 当月第一天
        LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear()); // 当年最后一天

        System.out.println("明天:" + tomorrow);
        System.out.println("下月今天:" + nextMonth);
        System.out.println("去年今天:" + lastYear);

        // 4. 日期比较
        boolean isBefore = today.isBefore(birthday); // false
        boolean isAfter = today.isAfter(birthday); // true
        boolean isEqual = today.isEqual(parseDate); // true

        // 5. 计算两个日期的间隔(Period:年、月、日)
        java.time.Period period = java.time.Period.between(birthday, today);
        System.out.printf("两个日期间隔:%d年%d月%d天%n",
                period.getYears(), period.getMonths(), period.getDays());
    }
}

3.2 核心类2:LocalTime(仅处理时间,无日期)

LocalTime专门表示「无日期的时间」(如09:58:00),仅包含时、分、秒、纳秒四个字段,适合只关注时间的场景(如上班时间、闹钟时间),同样是不可变对象,线程安全。

3.2.1 核心用法

import java.time.LocalTime;

public class LocalTimeDemo {
    public static void main(String[] args) {
        // 1. 创建LocalTime对象(3种常用方式)
        LocalTime now = LocalTime.now(); // 当前时间(系统时区)
        LocalTime lunchTime = LocalTime.of(12, 30); // 指定时间(时:分)
        LocalTime preciseTime = LocalTime.of(15, 45, 30); // 指定时间(时:分:秒)
        LocalTime parseTime = LocalTime.parse("09:58:00"); // 从ISO格式字符串解析(HH:mm:ss)

        System.out.println("当前时间:" + now); // 输出:09:58:00.123456789(包含纳秒)
        System.out.println("指定时间:" + lunchTime); // 输出:12:30

        // 2. 获取时间字段
        int hour = now.getHour(); // 9(24小时制)
        int minute = now.getMinute(); // 58
        int second = now.getSecond(); // 0
        int nano = now.getNano(); // 123456789(纳秒)

        System.out.printf("时:%d,分:%d,秒:%d,纳秒:%d%n", hour, minute, second, nano);

        // 3. 时间计算(返回新对象)
        LocalTime nextHour = now.plusHours(1); // 加1小时
        LocalTime minusMinute = now.minusMinutes(10); // 减10分钟
        LocalTime truncatedTime = now.truncatedTo(java.time.temporal.ChronoUnit.MINUTES); // 截断到分钟(去掉秒和纳秒)

        System.out.println("1小时后:" + nextHour);
        System.out.println("10分钟前:" + minusMinute);
        System.out.println("截断到分钟:" + truncatedTime);

        // 4. 时间比较
        boolean isBefore = now.isBefore(lunchTime); // true
        System.out.println("当前时间在午餐时间之前:" + isBefore);
    }
}

3.3 核心类3:LocalDateTime(日期+时间,最常用)

LocalDateTime是LocalDate和LocalTime的结合体,包含年、月、日、时、分、秒、纳秒,是最常用的时间类,适合需要同时处理日期和时间的场景(如用户注册时间、日志时间戳),不可变、线程安全。

3.3.1 核心用法(综合实战)

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class LocalDateTimeDemo {
    public static void main(String[] args) {
        // 1. 创建LocalDateTime对象(4种常用方式)
        LocalDateTime now = LocalDateTime.now(); // 当前日期时间
        LocalDateTime specificTime = LocalDateTime.of(2026, 4, 13, 9, 58, 0); // 指定日期时间
        LocalDateTime fromDateAndTime = LocalDateTime.of(LocalDate.now(), LocalTime.now()); // 结合LocalDate和LocalTime
        LocalDateTime parseTime = LocalDateTime.parse("2026-04-13T09:58:00"); // ISO格式解析(T分隔日期和时间)

        System.out.println("当前日期时间:" + now); // 输出:2026-04-13T09:58:00.123456789

        // 2. 字段操作(获取+修改)
        int year = now.getYear();
        int month = now.getMonthValue();
        int day = now.getDayOfMonth();
        int hour = now.getHour();
        LocalDateTime modifiedTime = now.withYear(2025).withMonth(12).withDayOfMonth(31); // 修改字段(返回新对象)

        System.out.printf("当前:%d年%d月%d日 %d时%n", year, month, day, hour);
        System.out.println("修改后的时间:" + modifiedTime);

        // 3. 时间计算(常用场景)
        LocalDateTime nextWeek = now.plusWeeks(1); // 加1周
        LocalDateTime lastMonth = now.minusMonths(1); // 减1个月
        long daysBetween = ChronoUnit.DAYS.between(modifiedTime, now); // 计算两个时间的间隔(天数)
        long hoursBetween = ChronoUnit.HOURS.between(parseTime, now); // 计算间隔(小时)

        System.out.println("1周后:" + nextWeek);
        System.out.println("1个月前:" + lastMonth);
        System.out.printf("两个时间间隔:%d天,%d小时%n", daysBetween, hoursBetween);

        // 4. 格式化(DateTimeFormatter,线程安全,替代SimpleDateFormat)
        // 定义格式化模板
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String timeStr = now.format(formatter); // LocalDateTime → 字符串
        System.out.println("格式化后的时间:" + timeStr); // 输出:2026-04-13 09:58:00

        // 解析(字符串 → LocalDateTime)
        String dateTimeStr = "2025-12-31 23:59:59";
        LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeStr, formatter);
        System.out.println("解析后的时间:" + parsedDateTime);
    }
}

3.4 其他常用辅助类

除了上述三个核心类,java.time包还有几个常用的辅助类,用于处理时间戳、持续时间等场景,补充完善时间处理能力。

3.4.1 Instant:时间戳(对应Date)

Instant表示「UTC时间戳」(格林尼治标准时间),内部存储的是自1970-01-01T00:00:00Z以来的秒数+纳秒数,与Date类的核心作用一致,但精度更高(纳秒级),不可变、线程安全。

import java.time.Instant;
import java.util.Date;

public class InstantDemo {
    public static void main(String[] args) {
        // 1. 创建Instant对象
        Instant now = Instant.now(); // 当前UTC时间戳
        Instant specificInstant = Instant.ofEpochMilli(1712995080000L); // 根据毫秒时间戳创建

        System.out.println("当前UTC时间戳:" + now); // 输出:2026-04-13T01:58:00.123456789Z(Z表示UTC时区)

        // 2. 转换为Date对象(兼容旧API)
        Date date = Date.from(now);
        Instant instantFromDate = date.toInstant();
        System.out.println("Instant转Date:" + date);
        System.out.println("Date转Instant:" + instantFromDate);

        // 3. 时间计算
        Instant later = now.plusSeconds(60); // 加60秒
        Instant earlier = now.minusMillis(1000); // 减1000毫秒
        System.out.println("60秒后:" + later);
    }
}

3.4.2 Duration与Period:时间间隔

Duration和Period都用于表示「时间间隔」,核心区别在于:

  • Duration:表示「时间间隔」(如2小时30分钟),基于秒和纳秒,适合计算两个时间(LocalTime、Instant)之间的间隔;

  • Period:表示「日期间隔」(如1年2个月3天),基于年、月、日,适合计算两个日期(LocalDate)之间的间隔。

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;

public class DurationPeriodDemo {
    public static void main(String[] args) {
        // 1. Duration:计算时间间隔
        LocalTime time1 = LocalTime.of(9, 0, 0);
        LocalTime time2 = LocalTime.of(11, 30, 0);
        Duration duration = Duration.between(time1, time2);
        System.out.println("时间间隔:" + duration.toHours() + "小时" + duration.toMinutes()%60 + "分钟"); // 2小时30分钟

        // 2. Period:计算日期间隔
        LocalDate date1 = LocalDate.of(2025, 1, 1);
        LocalDate date2 = LocalDate.of(2026, 4, 13);
        Period period = Period.between(date1, date2);
        System.out.println("日期间隔:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天"); // 1年3个月12天
    }
}

四、新旧API转换(实战必备)

开发中经常需要在新旧API之间转换(如遗留系统用Date,新项目用LocalDateTime),以下是常用的转换方法,直接复制可用。

4.1 Date ↔ LocalDateTime(最常用)

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

public class DateLocalDateTimeConvert {
    public static void main(String[] args) {
        // 1. Date → LocalDateTime(需指定时区,默认系统时区)
        Date date = new Date();
        LocalDateTime localDateTime = date.toInstant()
                .atZone(ZoneId.systemDefault()) // 转换为系统时区的ZonedDateTime
                .toLocalDateTime(); // 转换为LocalDateTime
        System.out.println("Date转LocalDateTime:" + localDateTime);

        // 2. LocalDateTime → Date(需指定时区)
        LocalDateTime now = LocalDateTime.now();
        Date convertDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
        System.out.println("LocalDateTime转Date:" + convertDate);
    }
}

4.2 Calendar ↔ LocalDateTime

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;

public class CalendarLocalDateTimeConvert {
    public static void main(String[] args) {
        // 1. Calendar → LocalDateTime
        Calendar cal = Calendar.getInstance();
        LocalDateTime localDateTime = cal.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();
        System.out.println("Calendar转LocalDateTime:" + localDateTime);

        // 2. LocalDateTime → Calendar
        LocalDateTime now = LocalDateTime.now();
        Calendar convertCal = Calendar.getInstance();
        convertCal.setTime(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()));
        System.out.println("LocalDateTime转Calendar:" + convertCal.getTime());
    }
}

五、Java时间处理实战场景(开发高频)

结合实际开发场景,整理以下高频需求的实现代码,直接复用,覆盖格式化、时间计算、校验等核心场景。

5.1 场景1:时间格式化(统一工具类)

封装java.time的DateTimeFormatter,实现统一的时间格式化和解析,替代线程不安全的SimpleDateFormat。

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

/**
 * 时间格式化工具类(Java 8+,线程安全)
 */
public class TimeFormatterUtil {
    // 常用格式化模板(可根据需求扩展)
    public static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    public static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");

    // 私有化构造方法,禁止实例化
    private TimeFormatterUtil() {}

    // 1. LocalDateTime格式化
    public static String formatLocalDateTime(LocalDateTime localDateTime) {
        if (localDateTime == null) {
            return null;
        }
        return localDateTime.format(DATETIME_FORMAT);
    }

    // 2. 字符串解析为LocalDateTime
    public static LocalDateTime parseLocalDateTime(String timeStr) {
        if (timeStr == null || timeStr.isEmpty()) {
            return null;
        }
        try {
            return LocalDateTime.parse(timeStr, DATETIME_FORMAT);
        } catch (DateTimeParseException e) {
            e.printStackTrace();
            return null; // 解析失败返回null,也可抛出异常
        }
    }

    // 3. LocalDate格式化
    public static String formatLocalDate(LocalDate localDate) {
        if (localDate == null) {
            return null;
        }
        return localDate.format(DATE_FORMAT);
    }

    // 4. LocalTime格式化
    public static String formatLocalTime(LocalTime localTime) {
        if (localTime == null) {
            return null;
        }
        return localTime.format(TIME_FORMAT);
    }

    // 测试
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("格式化:" + formatLocalDateTime(now));
        System.out.println("解析:" + parseLocalDateTime("2026-04-13 09:58:00"));
    }
}

5.2 场景2:计算两个时间的间隔(如计算年龄、有效期)

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;

/**
 * 时间计算工具类
 */
public class TimeCalculateUtil {
    // 1. 根据生日计算年龄
    public static int calculateAge(LocalDate birthDate) {
        if (birthDate == null) {
            throw new IllegalArgumentException("生日不能为空");
        }
        LocalDate today = LocalDate.now();
        // 若今年生日还没到,年龄减1
        if (birthDate.getMonthValue() > today.getMonthValue() 
                || (birthDate.getMonthValue() == today.getMonthValue() 
                && birthDate.getDayOfMonth() > today.getDayOfMonth())) {
            return Period.between(birthDate, today).getYears() - 1;
        }
        return Period.between(birthDate, today).getYears();
    }

    // 2. 计算两个时间的间隔(天数)
    public static long calculateDaysBetween(LocalDateTime start, LocalDateTime end) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("时间不能为空");
        }
        return ChronoUnit.DAYS.between(start, end);
    }

    // 3. 判断时间是否在有效期内(如订单是否超时)
    public static boolean isWithinValidity(LocalDateTime startTime, LocalDateTime endTime, LocalDateTime currentTime) {
        if (startTime == null || endTime == null || currentTime == null) {
            return false;
        }
        return currentTime.isAfter(startTime) && currentTime.isBefore(endTime);
    }

    // 测试
    public static void main(String[] args) {
        LocalDate birthDate = LocalDate.of(1990, 6, 15);
        System.out.println("年龄:" + calculateAge(birthDate));

        LocalDateTime start = LocalDateTime.of(2026, 4, 1, 0, 0, 0);
        LocalDateTime end = LocalDateTime.of(2026, 4, 15, 23, 59, 59);
        System.out.println("间隔天数:" + calculateDaysBetween(start, end));
        System.out.println("当前时间是否在有效期内:" + isWithinValidity(start, end, LocalDateTime.now()));
    }
}

5.3 场景3:获取指定时间(如当月第一天、下周同一时间)

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.time.DayOfWeek;

public class SpecificTimeUtil {
    // 1. 获取当月第一天(LocalDate)
    public static LocalDate getFirstDayOfMonth() {
        return LocalDate.now().with(TemporalAdjusters.firstDayOfMonth());
    }

    // 2. 获取当月最后一天(LocalDate)
    public static LocalDate getLastDayOfMonth() {
        return LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
    }

    // 3. 获取下周同一时间(LocalDateTime)
    public static LocalDateTime getNextWeekSameTime() {
        return LocalDateTime.now().plusWeeks(1);
    }

    // 4. 获取当月第一个周一
    public static LocalDate getFirstMondayOfMonth() {
        return LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
    }

    // 测试
    public static void main(String[] args) {
        System.out.println("当月第一天:" + getFirstDayOfMonth());
        System.out.println("当月最后一天:" + getLastDayOfMonth());
        System.out.println("下周同一时间:" + getNextWeekSameTime());
        System.out.println("当月第一个周一:" + getFirstMondayOfMonth());
    }
}

六、Java时间处理避坑指南(开发+面试重点)

时间处理看似简单,但容易出现各种坑点,尤其是旧API的使用和时区问题,以下是高频坑点及解决方案,也是面试中常考的知识点。

6.1 坑点1:旧API的线程安全问题(Date/Calendar/SimpleDateFormat)

Date、Calendar是可变对象,多线程共享会导致线程安全问题;SimpleDateFormat非线程安全,并发调用会出现格式错乱。

解决方案:

  • 新项目直接使用java.time包新API(不可变、线程安全);

  • 遗留系统:SimpleDateFormat使用ThreadLocal缓存,Date/Calendar避免多线程共享,每次使用时新建实例。

6.2 坑点2:月份取值范围混淆(旧API)

Calendar的月份是0-11(0代表1月,11代表12月),Date类的getMonth()方法(已废弃)也是0-11,新手容易忘记+1,导致月份错误。

解决方案:

  • 使用Calendar时,获取月份后必须+1(如cal.get(Calendar.MONTH) + 1);

  • 优先使用java.time包的LocalDate,月份是1-12,直观无偏移。

6.3 坑点3:时区混淆(最隐蔽)

旧API(Date/Calendar)默认依赖系统时区,Instant表示UTC时区,若不注意时区转换,会导致时间偏差(如UTC时间比北京时间晚8小时)。

解决方案:

  • 转换API时(如Date ↔ LocalDateTime),必须指定时区(推荐使用ZoneId.systemDefault()获取系统时区,或明确指定时区如ZoneId.of("Asia/Shanghai"));

  • 跨系统传输时间时,建议使用UTC时间戳(Instant),避免时区差异导致的错误。

6.4 坑点4:格式化模板错误(大小写混淆)

格式化模板中,大小写代表不同含义,写错会导致格式化/解析失败,常见错误:

  • yyyy(4位年份) vs YYYY(周基年,可能导致跨年错误);

  • MM(2位月份) vs mm(2位分钟);

  • HH(24小时制) vs hh(12小时制);

  • dd(2位日期) vs DD(一年中的第几天)。

解决方案:牢记常用模板的大小写,避免混用,推荐封装统一的格式化工具类。

6.5 坑点5:不可变对象的使用误区(新API)

java.time包的所有类都是不可变对象,调用plusXXX()、minusXXX()、withXXX()等方法时,不会修改原对象,而是返回新对象,新手容易误以为原对象被修改。

七、总结

Java时间处理的核心是「选择合适的API」:Java 8+新项目优先使用java.time包(LocalDate、LocalTime、LocalDateTime),其不可变性、线程安全、直观的API设计,能彻底解决旧API的痛点;维护遗留系统时,需掌握Date、Calendar、SimpleDateFormat的用法,同时注意规避线程安全、月份偏移等坑点。

本文涵盖了Java时间处理的所有核心知识点,从API演变、核心类用法,到实战工具类、避坑指南,层层递进,代码可直接复用。时间处理的关键在于「细节」——时区、格式化模板、不可变对象的使用,只要掌握这些细节,就能从容应对所有Java时间处理场景。

最后提醒:开发中建议统一时间处理规范,优先使用新API,封装通用工具类,避免重复编码,同时注意时区一致性,避免因时间偏差导致的业务问题。 Java时间处理详解:从Date、Calendar到java.time新API 时间处理是Java开发中最基础、最高频的场景之一——用户注册时间记录、订单超