众所周知,如果想把 LocalDateTime 转为时间戳,需要先指定时区,然后才能转为时间戳,例如:
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
long second = zonedDateTime.toEpochSecond();
但是既然 LocalDateTime(本地时间)已经确定当前时间,为什么不能直接转为时间戳?
因为时间戳指的是自 1970 年 1 月 1 日(00:00:00 UTC/GMT)以来的秒数,所以无论在哪个时区,同一时间获取的都是相同时间戳,可以用于跨时区。但是我们现实生活用到的本地时间是跟时区挂钩的,中国所在的时区是东八区,会比 UTC 时间快 8 个小时。时间戳是从 UTC 时间得来的,所以时间戳与本地时间的相互转换,需要根据时区来转换。
通过查看 LocalDateTime.now() 方法源码,也能看出会先获取系统默认时区,然后再时间戳和时区获得本地时间。
public static LocalDateTime now() {
return now(Clock.systemDefaultZone()); //先获取系统默认时区
}
从 Clock.systemDefaultZone()
方法一直往下走,到达获取系统默认时区的重要方法。首先获取 JVM 参数中的时区信息,如果不存在则获取操作系统的时区信息,否则默认使用 GMT。
//java.util.TimeZone#setDefaultZone
private static synchronized TimeZone setDefaultZone() {
TimeZone tz;
// get the time zone ID from the system properties
Properties props = GetPropertyAction.privilegedGetProperties();
String zoneID = props.getProperty("user.timezone"); //通过JVM属性获取
// if the time zone ID is not set (yet), perform the
// platform to Java time zone ID mapping.
if (zoneID == null || zoneID.isEmpty()) {
String javaHome = StaticProperty.javaHome();
try {
zoneID = getSystemTimeZoneID(javaHome); //获取操作系统时区
if (zoneID == null) {
zoneID = GMT_ID; //默认使用GMT
}
} catch (NullPointerException e) {
zoneID = GMT_ID;
}
}
// Get the time zone for zoneID. But not fall back to
// "GMT" here.
tz = getTimeZone(zoneID, false); //从默认的时区配置和java根目录下的/lib/tzdb.dat中根据时区id获取时区信息。
if (tz == null) {
// If the given zone ID is unknown in Java, try to
// get the GMT-offset-based time zone ID,
// a.k.a. custom time zone ID (e.g., "GMT-08:00").
String gmtOffsetID = getSystemGMTOffsetID();
if (gmtOffsetID != null) {
zoneID = gmtOffsetID;
}
tz = getTimeZone(zoneID, true);
}
assert tz != null;
final String id = zoneID;
props.setProperty("user.timezone", id);
defaultTimeZone = tz;
return tz;
}
getSystemTimeZoneID()
是一个 native 方法,根据不同操作系统,获取的方法不同。对于 window 系统,获取的是注册表里的时区信息。
//java.util.TimeZone#getSystemTimeZoneID
private static native String getSystemTimeZoneID(String javaHome);
获取到系统默认时区后,通过获取 1970 年 1 月 1 日午夜至今的秒数和时区偏移量,计算出本地时间的秒数,借此创建 LocalDateTime 实例。
//java.time.LocalDateTime#now(java.time.Clock)
public static LocalDateTime now(Clock clock) {
Objects.requireNonNull(clock, "clock");
final Instant now = clock.instant(); // called once //获取1970 年 1 月 1 日午夜至今的秒数
ZoneOffset offset = clock.getZone().getRules().getOffset(now); //获取时区偏移量
return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
}
//java.time.LocalDateTime#ofEpochSecond
public static LocalDateTime ofEpochSecond(long epochSecond, int nanoOfSecond, ZoneOffset offset) {
Objects.requireNonNull(offset, "offset");
NANO_OF_SECOND.checkValidValue(nanoOfSecond);
long localSecond = epochSecond + offset.getTotalSeconds(); // overflow caught later //将UTC时间下的秒数和时区偏移的秒数相加,获得本地时间的秒数
long localEpochDay = Math.floorDiv(localSecond, SECONDS_PER_DAY); //将本地时间的秒数除以每天的秒数,得到天数
int secsOfDay = Math.floorMod(localSecond, SECONDS_PER_DAY); //将本地时间的秒数对每天的秒数取余,得到一天内剩下的秒数
LocalDate date = LocalDate.ofEpochDay(localEpochDay);
LocalTime time = LocalTime.ofNanoOfDay(secsOfDay * NANOS_PER_SECOND + nanoOfSecond);
return new LocalDateTime(date, time); //通过LocalDate和LocalTime创建LocalDateTime实例
}
既然本地时间是根据 1970 年 1 月 1 日午夜至今的秒数加上时区偏移的秒数得到的,那么从 LocalDateTime 转换为时间戳也自然需要减去时区偏移的秒数。
//java.time.chrono.ChronoZonedDateTime#toEpochSecond
default long toEpochSecond() {
long epochDay = toLocalDate().toEpochDay();
long secs = epochDay * 86400 + toLocalTime().toSecondOfDay(); //获取本地时间的秒数
secs -= getOffset().getTotalSeconds(); //减去时区偏移的秒数
return secs;
}