返回

Java时间字符串合并当前日期: 2种方法 (含java.time)

java

Java 里怎么把时间字符串转成带当前日期的 Date 对象?

手里拿到一个只有时间信息的字符串,比如 "4:16:06 PM",想把它变成一个 java.util.Date 对象,并且这个对象的日期部分需要是当前的年月日。

举个例子,如果今天是 2024年5月23日,拿到 "4:16:06 PM" 这个字符串,希望能得到一个 Date 对象,它代表的时间点是 2024-05-23 16:16:06

直接看代码可能会更清楚:

String dateString = "4:16:06 PM";
// 想要的结果 (假设今天是 2024-05-23):
// Date convertedDate 代表 2024-05-23 16:16:06.000

这事儿怎么搞定呢?

问题在哪?

这问题看着简单,但有个小细节需要注意。

关键在于,你的输入 ("4:16:06 PM") 只告诉了我们“几点几分”,具体来说是时、分、秒以及上下午标记 (PM)。它压根儿没提“哪年哪月哪日”。

但是,java.util.Date 这个类,它设计出来就是为了表示一个非常具体的时间点,一个自 epoch(1970年1月1日 00:00:00 GMT)以来经过的毫秒数。所以,一个 Date 对象必然包含完整的年、月、日、时、分、秒信息。

这就有点信息不对等了:输入信息不全(只有时间),但期望的输出要求信息完整(日期 + 时间)。

所以,核心任务就是:解析出输入字符串里的时间信息,然后把它跟当前的日期信息组合起来,最后生成一个完整的 Date 对象。

怎么搞定?

有两种主流的办法可以解决这个问题:一种是使用比较老的 java.text.SimpleDateFormat 配合 java.util.Calendar,另一种是使用 Java 8 引入的现代日期时间 API java.time。强烈推荐后者,但咱们都讲讲。

方法一:使用 SimpleDateFormatCalendar (老办法)

这是 Java 8 之前的标准做法。虽然现在不推荐了,但了解一下没坏处,尤其是在维护老代码时。

原理:

  1. SimpleDateFormat 来指定输入字符串的时间格式(比如 "h:mm:ss a")。h 代表 1-12 小时制,mm 代表分钟,ss 代表秒,a 代表 AM/PM 标记。
  2. 用这个格式化器把时间字符串解析成一个临时的 Date 对象。注意,这个临时 Date 对象的日期部分可能不是你想要的(通常是 1970 年),但没关系,我们只关心它的时、分、秒。
  3. 获取一个 Calendar 实例,这个实例默认会包含当前的日期和时间。
  4. 再创建一个 Calendar 实例,用上面解析出的临时 Date 对象来设置它的时间。
  5. 从第二个 Calendar 实例中提取出小时、分钟、秒。
  6. 把提取出的时、分、秒设置到第一个 Calendar 实例(那个包含当前日期的实例)中,这样就把它一天中的时间点给“修正”了。
  7. 最后,从修改后的第一个 Calendar 实例中获取最终的 Date 对象。

听起来步骤有点绕,看代码就清晰了。

操作步骤与代码示例:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

public class StringToDateLegacy {

