Java 8引入的java.time API彻底重构了日期时间处理模型,用不可变对象取代了原先可变的Date和Calendar。在字符串与日期对象之间做转换时,核心类包括LocalDateTime、ZonedDateTime、Instant以及格式化类DateTimeFormatter。理解这些类型的边界,是写出健壮转换代码的前提。

一、为什么放弃SimpleDateFormat
在Java 8之前,开发者习惯用SimpleDateFormat把字符串转成Date。但该类内部持有日历状态,并非线程安全。在Web应用的多线程请求中,若把单个SimpleDateFormat实例作为静态变量共享,会出现格式化结果错乱甚至抛出异常。
java.time中的DateTimeFormatter设计成不可变且线程安全,可以放心声明为常量。同时,新的API把日期、时间、时区拆分成不同职责的类,避免了Date既带日期又带时间的模糊语义。例如LocalDateTime仅表达年月日时分秒,不含时区,而ZonedDateTime明确绑定ZoneId。
二、基础字符串解析与格式化
最常见的需求是把形如2024-03-15 14:30:00的字符串转成对象,再输出成另一种样式。DateTimeFormatter提供了ofPattern方法定义模板,parse负责反向解析,format负责正向输出。
下面示例演示将无时区的本地时间字符串转为LocalDateTime,并格式化为带斜杠的日期加中文时间描述:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Demo {
public static void main(String[] args) {
// 定义输入格式
DateTimeFormatter inputFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String text = "2024-03-15 14:30:00";
// 解析为LocalDateTime
LocalDateTime ldt = LocalDateTime.parse(text, inputFmt);
// 定义输出格式
DateTimeFormatter outputFmt = DateTimeFormatter.ofPattern("yyyy/MM/dd HH时mm分");
String result = ldt.format(outputFmt);
System.out.println(result); // 2024/03/15 14时30分
}
}
上述代码没有使用任何可变状态,inputFmt和outputFmt都可提取为静态常量供全局复用。若输入字符串不符合模板,parse会抛出DateTimeParseException,调用方应捕获并处理为业务提示。
与旧API相比,这种写法可读性更高:模板字母含义明确,且编译期不依赖隐藏的默认时区。LocalDateTime本身不涉及时区转换,因此适合内部业务逻辑只关心钟表时间的场景。
三、带时区的时间字符串处理
系统间通信常使用UTC时间,字符串以Z结尾表示零时区,例如2024-03-15T06:30:00Z。此时应使用Instant或ZonedDateTime解析,而不是直接用LocalDateTime,否则会丢失时区信息。
Instant表示时间线上的瞬时点,可从Z结尾字符串直接解析。需要按用户所在时区展示时,再绑定ZoneId转成ZonedDateTime。以下代码展示了解析UTC并转为上海时区的完整过程:
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class ZoneDemo {
public static void main(String[] args) {
String utcText = "2024-03-15T06:30:00Z";
// Instant天然支持Z后缀
Instant instant = Instant.parse(utcText);
// 绑定上海时区
ZonedDateTime shanghaiTime = instant.atZone(ZoneId.of("Asia/Shanghai"));
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println(shanghaiTime.format(fmt)); // 2024-03-15 14:30:00 CST
}
}
这段代码中,Instant.parse对ISO-8601的Z格式有内建支持,无需自定义模板。转为ZonedDateTime后,时间值自动从UTC偏移加八小时,体现真实本地钟点。
如果错误地把Z字符串交给LocalDateTime.parse,会由于缺少时区字段而解析失败,或者勉强用自定义模板去掉Z后误当作本地时间,导致后续计算偏差八小时。明确数据类型语义,是避免此类问题的关键。
四、常用预定义格式与自定义混合
除了ofPattern,DateTimeFormatter还提供ISO_LOCAL_DATE、ISO_OFFSET_DATE_TIME等静态常量,用于处理标准协议格式。在对外网关对接时,优先使用预定义常量可减少模板拼错风险。
当业务要求非标准展示,例如把日期写成二零二四年三月十五日,则需借助DateTimeFormatterBuilder拼装本地化文本。下面示例构建一个中文长日期格式:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
public class BuilderDemo {
public static void main(String[] args) {
DateTimeFormatter cnFmt = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR)
.appendLiteral("年")
.appendValue(ChronoField.MONTH_OF_YEAR)
.appendLiteral("月")
.appendValue(ChronoField.DAY_OF_MONTH)
.appendLiteral("日")
.toFormatter();
LocalDate date = LocalDate.of(2024, 3, 15);
System.out.println(date.format(cnFmt)); // 2024年3月15日
}
}
DateTimeFormatterBuilder适合字段顺序复杂或需插入固定文字的场景。它逐步追加字段与字面量,比单纯ofPattern更容易应对多语言需求。
在真实项目中,建议把所有的Formatter集中放在一个配置类里,按用途命名。这样既统一了全系统的日期样式,也方便后续调整而不必改动业务代码。
五、转换中的异常与容错
字符串来源不可信,前端或第三方可能传入空串、错位格式。直接调用parse会抛出DateTimeParseException,应在入口层做校验。
一种稳健做法是封装工具方法,对null或空白返回Optional.empty,而非抛异常中断流程。示例:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;
public class SafeParse {
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static Optional<LocalDateTime> tryParse(String text) {
if (text == null || text.trim().isEmpty()) {
return Optional.empty();
}
try {
return Optional.of(LocalDateTime.parse(text, FMT));
} catch (DateTimeParseException e) {
return Optional.empty();
}
}
}
通过返回Optional,调用方必须显式处理空值,避免NPE。相比抛出异常,这种风格在批量处理日志或文件导入时更平滑。
此外,若输入含有毫秒如2024-03-15 14:30:00.123,模板需写成yyyy-MM-dd HH:mm:ss.SSS,否则会解析失败。明确上下游约定的精度,是接口联调阶段必须确认的细节。
六、与旧API的互操作
遗留系统仍可能返回java.util.Date。java.time提供toInstant方法实现桥接:Date对象可转Instant,再转LocalDateTime并绑定系统默认时区。
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
public class LegacyBridge {
public static LocalDateTime fromDate(Date date) {
if (date == null) {
return null;
}
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}
反向转换时,可由LocalDateTime.atZone(ZoneId.systemDefault()).toInstant()得到Instant,再构造Date.from(instant)。这种桥接让新老代码可以并存迁移,不必一次性重写。
要注意系统默认时区可能因部署环境不同而变化,生产容器常设UTC。因此显式传入ZoneId比依赖systemDefault更安全,能防止在本地调试正常、线上偏移八小时的经典故障。