Java 日期格式化(附 3 种方法),含完整示例与避坑

编程狮(w3cschool.cn) 2026-09-23 13:03:37 浏览数 (43)
反馈

在 Java 里做 java日期格式化,先把结论放在最前面:新项目应当直接使用 java.time 体系(也就是 LocalDateTime 再加上 DateTimeFormatter);只有在维护 JDK 7 及更早的老代码时,才继续沿用 SimpleDateFormat;并且要记住 SimpleDateFormat 线程不安全,多线程下共享它会出乱子。很多新人照着旧博客写 new SimpleDateFormat 然后当成静态字段共享,结果并发请求里拿到一堆错位日期,排查很久才发现是格式化工具本身的问题。本文会把三种常用方式逐个拆开,每种都给带逐行中文注释的示例,并整理一张排错表,把 y 与 Y、M 与 m、线程安全、时区偏差这些高频坑一次讲清,让你既能写对代码,也能向同事解释为什么新项目不该再碰 SimpleDateFormat。

Java 日期格式化封面图

一、先看结论:Java 日期格式化怎么选

为了让读者少走弯路,这一节先用一张表把三种方式的取舍说清楚。java日期格式化 这件事,本质上是在“可读性、性能、线程安全、JDK 版本”之间做选择。除了本文聚焦的 java日期格式化,广义的 Java 时间格式化 还包括字符串解析、时区转换与本地化展示,但格式化的选型优先级是一致的。下面这张方法选择表建议收藏,做技术选型时先看它一眼。

方式 适用场景 不推荐场景 推荐顺序
DateTimeFormatter + LocalDateTime Java 8 及以上新项目、Spring Boot 等现代服务、高并发接口 需要兼容 JDK 6 / 7 的老旧环境 1(首推)
SimpleDateFormat 维护 JDK 7 及更早的历史代码、第三方库内部调用 任何多线程共享同一个实例的场景 2(兼容)
String.format 简单拼时间串、写日志、一次性格式化 需要灵活自定义格式、做国际化的复杂场景 3(补充)

读完这张表你应该明白,java日期格式化 的首选已经不是老旧的 SimpleDateFormat。只要 JDK 版本允许,优先用 java.time 体系,它不可变、天然线程安全,写出来的代码也更不容易出 bug。下面我们进入最小可运行示例。

二、环境与最小示例

这一节给出本地能跑起来的最小 java日期格式化 示例,方便你先跑通再逐步改造。环境要求很简单:JDK 8 及以上,任意文本编辑器或 IDE 即可,不需要额外依赖。下面这段代码用最常见的 yyyy-MM-dd HH:mm:ss 把当前时间格式化成字符串,每一行都加了中文注释,告诉你这行在做什么、为什么这样写。后面所有 java日期格式化 的写法,模板字母含义都和这里保持一致,记住一次就能举一反三。

// 导入 java.time 包下的日期时间类
import java.time.LocalDateTime;
// 导入格式化器,Java 8 新 API 提供的线程安全格式化工具
import java.time.format.DateTimeFormatter;

public class Demo {
    public static void main(String[] args) {
        // 获取当前系统时间(不含时区信息,适合本地时间展示)
        LocalDateTime now = LocalDateTime.now();
        // 定义格式模板:y 年 M 月 d 日 H 24小时制时 m 分 s 秒
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // 把时间对象按模板格式化成字符串
        String result = now.format(fmt);
        // 打印结果,例如 2026-09-23 14:08:30
        System.out.println(result);
    }
}

(以下为说明性代码,未在本机执行;预期结果:控制台输出形如 2026-09-23 14:08:30 的当前时间字符串。)

要注意的是,DateTimeFormatter 的 ofPattern 方法接收的模板字母含义是固定的:y 表示年、M 表示月、d 表示日、H 表示 24 小时制的小时、m 表示分钟、s 表示秒。之后无论用哪种方式做 java日期格式化,这些字母的含义都保持一致,只是 API 形态不同。把这个最小示例跑通,你就已经掌握了现代 Java 格式化器的核心用法。在本地运行验证输出格式是否符合预期,配置好时区后再排查偏差,检查模板字母是否写反。

三、方式一:SimpleDateFormat