    public static void main(String[] args) {
        String timeString = "4:16:06 PM";

        try {
            // 1. 定义时间字符串的格式
            // h: 1-12小时制
            // mm: 分钟
            // ss: 秒
            // a: AM/PM标记 (使用 Locale.US 确保能正确解析 'PM')
            SimpleDateFormat timeFormat = new SimpleDateFormat("h:mm:ss a", Locale.US);
            timeFormat.setLenient(false); // 建议关闭宽松解析,更严格

            // 2. 解析时间字符串,得到一个只包含时间信息的Date(日期部分通常是epoch 1970)
            Date parsedTime = timeFormat.parse(timeString);

            // 3. 获取一个包含当前日期和时间的Calendar实例
            Calendar nowCalendar = Calendar.getInstance(); // 包含了今天的年月日时分秒

            // 4. 创建另一个Calendar实例,用解析出来的时间设置它
            Calendar timeCalendar = Calendar.getInstance();
            timeCalendar.setTime(parsedTime);

            // 5. 从timeCalendar中提取出时、分、秒、毫秒
            int hourOfDay = timeCalendar.get(Calendar.HOUR_OF_DAY); // 使用 24 小时制的小时
            int minute = timeCalendar.get(Calendar.MINUTE);
            int second = timeCalendar.get(Calendar.SECOND);
            int millisecond = timeCalendar.get(Calendar.MILLISECOND);

            // 6. 把提取到的时、分、秒、毫秒设置到 nowCalendar 上
            // 只修改时间部分,日期部分保持不变 (即保持为当前日期)
            nowCalendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
            nowCalendar.set(Calendar.MINUTE, minute);
            nowCalendar.set(Calendar.SECOND, second);
            nowCalendar.set(Calendar.MILLISECOND, millisecond); // 如果需要毫秒精度,也设置

            // 7. 从修改后的 nowCalendar 获取最终的 Date 对象
            Date finalDate = nowCalendar.getTime();

            System.out.println("原始字符串: " + timeString);
            System.out.println("转换后的 Date 对象: " + finalDate);

            // 也可以再格式化输出,验证一下
            SimpleDateFormat finalFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            System.out.println("格式化输出: " + finalFormat.format(finalDate));

        } catch (ParseException e) {
            System.err.println("解析时间字符串失败: " + timeString + ",格式不匹配或无效。");
            e.printStackTrace();
        }
    }
}

额外建议:

  • 线程安全: SimpleDateFormat非线程安全 的。如果在多线程环境中使用同一个 SimpleDateFormat 实例进行解析或格式化,可能会导致结果错乱或抛出异常。每次使用时都创建新的实例,或者使用 ThreadLocal 包装,或者干脆用 java.time
  • Locale 问题: AM/PM 标记在不同语言环境下可能不同(比如中文是“上午”/“下午”)。代码里使用了 Locale.US 来确保能正确解析英文的 "PM"。如果你的输入字符串可能是其他语言,需要相应地调整 Locale
  • 解析异常: parse() 方法会抛出 ParseException (checked exception),必须捕获处理。如果输入字符串的格式跟指定的模式不符,就会抛这个异常。setLenient(false) 让解析更严格,推荐加上。

进阶使用技巧:

  • 如果你需要处理带时区的时间字符串,SimpleDateFormat 也可以配合 TimeZone 类使用,但这会增加复杂性。

方法二:使用 java.time (现代推荐方法)

从 Java 8 开始,引入了全新的日期时间 API (java.time 包),它更加健壮、易用且线程安全。这是目前处理日期时间问题的首选 方案。

原理:

  1. DateTimeFormatter 来定义输入时间字符串的格式。同样,是 "h:mm:ss a" 并且指定 Locale.US
  2. 用这个 DateTimeFormatter 把时间字符串直接解析成一个 LocalTime 对象。LocalTime 正好就只表示时间,不包含日期信息,非常契合我们的输入。
  3. 获取当前的日期信息,用 LocalDate.now() 得到一个 LocalDate 对象。
  4. LocalDate (当前日期) 和 LocalTime (解析出的时间) 合并成一个 LocalDateTime 对象。LocalDateTime 表示了一个不带时区信息的日期和时间。
  5. 由于最终需要的是 java.util.Date 对象(可能是为了兼容老系统或库),需要将 LocalDateTime 转换为 Date。这一步通常需要考虑时区:
    • 先把 LocalDateTime 结合系统默认时区(或者一个你指定的时区)转换成 ZonedDateTime
    • 然后从 ZonedDateTime 获取一个 Instant (时间线上的精确时刻)。
    • 最后通过 Date.from(Instant) 方法得到 java.util.Date 对象。

操作步骤与代码示例:

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.Locale;

public class StringToDateTime8 {

