Java 的 java.util.Date
类是 Java 早期用于表示特定时间点(精确到毫秒)的核心类。尽管在 Java 8 中引入了更现代、更强大的 java.time
包(如 LocalDateTime
, ZonedDateTime
, Instant
等),Date
类仍在许多旧系统和库中广泛使用。掌握 Date
类对于维护和理解现有代码至关重要。
一、核心概念
时间表示:
Date
对象表示自 1970年1月1日 00:00:00 UTC(格林威治标准时间) 以来经过的毫秒数(称为 Unix 时间戳或纪元时间)。- 例如:
new Date()
创建一个表示当前时间的Date
对象。
内部存储:
- 实际上,
Date
类内部使用一个long
类型的字段fastTime
来存储从纪元时间到当前时间的毫秒偏移量。
- 实际上,
时区无关性(但有陷阱):
Date
对象本身只存储一个时间点(毫秒值),不包含时区信息。- 但是,其
toString()
方法会使用 JVM 的默认时区来格式化输出,这常导致误解。
已过时的方法:
Date
类中许多用于获取年、月、日等的方法(如getYear()
,getMonth()
,getDate()
)已被标记为@Deprecated
,应避免使用。
二、操作步骤(非常详细)
步骤 1:创建 Date
对象
1.1 创建表示当前时间的 Date
对象
import java.util.Date;
// 方法一:使用无参构造函数
Date now = new Date();
System.out.println("当前时间: " + now); // 输出格式依赖于默认时区
// 方法二:使用 System.currentTimeMillis()
long currentTimeMillis = System.currentTimeMillis();
Date now2 = new Date(currentTimeMillis);
1.2 创建表示特定时间的 Date
对象
// 从时间戳创建(毫秒)
long specificTimestamp = 1672531200000L; // 例如:2023-01-01 00:00:00 UTC
Date specificDate = new Date(specificTimestamp);
// 从字符串解析(需要 SimpleDateFormat,见后续步骤)
步骤 2:获取时间信息(推荐使用 Calendar
或 java.time
)
⚠️ 注意:以下方法已过时,仅作了解,不推荐在新代码中使用。
// 已过时方法示例(不推荐)
Date date = new Date();
int year = date.getYear(); // 返回自1900年起的年份(如 123 表示 2023年)
int month = date.getMonth(); // 返回 0-11(0=1月)
int day = date.getDate(); // 返回 1-31
✅ 推荐做法:使用 Calendar
import java.util.Calendar;
Date date = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int year = cal.get(Calendar.YEAR); // 2025
int month = cal.get(Calendar.MONTH) + 1; // 1-12(注意:Calendar.MONTH 从 0 开始)
int day = cal.get(Calendar.DAY_OF_MONTH); // 1-31
int hour = cal.get(Calendar.HOUR_OF_DAY); // 0-23
int minute = cal.get(Calendar.MINUTE); // 0-59
int second = cal.get(Calendar.SECOND); // 0-59
步骤 3:格式化 Date
为字符串
使用 SimpleDateFormat
(线程不安全!)
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
Date date = new Date();
// 定义格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
String formattedDate = sdf.format(date);
System.out.println("格式化时间: " + formattedDate); // 例如:2025-08-15 10:32:00
// 其他常见格式
SimpleDateFormat sdf2 = new SimpleDateFormat("dd/MM/yyyy");
String dateStr = sdf2.format(date); // 15/08/2025
步骤 4:将字符串解析为 Date
String dateString = "2025-08-15 10:30:00";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Date parsedDate = sdf.parse(dateString);
System.out.println("解析的日期: " + parsedDate);
} catch (ParseException e) {
e.printStackTrace();
}
步骤 5:比较两个 Date
对象
Date date1 = new Date();
// 模拟稍后的时间
Date date2 = new Date(date1.getTime() + 1000 * 60); // 1分钟后
// 方法一:compareTo()
int result = date1.compareTo(date2);
if (result < 0) {
System.out.println("date1 在 date2 之前");
} else if (result > 0) {
System.out.println("date1 在 date2 之后");
} else {
System.out.println("date1 和 date2 相同");
}
// 方法二:before(), after(), equals()
if (date1.before(date2)) {
System.out.println("date1 在 date2 之前");
}
if (date2.after(date1)) {
System.out.println("date2 在 date1 之后");
}
if (date1.equals(date2)) {
System.out.println("两个日期相等");
}
步骤 6:计算时间差
Date start = new Date();
// 模拟一些操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Date end = new Date();
long diffInMillis = end.getTime() - start.getTime();
long diffInSeconds = diffInMillis / 1000;
long diffInMinutes = diffInSeconds / 60;
System.out.println("时间差: " + diffInMillis + " 毫秒");
System.out.println("时间差: " + diffInSeconds + " 秒");
三、常见错误
误以为
Date
包含时区:Date
只是一个时间点。显示时的时区由格式化工具(如SimpleDateFormat
)或 JVM 默认时区决定。
使用已过时的方法:
- 如
getYear()
,setHours()
等。这些方法容易出错且不直观。
- 如
SimpleDateFormat
线程安全问题:SimpleDateFormat
不是线程安全的。在多线程环境下共享同一个实例会导致数据错误。- 错误示例:
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 共享实例 public String formatDate(Date date) { return sdf.format(date); // 多线程下可能出错 }
忽略异常处理:
SimpleDateFormat.parse()
抛出ParseException
,必须捕获。
月份从 0 开始的陷阱:
Calendar.MONTH
从 0 开始(0=1月),而Date
的过时方法getMonth()
也如此,容易导致逻辑错误。
四、注意事项
- 避免在新项目中使用
Date
进行复杂操作:优先使用java.time
包。 Date
对象是可变的:可以通过setTime()
修改其值,注意不要意外修改共享的Date
对象。- 序列化兼容性:
Date
实现了Serializable
,但注意反序列化时的时区问题。 toString()
的误导性:其输出依赖于 JVM 时区,不代表Date
内部存储了时区。
五、使用技巧
获取时间戳:
long timestamp = new Date().getTime(); // 或 System.currentTimeMillis()
快速格式化(单次使用):
// 对于一次性操作,可以直接创建 SimpleDateFormat String nowStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
使用
TimeZone
控制格式化时区:SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("GMT+8")); // 设置为北京时间 String gmt8Time = sdf.format(new Date());
六、最佳实践与性能优化
最佳实践
优先使用
java.time
:- 对于新代码,使用
LocalDateTime
,ZonedDateTime
,Instant
等。 - 例如:
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatted = now.format(formatter);
- 对于新代码,使用
避免共享
SimpleDateFormat
:- 方案一:每次使用时创建新实例(适合低频场景)。
- 方案二:使用
ThreadLocal
:private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public String formatDate(Date date) { return DATE_FORMATTER.get().format(date); }
- 方案三:使用
java.time.format.DateTimeFormatter
(推荐):private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // DateTimeFormatter 是线程安全的
明确时区处理:
- 在涉及跨时区的场景中,始终明确指定时区,避免依赖默认时区。
封装日期操作:
- 创建工具类封装常用的日期格式化、解析、计算逻辑。
性能优化
重用
DateTimeFormatter
(来自java.time
):DateTimeFormatter
是不可变且线程安全的,可以安全地声明为static final
常量。
避免频繁创建
SimpleDateFormat
:- 如果必须使用
SimpleDateFormat
,通过ThreadLocal
或池化技术减少对象创建开销。
- 如果必须使用
使用时间戳进行计算:
- 对于简单的加减、比较,直接操作
long
类型的时间戳(毫秒)比创建Date
或Calendar
对象更高效。
- 对于简单的加减、比较,直接操作
缓存频繁使用的日期:
- 如“今天零点”、“月初”等,如果频繁使用,可缓存计算结果(注意过期策略)。
总结
项目 | 说明 |
---|---|
核心 | Date 表示时间点(毫秒),不包含时区 |
推荐替代 | java.time 包(Java 8+) |
关键操作 | 创建、格式化、解析、比较、计算差值 |
最大陷阱 | SimpleDateFormat 线程不安全、过时方法、时区误解 |
最佳实践 | 使用 java.time 、避免共享 SimpleDateFormat 、明确时区 |