SimpleDateFormat 是 JDK 1.1 就存在的老 API,很多历史项目仍在用它。它曾经是 Java 做 java日期格式化 的标准答案,如今只建议保留在老代码里。它的用法表面上和 DateTimeFormatter 很像,但有一个致命缺陷:线程不安全,这也是很多老项目在做 java日期格式化 时翻车的根源。下面先看标准用法,再讲为什么不能共享。

// 导入日期与格式化相关类
import java.text.SimpleDateFormat;
import java.util.Date;

public class SdfDemo {
    public static void main(String[] args) {
        // 获取当前时刻的 Date 对象
        Date now = new Date();
        // 创建格式化器,模板含义:y 年 M 月 d 日 H 时 m 分 s 秒
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 格式化为字符串,注意 sdf 是方法内局部变量
        String result = sdf.format(now);
        // 输出形如 2026-09-23 14:08:30
        System.out.println(result);
    }
}

(以下为说明性代码,未在本机执行;预期结果:控制台输出当前时间的 yyyy-MM-dd HH:mm:ss 字符串。)

之所以强调“局部变量”,是因为 SimpleDateFormat 内部持有可变的 Calendar 状态。如果在多线程里把同一个 SimpleDateFormat 实例当成静态字段共享,多个线程会同时修改它的内部状态,导致格式化结果错乱,严重时抛 ArrayIndexOutOfBoundsException。这就是 java日期格式化 里最经典的线程安全坑。

如果你确实要在老代码里复用实例,有两种正确姿势。第一种是每次都 new 一个新的局部变量,简单但高并发下对象创建成本高。第二种是用 ThreadLocal,让每个线程各持一份实例:

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

public class SdfThreadLocal {
    // 用 ThreadLocal 给每个线程分配独立的 SimpleDateFormat 实例
    private static final ThreadLocal<SimpleDateFormat> TL =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static String format(Date date) {
        // 每个线程拿到自己那份,互不干扰,避免线程安全问题
        return TL.get().format(date);
    }
}

(以下为说明性代码,未在本机执行;预期结果:format 方法返回线程安全的格式化字符串。)

不过即便用 ThreadLocal,这套写法也比 java.time 啰嗦。所以新项目做 java日期格式化,还是建议直接上 DateTimeFormatter,从语言层面消灭可变状态带来的隐患。

四、方式二:DateTimeFormatter + LocalDateTime

DateTimeFormatter 是 Java 8 引入的 java.time 体系核心组件,它最大的优势是:不可变、天然线程安全,可以放心地定义成静态常量在多线程间共享。下面示例展示如何把格式化器声明为常量并复用。

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

public class DtfDemo {
    // 定义为静态常量也没问题,DateTimeFormatter 是不可变且线程安全的
    private static final DateTimeFormatter FMT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        // 取当前本地时间
        LocalDateTime now = LocalDateTime.now();
        // 直接复用常量格式化器,多线程下不会出现错乱
        String result = now.format(FMT);
        // 输出当前时间字符串
        System.out.println(result);
    }
}

(以下为说明性代码,未在本机执行;预期结果:控制台输出形如 2026-09-23 14:08:30 的当前时间。)

相比 SimpleDateFormat,DateTimeFormatter + LocalDateTime 还有更多好处。首先是语义清晰:LocalDateTime 只表示“本地日期时间”,不含时区;如果需要带时区,可以用 ZonedDateTime 配合带 z / Z 的模板。其次是解析同样简单,调用 LocalDateTime.parse(text, FMT) 即可把字符串还原成对象。再次是它支持 ofPattern 的第二个参数传入 Locale,方便做本地化。对于现代项目的 java日期格式化 需求,这一套几乎全覆盖,既安全又易读。

想在更系统的层面把这些 API 吃透,可以先从基础教程打底,例如 Java 教程;平时写代码想快速查字母含义,用 Java 速查手册会更顺手。

五、方式三:String.format 与常见失败

String.format 也能做 java日期格式化,但不是用它直接吃 Date 对象,而是借助 %t 系列转换符。常见写法如下:

import java.util.Date;

