Java时间字符串合并当前日期: 2种方法 (含java.time)
2025-04-10 03:26:10
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
。强烈推荐后者,但咱们都讲讲。
方法一:使用 SimpleDateFormat
和 Calendar
(老办法)
这是 Java 8 之前的标准做法。虽然现在不推荐了,但了解一下没坏处,尤其是在维护老代码时。
原理:
- 用
SimpleDateFormat
来指定输入字符串的时间格式(比如"h:mm:ss a"
)。h
代表 1-12 小时制,mm
代表分钟,ss
代表秒,a
代表 AM/PM 标记。 - 用这个格式化器把时间字符串解析成一个临时的
Date
对象。注意,这个临时Date
对象的日期部分可能不是你想要的(通常是 1970 年),但没关系,我们只关心它的时、分、秒。 - 获取一个
Calendar
实例,这个实例默认会包含当前的日期和时间。 - 再创建一个
Calendar
实例,用上面解析出的临时Date
对象来设置它的时间。 - 从第二个
Calendar
实例中提取出小时、分钟、秒。 - 把提取出的时、分、秒设置到第一个
Calendar
实例(那个包含当前日期的实例)中,这样就把它一天中的时间点给“修正”了。 - 最后,从修改后的第一个
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
包),它更加健壮、易用且线程安全。这是目前处理日期时间问题的首选 方案。
原理:
- 用
DateTimeFormatter
来定义输入时间字符串的格式。同样,是"h:mm:ss a"
并且指定Locale.US
。 - 用这个
DateTimeFormatter
把时间字符串直接解析成一个LocalTime
对象。LocalTime
正好就只表示时间,不包含日期信息,非常契合我们的输入。 - 获取当前的日期信息,用
LocalDate.now()
得到一个LocalDate
对象。 - 把
LocalDate
(当前日期) 和LocalTime
(解析出的时间) 合并成一个LocalDateTime
对象。LocalDateTime
表示了一个不带时区信息的日期和时间。 - 由于最终需要的是
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或库交互时才进行转换。
进阶使用技巧:
DateTimeFormatter
比SimpleDateFormat
更强大,支持更复杂的模式和可选段落解析。- 如果你的时间字符串格式可能不唯一,可以使用
DateTimeFormatterBuilder
构建更灵活的解析器。 java.time
提供了丰富的日期时间计算方法(加减天数、小时、获取星期几等),比Calendar
更直观好用。
选哪个?
强烈推荐使用 java.time
(方法二)。
理由:
- 线程安全: 无需担心多线程问题。
- API 设计更好: 类职责清晰(
LocalDate
管日期,LocalTime
管时间,LocalDateTime
管日期时间),方法命名直观。 - 不可变性: 对象一旦创建就不会被修改,减少了程序出错的可能性。
- 功能更强大: 对时区、夏令时等的支持更完善。
只有当你被限制在 Java 8 之前的环境,或者需要与深度依赖 java.util.Date
且难以修改的旧代码库交互时,才考虑使用 SimpleDateFormat
和 Calendar
(方法一),并且务必注意它的线程安全问题。
通过上面两种方法,你都可以将只有时间信息的字符串 "4:16:06 PM"
结合当前日期,成功转换成所需的 java.util.Date
对象。