    public static void main(String[] args) {
        String timeString = "4:16:06 PM";

        try {
            // 1. 定义时间字符串的格式化器
            // 注意模式字母和 SimpleDateFormat 略有不同,但 h, mm, ss, a 含义相似
            // 同样使用 Locale.US 确保 'PM' 被正确解析
            DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("h:mm:ss a", Locale.US);

            // 2. 直接将时间字符串解析为 LocalTime 对象
            LocalTime parsedTime = LocalTime.parse(timeString, timeFormatter);

            // 3. 获取当前的日期 LocalDate 对象
            LocalDate currentDate = LocalDate.now();

            // 4. 合并当前日期和解析出的时间,得到 LocalDateTime
            LocalDateTime currentDateTime = currentDate.atTime(parsedTime);
            // 或者: LocalDateTime currentDateTime = parsedTime.atDate(currentDate);

            // 5. 如果需要转成 java.util.Date (考虑时区)
            //    a. 结合系统默认时区转为 ZonedDateTime
            ZoneId systemZone = ZoneId.systemDefault();
            ZonedDateTime zonedDateTime = currentDateTime.atZone(systemZone);

            //    b. 从 ZonedDateTime 获取 Instant
            Instant instant = zonedDateTime.toInstant();

            //    c. 从 Instant 得到 java.util.Date
            Date finalDate = Date.from(instant);

            System.out.println("原始字符串: " + timeString);
            System.out.println("解析出的 LocalTime: " + parsedTime);
            System.out.println("当前日期 LocalDate: " + currentDate);
            System.out.println("合并后的 LocalDateTime: " + currentDateTime);
            System.out.println("系统默认时区: " + systemZone);
            System.out.println("最终转换的 Date 对象: " + finalDate);

            // 使用 DateTimeFormatter 格式化输出,更灵活
            DateTimeFormatter outputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
            System.out.println("格式化输出 (使用 java.time): " + zonedDateTime.format(outputFormatter));

        } catch (DateTimeParseException e) {
            System.err.println("解析时间字符串失败: " + timeString + ",格式不匹配或无效。");
            e.printStackTrace();
            // 可以打印 e.getParsedString() 和 e.getErrorIndex() 帮助定位错误
        } catch (Exception e) { // 其他潜在异常,如 ZoneRulesException
            System.err.println("发生其他错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

额外建议:

  • 线程安全: java.time 包里的所有核心类(如 LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, DateTimeFormatter)都是不可变且线程安全 的。你可以放心地在多线程环境中共享 DateTimeFormatter 实例。
  • 时区处理:LocalDateTime 转换回 java.util.Date 时,时区是关键。代码中用了 ZoneId.systemDefault(),即操作系统的默认时区。这在大多数情况下可行,但如果你的应用需要处理特定时区,应该显式指定 ZoneId,例如 ZoneId.of("America/New_York")。不注意时区可能导致时间偏差。java.util.Date 本身不存储时区信息,它内部是 UTC 毫秒数,但在 toString() 时会使用 JVM 的默认时区。
  • 异常处理: LocalTime.parse() 抛出的是 DateTimeParseException (runtime exception)。虽然不用强制捕获,但实际项目中最好还是捕获它,以应对可能的格式错误。
  • 尽量使用 java.time 类型: 如果你的项目整体可以迁移到使用 java.time API,那最好就一直使用 LocalDate, LocalTime, LocalDateTime, ZonedDateTime 等类型,避免不必要的 java.util.Date 转换。只在需要与旧API或库交互时才进行转换。

进阶使用技巧:

  • DateTimeFormatterSimpleDateFormat 更强大,支持更复杂的模式和可选段落解析。
  • 如果你的时间字符串格式可能不唯一,可以使用 DateTimeFormatterBuilder 构建更灵活的解析器。
  • java.time 提供了丰富的日期时间计算方法(加减天数、小时、获取星期几等),比 Calendar 更直观好用。

选哪个?

强烈推荐使用 java.time (方法二)。

理由:

  • 线程安全: 无需担心多线程问题。
  • API 设计更好: 类职责清晰(LocalDate管日期,LocalTime管时间,LocalDateTime管日期时间),方法命名直观。
  • 不可变性: 对象一旦创建就不会被修改,减少了程序出错的可能性。
  • 功能更强大: 对时区、夏令时等的支持更完善。

只有当你被限制在 Java 8 之前的环境,或者需要与深度依赖 java.util.Date 且难以修改的旧代码库交互时,才考虑使用 SimpleDateFormatCalendar (方法一),并且务必注意它的线程安全问题。

通过上面两种方法,你都可以将只有时间信息的字符串 "4:16:06 PM" 结合当前日期,成功转换成所需的 java.util.Date 对象。