Java 里的java.util.Date和java.sql.Date绝对是那种看起来不起眼但能搞得你 Debug 到半夜的“坑王”。
我们先从表面上看,java.sql.Date是继承自java.util.Date的,按理说是“子承父业”,兼容没问题对吧?但实际上,这俩压根儿不是一个世界的。java.util.Date是 Java 标准库里提供的“通用型”时间类,而java.sql.Date则是 JDBC 里为了数据库操作特化出来的版本,核心区别就在于:一个有时间,一个没时间!
就是这么沙雕的区别,java.sql.Date把时间部分(小时、分钟、秒)全都砍了个干净,只保留年月日。为什么这么干?因为很多数据库的DATE类型本来就只存年月日,比如 MySQL 的DATE字段。所以java.sql.Date就跟着这个“传统艺能”走了,给你个精确到天的“日子”。
来看段代码你就明白了:
public class DateExample {
public static void main(String[] args) {
java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
System.out.println("utilDate: " + utilDate); // 有时间
System.out.println("sqlDate: " + sqlDate); // 只有年月日
}
}
输出可能是这样的:
utilDate: Thu Jun 05 15:30:12 CST 2025
sqlDate: 2025-06-05
你看,时间部分直接没了!这就导致很多时候你把java.sql.Date传回前端,前端一看没时间,直接懵了...
我遇到过最离谱的是,有个哥们把java.sql.Date强转成java.util.Date,然后还奇怪为啥结果有问题。讲真,这是个巨坑。虽然它们在继承关系上确实可以强转,但你转完之后,它的“灵魂”还是那个没有时间的sql.Date
所以你如果真的要拿java.sql.Date去做一些带时间的运算,比如和当前时间比较,或者转换成时间戳,那基本就是拿刀割自己手指头。比如这个例子:
java.sql.Date sqlDate = java.sql.Date.valueOf("2025-06-05");
long millis = sqlDate.getTime(); // 你以为这是某个时间点?
Date converted = new Date(millis);
System.out.println(converted); // 时间是 00:00:00 啊兄弟!
你以为你得到了“某一天的某个时刻”,结果其实是“那天的零点”... 这还不如直接用LocalDate呢。
对,说到这个就得扯一下 Java 8 的时间新 API——java.time.*。这波更新真的可以说是“Date 的反攻”。像LocalDate和LocalDateTime就分别对应java.sql.Date和java.util.Date的干净替代品,前者精确到天,后者精确到秒,而且线程安全,没有时区困扰,API 超清晰,看着就让人舒服🥰。
LocalDate localDate = LocalDate.now();
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("LocalDate: " + localDate); // 2025-06-05
System.out.println("LocalDateTime: " + localDateTime); // 2025-06-05T15:45:30
最重要的是,java.sql.Date、java.sql.Time和java.sql.Timestamp都有对应的toLocalDate()或toLocalDateTime()方法,基本可以无痛迁移。
但你要说现实项目里能不能彻底抛弃java.sql.Date?我只能说:想多了。只要你在用 JDBC,尤其是用 MyBatis 或 Hibernate 这种 ORM 框架,它们底层还是会默认你搞点java.sql.Date出来。所以我个人的策略是:入库出库都用LocalDate/LocalDateTime,中间统一转换。
比如用 MyBatis 的 TypeHandler 机制,写一个LocalDate转java.sql.Date的 Handler:
@MappedTypes(LocalDate.class)
public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
LocalDate parameter, JdbcType jdbcType) throws SQLException {
ps.setDate(i, Date.valueOf(parameter));
}
@Override
public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
Date date = rs.getDate(columnName);
return date != null ? date.toLocalDate() : null;
}
// 其他两个 get 方法略
}
配上 XML 或注解,就能实现LocalDate的透明持久化,再也不用在每个 Entity 里扔一堆类型转换的代码了
另外顺带一提,还有不少人会问:“那java.sql.Timestamp是啥角色?”很简单,它就是java.util.Date+ 纳秒精度的变种,主要是数据库里有精确到秒甚至微秒的TIMESTAMP字段时用的。但我一般都尽量避免用它,因为那玩意儿坑更多,特别是跨数据库还经常有精度兼容性问题(MySQL 精确到毫秒,Oracle 精确到纳秒,一导出 CSV 就炸了...)
回到最初的问题:到底java.util.Date和java.sql.Date有啥区别?
如果用一句话总结就是:
java.util.Date是全能选手,啥时间信息都有; java.sql.Date是简化版,为了和数据库的DATE类型打配合,砍掉了时间部分,只保留“日期”。
但这区别,常常在你不注意的时候就搞事,所以我现在干脆上来就用LocalDate和LocalDateTime,实在不行中间过渡一下,也总比在项目上线前踩坑强
不知道你们是不是也在项目中被这俩“日期兄弟”折腾过?欢迎留言交流,毕竟这个坑,谁踩谁知道...
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek
也可以看我写的这篇文章《DeepSeek满血复活,直接起飞!》来进行本地搭建。
东哥作为一名超级老码农,整理了全网最全《Java高级架构师资料合集》。