public class FormatDemo {
    public static void main(String[] args) {
        // 取当前时间
        Date now = new Date();
        // %tF 输出 yyyy-MM-dd,%tT 输出 HH:mm:ss,拼成完整时间串
        String result = String.format("%tF %tT", now, now);
        // 输出形如 2026-09-23 14:08:30
        System.out.println(result);
    }
}

(以下为说明性代码,未在本机执行;预期结果:控制台输出 2026-09-23 14:08:30。)

String.format 适合简单拼接,但自由度不如模板字符串,且可读性一般。和前面两种相比,java日期格式化 用 String.format 的容错空间更小,模板写错不会报错只会给错结果。更重要的是,它同样绕不开那些经典坑。下面这张排错表把 java日期格式化 里最高频的失败现象、原因和修复方向列出来,建议对照自查。

现象 常见原因 修复方向
年份变成 2027 之类错位的大数 把小写 y(日历年)误写成大写 Y(星期年,基于周) 普通日期一律用 yyyy,只有按周统计场景才用 YYYY
月份显示为 1 位分钟数或时间整体错乱 把大写 M(月)误写成小写 m(分钟) 月份用 MM,分钟用 mm,二者不要混用
并发下偶发日期错乱、抛 ArrayIndexOutOfBoundsException 多线程共享同一个 SimpleDateFormat 静态实例 改用 DateTimeFormatter,或用 ThreadLocal 隔离
显示时间比预期早/晚 8 小时 服务器时区与本地不一致,SimpleDateFormat 默认取 JVM 时区 显式指定时区,或用带时区的 ZonedDateTime

Java 日期格式化决策图

总结

回顾全文,java日期格式化 的正确姿势其实只有一句话:新项目用 java.time(LocalDateTime + DateTimeFormatter),老代码维护才用 SimpleDateFormat,且后者线程不安全、必须隔离使用。三种方式各有定位——DateTimeFormatter 是首选,SimpleDateFormat 负责兼容历史代码,String.format 适合轻量拼接。真正容易出事的从来不是“怎么写格式”,而是把 y 与 Y、M 与 m 写反,或在多线程里共享可变格式化器。把本文的方法选择表和排错表存好,下次再遇到日期错乱,基本可以两分钟定位。编程狮 也建议团队把日期工具类统一收敛到 java.time,从根上消灭线程安全隐患,让 Java 时间格式化 这件小事不再成为线上事故的来源。想快速查格式字母,可随时翻 Java 速查手册

延伸学习

常见问题

Q:SimpleDateFormat 为什么线程不安全,一定要用 ThreadLocal 吗?

A:SimpleDateFormat 内部用 Calendar 做中间状态,format 方法会先清空再写入这个共享状态。多个线程同时调用同一个实例时,彼此覆盖对方写入的值,于是得到错误年份、错位月份甚至抛异常。是否用 ThreadLocal 取决于场景:最省心的是每次 new 局部实例;若想减少对象创建,再用 ThreadLocal 给每个线程各存一份;而在 Java 8+ 项目中,直接用线程安全的 DateTimeFormatter 是更彻底的解决方案,根本不需要再为 java日期格式化 担心隔离问题。

Q:yyyy 和 YYYY 到底有什么区别,什么时候会踩坑?

A:小写 y 是日历年(calendar year),大写 Y 是星期年(week year),按 ISO 规则以周四所在周定年。绝大多数业务场景应当用小写 yyyy。坑点出现在年末那一周:例如 2025-12-29 是周一,按某些地区的周规则它属于 2026 年的第一周,于是用 YYYY 格式化会输出 2026,导致年份凭空跳变。做 java日期格式化 时,除非你明确在做“按周统计的年份”,否则一律写 yyyy,避免年末日期错乱。这也是为什么编程狮 在教程里反复强调先用对字母再做格式化。

Q:服务器显示的时间和本地差了 8 小时,怎么解决?

A:这是时区问题。SimpleDateFormat 默认读取 JVM 时区,而服务器常设在 UTC,与东八区相差 8 小时;另外数据库存储若用 UTC 而展示用本地时间,也会错位。修复方向有三:一是显式给格式化器指定时区,如 sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));二是优先使用带时区的 ZonedDateTime / OffsetDateTime,让时间自带时区信息;三是统一服务端、数据库、前端的时区约定,避免到处换算。新项目用 java.time 能显著降低这类偏差。

0 人点赞