Java 的 java.util.Date 类是 Java 早期用于表示特定时间点(精确到毫秒)的核心类。尽管在 Java 8 中引入了更现代、更强大的 java.time 包(如 LocalDateTime, ZonedDateTime, Instant 等),Date 类仍在许多旧系统和库中广泛使用。掌握 Date 类对于维护和理解现有代码至关重要。


一、核心概念

  1. 时间表示

    • Date 对象表示自 1970年1月1日 00:00:00 UTC(格林威治标准时间) 以来经过的毫秒数(称为 Unix 时间戳或纪元时间)。
    • 例如:new Date() 创建一个表示当前时间的 Date 对象。
  2. 内部存储

    • 实际上,Date 类内部使用一个 long 类型的字段 fastTime 来存储从纪元时间到当前时间的毫秒偏移量。
  3. 时区无关性(但有陷阱)

    • Date 对象本身只存储一个时间点(毫秒值),不包含时区信息
    • 但是,其 toString() 方法会使用 JVM 的默认时区来格式化输出,这常导致误解。
  4. 已过时的方法

    • 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:获取时间信息(推荐使用 Calendarjava.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 + " 秒");

三、常见错误

  1. 误以为 Date 包含时区

    • Date 只是一个时间点。显示时的时区由格式化工具(如 SimpleDateFormat)或 JVM 默认时区决定。
  2. 使用已过时的方法

    • getYear(), setHours() 等。这些方法容易出错且不直观。
  3. SimpleDateFormat 线程安全问题

    • SimpleDateFormat 不是线程安全的。在多线程环境下共享同一个实例会导致数据错误。
    • 错误示例
      private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 共享实例
      
      public String formatDate(Date date) {
          return sdf.format(date); // 多线程下可能出错
      }
      
  4. 忽略异常处理

    • SimpleDateFormat.parse() 抛出 ParseException,必须捕获。
  5. 月份从 0 开始的陷阱

    • Calendar.MONTH 从 0 开始(0=1月),而 Date 的过时方法 getMonth() 也如此,容易导致逻辑错误。

四、注意事项

  1. 避免在新项目中使用 Date 进行复杂操作:优先使用 java.time 包。
  2. Date 对象是可变的:可以通过 setTime() 修改其值,注意不要意外修改共享的 Date 对象。
  3. 序列化兼容性Date 实现了 Serializable,但注意反序列化时的时区问题。
  4. toString() 的误导性:其输出依赖于 JVM 时区,不代表 Date 内部存储了时区。

五、使用技巧

  1. 获取时间戳

    long timestamp = new Date().getTime(); // 或 System.currentTimeMillis()
    
  2. 快速格式化(单次使用):

    // 对于一次性操作,可以直接创建 SimpleDateFormat
    String nowStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
    
  3. 使用 TimeZone 控制格式化时区

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    sdf.setTimeZone(TimeZone.getTimeZone("GMT+8")); // 设置为北京时间
    String gmt8Time = sdf.format(new Date());
    

六、最佳实践与性能优化

最佳实践

  1. 优先使用 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);
      
  2. 避免共享 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 是线程安全的
      
  3. 明确时区处理

    • 在涉及跨时区的场景中,始终明确指定时区,避免依赖默认时区。
  4. 封装日期操作

    • 创建工具类封装常用的日期格式化、解析、计算逻辑。

性能优化

  1. 重用 DateTimeFormatter(来自 java.time):

    • DateTimeFormatter 是不可变且线程安全的,可以安全地声明为 static final 常量。
  2. 避免频繁创建 SimpleDateFormat

    • 如果必须使用 SimpleDateFormat,通过 ThreadLocal 或池化技术减少对象创建开销。
  3. 使用时间戳进行计算

    • 对于简单的加减、比较,直接操作 long 类型的时间戳(毫秒)比创建 DateCalendar 对象更高效。
  4. 缓存频繁使用的日期

    • 如“今天零点”、“月初”等,如果频繁使用,可缓存计算结果(注意过期策略)。

总结

项目 说明
核心 Date 表示时间点(毫秒),不包含时区
推荐替代 java.time 包(Java 8+)
关键操作 创建、格式化、解析、比较、计算差值
最大陷阱 SimpleDateFormat 线程不安全、过时方法、时区误解
最佳实践 使用 java.time、避免共享 SimpleDateFormat、明确时区