
Java得到当前时间的几种方法:Date、Calendar 和 java.time 怎么选
Java 获取“当前时间”看起来只是调用一个方法,实际要先弄清楚需要的是什么:代表时间线上的一个瞬间、某个时区里的本地日期时间,还是用于计算耗时的计时值。选错类型,常见后果包括时区偏差、日期格式不一致、夏令时转换异常,甚至把可预测的时间戳误当成安全凭证。
本文比较传统的 Date、Calendar 和 Java 8 引入的 java.time,并给出可以直接运行的示例。新代码通常优先使用 java.time;与旧 API 或旧接口交互时,再按需转换。
先分清三类“时间”
- 瞬间(Instant):时间线上的一个点,可用于记录事件发生时刻、传输时间戳。它本身不带时区。
- 本地日期或时间:例如“2025 年 3 月 8 日”或“上午 9 点”。它不一定能唯一对应到一个瞬间,必须结合时区等信息才能确定。
- 耗时:例如某段代码运行了多少毫秒。测量耗时应使用单调时钟,而不是日历时间。
很多“当前时间”问题,实质上是把这三类概念混在了一起。若要保存发生时刻,通常使用 Instant;若要展示给用户,通常先确定目标时区,再转换成当地时间;若要计算程序运行时长,则使用 System.nanoTime()。
一、Date:表示时间戳的传统类型
java.util.Date 表示一个时间点,内部以毫秒为单位表示自 Unix 纪元起经过的时间。它不保存时区,也不适合直接表达“某地的上午九点”。获取当前时间最常见的写法是:
import java.util.Date;
public class DateNowExample {
public static void main(String[] args) {
Date now = new Date();
System.out.println(now);
System.out.println("epochMillis = " + now.getTime());
}
}
new Date() 会取得当前时刻;getTime() 返回从 1970-01-01T00:00:00Z 起算的毫秒数。也可以直接获取毫秒时间戳:
long epochMillis = System.currentTimeMillis();
System.out.println(epochMillis);
Date date = new Date(epochMillis);
System.out.println(date);
System.currentTimeMillis() 和 new Date().getTime() 都表达墙上时钟时间,适合记录大致的当前时刻或与旧系统交换毫秒时间戳。它们的精度是毫秒表示精度,但系统时钟实际更新频率和准确度由运行环境决定,不能据此认为每次调用都会得到唯一值,也不能保证达到毫秒级准确性。
Date 的适用场景与限制
- 维护使用
Date的旧代码或旧接口。 - 与毫秒时间戳字段、部分旧版 JDBC API 进行兼容。
- 把它作为瞬间传递,而不是用它表达某个时区的日历规则。
Date 是可变对象。调用 setTime() 会改变对象状态;若把它放进共享数据结构或从对象中直接返回,调用者可能改变内部时间。需要防止这类副作用时,可返回副本,或改用不可变的 Instant。
此外,Date.toString() 会以默认时区生成便于阅读的文本,不适合作为稳定的数据交换格式。不同机器的默认时区可能不同,输出也会不同。不要把这种输出当成固定格式的时间戳。
不建议使用已经弃用的 Date 年、月、日构造器及相关访问方法。它们的年份偏移、月份编号等设计容易引发错误;需要日历字段计算时应使用 Calendar 或 java.time。
二、Calendar:旧式日历字段计算
Calendar 在 Java 早期用于处理日期字段、时区和日期运算。通过 getInstance() 获取当前时刻对应的日历对象:
import java.util.Calendar;
public class CalendarNowExample {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
System.out.println("year = "
+ calendar.get(Calendar.YEAR));
System.out.println("month = "
+ (calendar.get(Calendar.MONTH) + 1));
System.out.println("day = "
+ calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("hour = "
+ calendar.get(Calendar.HOUR_OF_DAY));
System.out.println("minute = "
+ calendar.get(Calendar.MINUTE));
System.out.println("second = "
+ calendar.get(Calendar.SECOND));
System.out.println("millisecond = "
+ calendar.get(Calendar.MILLISECOND));
}
}
Calendar.getInstance() 默认使用当前默认时区和默认区域设置。区域设置会影响部分日历行为;如果业务要求明确的时区,不要依赖机器默认值,而应显式设置时区。
import java.util.Calendar;
import java.util.TimeZone;
public class CalendarZoneExample {
public static void main(String[] args) {
Calendar utc = Calendar.getInstance(
TimeZone.getTimeZone("UTC"));
System.out.println(utc.getTime());
Calendar shanghai = Calendar.getInstance(
TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println(shanghai.getTime());
}
}
这里需要注意:Calendar 内部仍对应一个时间点,getTime() 返回的 Date 也不包含时区。为不同 Calendar 设置不同时区,改变的是读取和计算日历字段的方式,并不是创建了两个不同的“当前瞬间”。上例中两者创建时间略有先后,输出文本本身还会受默认时区影响。
Calendar 常见错误
- 月份从 0 开始。
Calendar.MONTH的 0 表示一月,11 表示十二月。向用户展示时通常要加 1;设置月份时也要遵守这个编号规则。 - 对象可变且非线程安全。
add()、set()会修改当前对象。不要在多个线程间无保护地共享可变的Calendar。 - 字段计算可能受到 lenient 规则影响。 默认情况下,某些超出范围的字段会被归一化。例如设置为不存在的日期,可能被滚动到下个月,而不是直接报错。可调用
setLenient(false)要求严格校验,但应通过getTime()等触发计算后捕获IllegalArgumentException。 - 加天数不等于加固定毫秒数。 在有夏令时规则的时区,一天可能不是固定的 24 小时。要表达“当地日历上的下一天”,按日历日运算;要表达经过了精确的 24 小时,则按持续时间运算。
例如,按日历天增加一天可以写成:
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
不应简单地用 24 * 60 * 60 * 1000 毫秒代替所有“加一天”的业务需求。
三、java.time:新代码的优先选择
java.time 从 Java 8 开始提供。它把瞬间、日期、时间、时区和格式化拆分成语义明确的类型;大多数类型不可变,格式化器也可安全地在多线程间复用。只需要当前瞬间时,使用 Instant.now():
import java.time.Instant;
public class InstantNowExample {
public static void main(String[] args) {
Instant now = Instant.now();
System.out.println(now);
System.out.println("epochMillis = " + now.toEpochMilli());
}
}
Instant 以 UTC 时间线表示瞬间,可以精确到纳秒的表示单位,但实际时钟能提供的精度和准确度取决于系统与运行环境。转换为毫秒时间戳时,小于毫秒的部分不会保留。
获取当前本地日期和时间
若业务只关心服务器当前默认时区中的本地日期或时间,可以使用:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class LocalNowExample {
public static void main(String[] args) {
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
System.out.println(date);
System.out.println(time);
System.out.println(dateTime);
}
}
LocalDateTime 不包含时区或偏移量,因此无法单独确定它对应哪个瞬间。它适合表达不绑定时区的日历值,例如一张尚未指定地点的表单日期时间;不适合单独存储服务器事件发生时刻。需要明确目标时区时,可以显式传入:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class ZonedNowExample {
public static void main(String[] args) {
ZoneId zone = ZoneId.of("Asia/Shanghai");
ZonedDateTime now = ZonedDateTime.now(zone);
System.out.println(now);
System.out.println("local = " + now.toLocalDateTime());
LocalDateTime local = LocalDateTime.now(zone);
System.out.println(local);
}
}
在服务端代码中,显式指定 ZoneId 能避免服务器部署环境变化后产生意外。常见时区 ID 如 Asia/Shanghai、Europe/Paris 和 UTC。应优先使用 IANA 时区 ID,而不是固定的 +08:00,因为地区时区规则可能随政策调整;固定偏移只表达相对 UTC 的偏移,不携带地区规则。
格式化和解析
显示时间时,应明确格式和时区。下面用 DateTimeFormatter 格式化指定时区的当前时刻:
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class FormatNowExample {
public static void main(String[] args) {
Instant now = Instant.now();
ZoneId zone = ZoneId.of("Asia/Shanghai");
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX")
.withZone(zone);
System.out.println(formatter.format(now));
}
}
这里的 XXX 会输出偏移量,有助于避免不同地区的同一组时分秒被误认为同一时刻。若格式只包含 yyyy-MM-dd HH:mm:ss,它仍不包含时区信息,跨系统传输时要另外约定时区。
解析外部输入时,应使用与输入格式匹配的类型和格式化器,并对非法输入进行校验。不要只靠字符串长度或正则表达式判断日期有效性。DateTimeFormatter 不可变且线程安全;旧的 SimpleDateFormat 则可变,通常不能在多个线程间共享同一个实例。
四、与旧 API 互操作
旧接口要求 Date 时,可以从 Instant 转换;反过来也可以:
import java.time.Instant;
import java.util.Date;
public class ConvertExample {
public static void main(String[] args) {
Instant now = Instant.now();
Date legacyDate = Date.from(now);
Instant restored = legacyDate.toInstant();
System.out.println(legacyDate);
System.out.println(restored);
}
}
Date.from(instant) 以毫秒精度承载时间,因此转换可能丢失纳秒以下的细节。转换前要确认目标 API 的精度满足需求。若旧代码必须使用 Calendar,可以通过 GregorianCalendar.from(zonedDateTime) 转换为日历对象;相反方向可使用 calendar.toInstant(),并用明确的 ZoneId 构造 ZonedDateTime。
五、夏令时、歧义时间与日期边界
时区不是简单的固定小时差。某些地区会因夏令时或政策变化调整偏移。对一个瞬间而言,给定时区后通常可以得到相应的本地时间;但反过来,把一个本地日期时间映射到瞬间时,可能遇到两类特殊情况:
- 不存在的本地时间:时钟向前调整时,某些本地时间会被跳过。
- 重复的本地时间:时钟向后调整时,同一个本地时间可能对应两个不同瞬间。
ZonedDateTime 会依据时区规则处理本地时间与时区的组合,但业务若要求拒绝歧义、选择较早或较晚的偏移,不能忽略这种选择。可以检查时区规则提供的有效偏移数量,针对不存在或重复的本地时间制定明确策略。预约、航班和交易截止时间等场景尤其需要明确业务规则,而不是把所有本地时间当成 UTC。
“当天开始”和“当天结束”也常被错误实现为加减固定毫秒数。若要查询某个地区某一天的记录,通常应构造该地区当天的起始边界与下一天的起始边界,使用半开区间 [开始时间, 下一天开始时间)。这样既能避免毫秒精度造成的“最后一刻”问题,也能适应一天长度不是固定 24 小时的时区规则。
六、测量耗时不要使用当前日期时间
System.currentTimeMillis() 表示墙上时钟,可能因系统校时、虚拟机环境或人工调整而前后跳动,不适合作为高可靠的耗时计时器。测量一段代码的持续时间,应使用 System.nanoTime():
long start = System.nanoTime();
doWork();
long elapsedNanos = System.nanoTime() - start;
long elapsedMillis = elapsedNanos / 1_000_000L;
System.out.println("耗时毫秒: " + elapsedMillis);
nanoTime() 是用于测量经过时间的单调计时源,不代表日期或时间,不能拿它转换成日历时间或保存为事件时间戳。它适合在同一进程中比较起止读数;不要假定其起点对应某个固定日期。
七、测试代码中的“当前时间”
直接调用 Instant.now() 会让测试依赖真实系统时钟,测试结果可能随执行时间变化。java.time.Clock 可以把取时行为作为依赖传入,生产环境使用系统时钟,测试环境使用固定时钟:
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
public class ClockExample {
static Instant currentInstant(Clock clock) {
return Instant.now(clock);
}
public static void main(String[] args) {
Clock productionClock = Clock.systemUTC();
System.out.println(currentInstant(productionClock));
Clock fixedClock = Clock.fixed(
Instant.parse("2025-01-02T03:04:05Z"),
ZoneId.of("UTC"));
System.out.println(currentInstant(fixedClock));
}
}
Clock.fixed() 对测试很有用,能稳定覆盖跨日、过期和边界条件。需要模拟时钟推进时,可以根据测试需要使用可控的时钟实现,而不是通过等待真实时间流逝来完成测试。
八、时间戳不是安全凭证
当前时间、毫秒时间戳或 Instant 都不是安全随机数。它们可能被预测、重复或受到系统时钟调整影响,不应单独用作密码、认证令牌、一次性验证码、不可伪造的订单编号或防重放机制。安全令牌应使用专门的密码学安全随机数生成器,并按协议设计有效期、签名或校验机制。
时间可以用于过期判断,但要清楚信任边界:客户端提交的时间戳可以被伪造;服务端时钟也可能有偏差。需要可信顺序或防重放时,应结合服务端生成的随机数、签名、唯一性约束或持久化状态等机制,不能只依靠“时间戳看起来足够精确”。
在比较时间时,也应选择符合业务语义的类型。比较两个事件发生时刻,用 Instant;比较某一天是否相同,先明确所处时区再转为 LocalDate;计算日历日差和计算经过时长不是一回事。时间类型表达清楚,通常比事后补充注释更可靠。
怎么选:快速决策
- 新代码记录事件发生时刻:用
Instant。 - 需要某个地区当前时间或日历规则:用
ZonedDateTime,并显式指定ZoneId。 - 只表达不绑定时区的日期、时间:用
LocalDate、LocalTime或LocalDateTime,但不要误把它们当作唯一时间戳。 - 计算运行耗时:用
System.nanoTime()。 - 维护旧接口:按接口要求使用
Date或Calendar,尽量在边界处转换成java.time类型。 - 生成展示文本:用明确格式的
DateTimeFormatter,并明确输出时区或偏移量。
总体上,Date 适合兼容旧系统,Calendar 能处理旧式日历字段但容易受可变性和编号规则影响;新开发优先选择语义清楚的 java.time。关键不在于找到一个“万能的当前时间方法”,而在于先确定需要的是瞬间、本地日历值还是耗时,再选择对应类型并明确时区、精度和边界规则。




