Java得到当前时间的几种方法:Date、Calendar 和 java.time 怎么选

星座4天前发布 jdkjadmin
0 00
Java得到当前时间的几种方法:Date、Calendar 和 java.time 怎么选

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。关键不在于找到一个“万能的当前时间方法”,而在于先确定需要的是瞬间、本地日历值还是耗时,再选择对应类型并明确时区、精度和边界规则。

© 版权声明

相关文章