彻底弄懂GMT、UTC、CST、时区和夏令时
格林威治时间、世界时、祖鲁时间、GMT、UTC、时区、夏令时,这些眼花缭乱的时间术语,我们可能都不陌生,但是真正遇到问题,可能又不那么确定,不得不再去查一查,处理完可能过段时间又忘记。今天,我们彻底来梳理一下它们。
GMT
GMT: Greenwich Mean Time, 即格林威治平时(也称格林威治时间),它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。为什么是英国,因为近代的世界秩序是由英国发起的。
1884年10月在美国华盛顿召开了一个国际子午线会议,该会议将格林威治子午线设定为本初子午线,并将格林威治平时 (GMT, Greenwich Mean Time) 作为世界时间标准(UT, Universal Time)。由此也确定了全球24小时自然时区的划分,所有时区都以和 GMT 之间的偏移量做为参考(下文时区时还会详解)。如中国所在的东八区(中国东西比较长,实际横跨了5个时区,从东五 -> 东九,国家为了方便统一使用北京所在的东八区,但不同地方的人实际感受是不一样的,如新疆属于东六区,当东八区晚上9点时,那里实际才7点,夏天时天还大亮着)。
1972年之前,格林威治时间(GMT)一直是世界时间的标准。1972年之后,GMT 不再是一个时间标准了。原因是地球运转轨道速度不均匀,且地球本身自转也在减速,时间精准度上差了一些
UTC
UTC(Coodinated Universal Time), 协调世界时,又称世界统一时间、世界标准时间、国际协调时间。UTC 时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成。在军事中,协调世界时会使用“Z”来表示。又由于Z在无线电联络中使用“Zulu”作代称,协调世界时也会被称为"Zulu time", 即祖鲁时间。
UTC 由两部分构成:
原子时间(TAI, International Atomic Time):
结合了全球400个所有的原子钟而得到的时间,它决定了我们每个人的钟表中,时间流动的速度。
世界时间(UT, Universal Time):
也称天文时间,或太阳时,他的依据是地球的自转,我们用它来确定多少原子时,对应于一个地球日的时间长度。
UTC 比 GMT更精准,以原子时计时,适应现代社会的精确计时。
CST
CST: China Standard Time, 即北京时间,相当于 UTC+8,也可表示Central Standard Time, 即美国中部时间(UTC-6)
时区
- 为什么需要时区
随着火车铁路与其他交通和通讯工具的发展,以及全球化贸易的推动,各地使用各自的当地太阳时间带来了时间不统一的问题,在19世纪催生了统一时间标准的需求,时区由此诞生。
时区如何定义
从格林威治本初子午线起,经度每向东或者向西间隔15°,就划分一个时区,在这个区域内,大家使用同样的标准时间。全球共分为24个标准时区,相邻时区的时间相差一个小时。
所以北京时间可以表示为:UTC+8。
夏令时
它是为节约能源而人为规定地方时间的制度。一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。
目前全世界是欧美国家采用夏令时。 如欧盟规定:每年三月的最后一个星期日的凌晨1点开始时钟调快一小时,开始夏令时,到本年的十月最后一个星期日的凌晨一点开始又调回去,结束夏令时。 美国:每年3月的第二个周日凌晨2AM(当地时间)开始,将时钟调到3点,拨快1小时,俗称“Spring Forward 1 Hour”;而在11月的第一个周日凌晨2AM(当地时间)夏令时结束,要将时钟调到1点,拨慢1小时,俗称“Fall Back 1 Hour”。
本意是节约能源,现实比较鸡肋,欧盟已经开始倡导取消夏令时。
javascript 中的Date
- new Date()
得到本地时间,在不同时区打印 new Date() ,输出的结果将会不一样
new Date()
output:
Thu Jan 20 2022 15:24:49 GMT+0800 (中国标准时间)
-
new Date().getTime()
得到本地时间距 1970年1月1日午夜(GMT时间)之间的毫秒数 -
new Date().getTimezoneOffset()
返回本地时间与 GMT 时间之间的时间差,以分钟为单位, 是一个固定值:时区 * 60
new Date().getTimezoneOffset()
output:
-480
为什么是负值,因为是东八区,本身就比标准时间快8小时,当前毫秒 + 偏移量即为标准时间。
- 知道某地时间和所属时区,能快速计算任何一个地方的当前时间
let absTime = new Date().getTime() + new Date().getTimezoneOffset() * 60 * 1000;
var japanTime = new Date(absTime + 9 * 60 * 60 * 1000);
console.log(japanTime);
output:
Thu Jan 20 2022 16:46:05 GMT+0800 (中国标准时间)
比北京时间+1小时
java中已有java.util.Date类,为什么需要新的API
这个类目前大多数方法都被标记为@Deprecated。 它承担了太多功能,既支持类型转换,也支持时区,也支持日期增减,比较等。 而且初始化逻辑也比较奇怪,比如月份从0开始。
在JDK1.1中引入了Calendar,日期的增减,时区转换等靠Calendar。 然后日期的格式化,转换依靠SimpleDateFormat实现。
因为诸多缺陷和不便使得开发者转而使用流行的第三方库:Joda-Time。 到JDK8,引入了java.time包,用来增强日期处理,很多特性借鉴的是Joda-Time。
LocalDate & LocalTime
LocalDate的实例表示一个不可变对象,它只提供简单的日期,不包含时间信息,也不附带任何与时区。
/**
* LocalDate示例
*/
public class LocalDateSample {
public static void main(String[] args) {
LocalDate localDate = LocalDate.of(2022, 1, 20);
System.out.println("localDate: " + localDate);
// 年
int year = localDate.getYear();
System.out.println("year: " + year);
// 月
Month month = localDate.getMonth();
System.out.println("month: " + month);
// 日
int day = localDate.getDayOfMonth();
System.out.println("day: " + day);
// 周
DayOfWeek week = localDate.getDayOfWeek();
System.out.println("week: " + week);
// 当前月份总天数
int length = localDate.lengthOfMonth();
System.out.println("total days in this months: " + length);
// 是否是闰年
boolean leap = localDate.isLeapYear();
System.out.println("is leap year: " + leap);
localDate = LocalDate.now();
System.out.println("now: " + localDate);
}
}
// output:
localDate: 2022-01-20
year: 2022
month: JANUARY
day: 20
week: THURSDAY
total days in this months: 31
is leap year: false
now: 2022-01-20
亦可通过枚举类实例获取指定的属性:
/**
* 演示通过ChronoField枚举获取日期属性
*/
public class LocalDateTemporalFieldSample {
public static void main(String[] args) {
LocalDate now = LocalDate.now();
int year = now.get(ChronoField.YEAR);
System.out.println("year: " + year);
int month = now.get(ChronoField.MONTH_OF_YEAR);
System.out.println("month: " + month);
int day = now.get(ChronoField.DAY_OF_MONTH);
System.out.println("day: " + day);
int week = now.get(ChronoField.DAY_OF_WEEK);
System.out.println("week: " + week);
}
}
// output:
year: 2022
month: 1
day: 20
week: 4
LocalTime跟LocalDate类似,只不过它关注的是时间,示例:
/**
* LocalTime示例
*/
public class LocalTimeSample {
public static void main(String[] args) {
LocalTime now = LocalTime.now();
int hour = now.getHour();
System.out.println("hour: " + hour);
int minute = now.getMinute();
System.out.println("minute: " + minute);
int second = now.getSecond();
System.out.println("second: " + second);
hour = now.get(ChronoField.HOUR_OF_DAY);
System.out.println("hour: " + hour);
minute = now.get(ChronoField.MINUTE_OF_HOUR);
System.out.println("minute: " + minute);
second = now.get(ChronoField.SECOND_OF_MINUTE);
System.out.println("second: " + second);
}
}
// output:
hour: 17
minute: 24
second: 2
hour: 17
minute: 24
second: 2
LocalDate & LocalTime都支持通过字符串转换为对应的类型
LocalDate localDate = LocalDate.parse("2022-01-21");
LocalTime localTime = LocalTime.parse("09:25:00");
System.out.println("localDate: " + localDate);
System.out.println("localTime: " + localTime);
// output:
localDate: 2022-01-21
localTime: 09:25
LocalDateTime
是LocalDate和LocalTime的合体,也不带时区,可以直接创建,也可以通过合并LocalDate和LocalTime对象创建,示例:
/**
* 演示LocalDateTime各种实例化
*/
public class LocalDateTimeSample {
public static void main(String[] args) {
// now
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
// 指定年月日时分秒初始化
localDateTime = LocalDateTime.of(2021, 1, 21, 10, 3, 10);
System.out.println(localDateTime);
// 以现有的LocalDate和LocalTime实例化
localDateTime = LocalDateTime.of(LocalDate.now(), LocalTime.now());
System.out.println(localDateTime);
// LocalDate附加时间实例化LocalDateTime
localDateTime = LocalDate.now().atTime(LocalTime.now());
localDateTime = LocalDate.now().atTime(10, 3, 10);
System.out.println(localDateTime);
// LocalTime附加日期实例化LocalDateTime
localDateTime = LocalTime.now().atDate(LocalDate.now());
System.out.println(localDateTime);
// 从LocalDateTime取出LocalDate
LocalDate localDate = localDateTime.toLocalDate();
System.out.println(localDate);
// 从LocalDateTime取出LocalTime
LocalTime localTime = localDateTime.toLocalTime();
System.out.println(localTime);
}
}
机器的日期和时间格式
对于人而言,表示日期时间采用年月日时分秒是非常自然的,对于计算机建模时间最自然的格式是表示一个持续时间段上某个点的值,这也是java.time.Instant类提供的方式,以UNIX元年时间(1970年1月1日0时0分0秒)开始所经历的秒数计算。
注: 1s = 1_000 mills = 1_000_000 micro seconds = 1_000_000_000 nano seconds
/**
* 时刻示例
*
* 计算机内部用时刻来表示一个时间,以1970年1月1日0时0分0秒至今的偏移量计算
*
*/
public class InstantSample {
public static void main(String[] args) {
Instant instant = Instant.now();
// 自1970年至今经历的秒数
System.out.println("getEpochSecond: " + instant.getEpochSecond());
// 同上
System.out.println("ChronoField.INSTANT_SECONDS: " + instant.getLong(ChronoField.INSTANT_SECONDS));
// 毫秒部分
System.out.println("ChronoField.MILLI_OF_SECOND: " + instant.getLong(ChronoField.MILLI_OF_SECOND));
// 微秒单位
System.out.println("ChronoField.MICRO_OF_SECOND: " + instant.getLong(ChronoField.MICRO_OF_SECOND));
// 纳秒单位
System.out.println("ChronoField.NANO_OF_SECOND: " + instant.getLong(ChronoField.NANO_OF_SECOND));
// 以固定的秒实例化一个时刻
instant = Instant.ofEpochSecond(1L);
System.out.println("ofEpochSecond(1L): " + instant.getEpochSecond());
// 以固定的秒和纳秒实例化一个时刻
instant = Instant.ofEpochSecond(1L, 1_000_000_000L);
System.out.println("ofEpochSecond(1L, 1_000_000_000L): " + instant.getEpochSecond());
// 以固定的秒和纳秒实例化一个时刻(纳秒也可以是负数)
instant = Instant.ofEpochSecond(3L, -1_000_000_000L);
System.out.println("ofEpochSecond(3L, -1_000_000_000L)" + instant.getEpochSecond());
}
}
// result:
getEpochSecond: 1642735463
ChronoField.INSTANT_SECONDS: 1642735463
ChronoField.MILLI_OF_SECOND: 593
ChronoField.MICRO_OF_SECOND: 593597
ChronoField.NANO_OF_SECOND: 593597800
ofEpochSecond(1L): 1
ofEpochSecond(1L, 1_000_000_000L): 2
ofEpochSecond(3L, -1_000_000_000L):2
Duration & Period
Duration , Period都有时间区间,时间段的意思。两者的区别是Duration粒度小,通常用于表示小于天的时间段(如10s, 1min, 1nanos etc)。 而Period表示的粒度大,如1days, 1weeks, 1months, 1year 等。
/**
* 演示Duration
*
* Duration有时间段的含义,它用于以秒和纳秒衡量时间的长短,所以不能仅传递LocalDate
*
* 它的粒度比Period更细
*
*/
public class DurationSample {
public static void main(String[] args) {
// 通过两个LocalTime实例化Duration
Duration d1 = Duration.between(LocalTime.of(10, 0), LocalTime.of(10, 30));
System.out.println("d1: " + d1);
// 通过两个LocalDateTime实例化Duration
Duration d2 = Duration.between(LocalDateTime.of(2022, 1, 21, 0, 0, 0), LocalDateTime.of(2022, 1, 22, 0, 0, 1));
System.out.println("d2: " + d2);
// 通过两个Instant实例化Duration
Duration d3 = Duration.between(Instant.ofEpochSecond(1L), Instant.ofEpochSecond(1L, 1_000_000_000));
System.out.println("d3: " + d3);
// 通过指定的天数实例化Duration
Duration duration = Duration.ofDays(1);
System.out.println("ofDays: " + duration);
duration = Duration.ofHours(1);
System.out.println("ofHours: " + duration);
duration = Duration.ofMinutes(1);
System.out.println("ofMinutes: " + duration);
duration = Duration.ofSeconds(1);
System.out.println("ofSeconds: " + duration);
duration = Duration.ofMillis(1000);
System.out.println("ofMillis: " + duration);
duration = Duration.ofNanos(1_000_000_000);
System.out.println("ofNanos: " + duration);
duration = Duration.of(1, ChronoUnit.MILLIS);
System.out.println("of with TemporalUnit: " + duration);
}
}
// output:
d1: PT30M
d2: PT24H1S
d3: PT1S
ofDays: PT24H
ofHours: PT1H
ofMinutes: PT1M
ofSeconds: PT1S
ofMillis: PT1S
ofNanos: PT1S
of with TemporalUnit: PT0.001S
/**
* 演示Period示例
* period也是时间段的意思,不过相比Duration表示的时间段粒度更粗,以年月日为单位
*/
public class PeriodSample {
public static void main(String[] args) {
// 通过两个LocalDate示例话period
Period period = Period.between(LocalDate.of(2022, 1, 20), LocalDate.of(2022, 1, 21));
System.out.println(period);
// 通过传入年月日初始化
period = Period.of(1, 1, 1);
System.out.println(period);
// 通过天初始化
period = Period.ofDays(1);
System.out.println(period);
// 通过月初始化
period = Period.ofMonths(1);
System.out.println(period);
// 通过周初始化
period = Period.ofWeeks(1);
System.out.println(period);
// 通过年初始化
period = Period.ofYears(1);
System.out.println(period);
}
}
// output:
P1D
P1Y1M1D
P1D
P1M
P7D
P1Y
操纵,解析,格式化日期
withAttribute
withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。
/**
* 演示LocalDate withAttribute重新实例化LocalDate
*/
public class WithAttributeSample {
public static void main(String[] args) {
LocalDate localDate = LocalDate.of(2021, 1, 21);
System.out.println("localDate: " + localDate);
// 调整年份
localDate = localDate.withYear(2022);
System.out.println("localDate with year: " + localDate);
// 调整月份
localDate = localDate.withMonth(2);
System.out.println("localDate with Month: " + localDate);
// 调整天数
localDate = localDate.withDayOfMonth(22);
System.out.println("localDate with Day: " + localDate);
// 调整星期
localDate = localDate.with(ChronoField.DAY_OF_WEEK, 1);
System.out.println("localDate with ChronoField.DAY_OF_WEEK: " + localDate);
// 调整天数
localDate = localDate.plusDays(1);
System.out.println("localDate.plusDays: " + localDate);
// 调整天数
localDate = localDate.minus(1, ChronoUnit.DAYS);
System.out.println("localDate.minus: " + localDate);
}
}
// output:
localDate: 2021-01-21
localDate with year: 2022-01-21
localDate with Month: 2022-02-21
localDate with Day: 2022-02-22
localDate with ChronoField.DAY_OF_WEEK: 2022-02-21
localDate.plusDays: 2022-02-22
localDate.minus: 2022-02-21
TemporalAdjuster
上面的with, plus, minus等都是比较简单的日期、时间操作,如果想控制的更复杂,如将日期调整到下一个周日, 下个工作日,或者本月的最后一天,此时可以使用TemporalAdjuster,示例:
import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2014, 3, 18);
// 下一个周日(当天就是周日直接返回当天)
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
// 当月的最后一天
LocalDate date3 = date2.with(lastDayOfMonth());
自定义TemporalAdjuster
当JDK默认的TemporalAdjuster不满足需求时,也可以自己定制一个实现,以下为获取下一个工作日的实现:
逻辑:当前是周日~周四,则+1天
当前是周五,则 + 3天
当前是周六,则 + 2天
public class NextWorkDaySample {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime workDateTime = now.with(new NextWorkDay());
System.out.println(workDateTime);
}
}
/**
* 自定义的TemporalAdjuster实现
* 自动找到下一个工作日
*/
class NextWorkDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int addDays = 1;
if (dayOfWeek == DayOfWeek.FRIDAY) {
addDays = 3;
} else if (dayOfWeek == DayOfWeek.SATURDAY) {
addDays = 2;
}
return temporal.plus(addDays, ChronoUnit.DAYS);
}
}
// output:
2022-01-24T16:03:37.633422600
解析和格式化日期时间对象
解析(从字符串-> 日期时间)以及格式化(日期时间 -> 指定格式的字符串)是非常重要的功能, 最重要的类是DateTimeFormatter。
/**
* DateTimeFormatter常见示例
*/
@Slf4j
public class DateTimeFormatterSample {
public static void main(String[] args) {
// 格式化
LocalDate now = LocalDate.now();
String s = now.format(DateTimeFormatter.BASIC_ISO_DATE);
log.info("==> BASIC_ISO_DATE: {}", s);
s = now.format(DateTimeFormatter.ISO_LOCAL_DATE);
log.info("==> ISO_LOCAL_DATE: {}", s);
LocalTime time = LocalTime.now();
s = time.format(DateTimeFormatter.ISO_TIME);
log.info("==> ISO_TIME: {}", s);
s = time.format(DateTimeFormatter.ISO_LOCAL_TIME);
log.info("==> ISO_LOCAL_TIME: {}", s);
// 自定义DateTimeFormatter
LocalDateTime localDateTime = LocalDateTime.parse("2022-01-24 14:33:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info("parse customer: {}", localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
处理不同的时区
时区的概念在本文开头的时候已经详细解释过了, 在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识,比如:
ZoneId romeZone = ZoneId.of("Europe/Rome");
/**
* 演示时区的时间换算
*/
public class ZoneSample {
public static void main(String[] args) {
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
System.out.println(zoneIds);
// Asia/Aden, America/Cuiaba, ...
// 时区ID是{区域}/{城市}的格式,中国东八区的Asia/Shanghai,Asia/Chungking
System.out.println("systemDefaultZone: " + ZoneId.systemDefault());
// 可有时区ID初始化一个时区
ZoneId utc8 = ZoneId.of("Asia/Shanghai");
// 给某个具体的时间点附加时区
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = localDateTime.atZone(utc8);
System.out.println("cst: " + zonedDateTime);
// 时区时间转换
// 当前是北京时间(UTC+8),转换为美国东部时间(UTC-5)
ZonedDateTime cst = ZonedDateTime.now(ZoneId.systemDefault());
cst = cst.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("est: " + cst);
}
}
// output:
[Asia/Aden, America/Cuiaba ...
systemDefaultZone: Asia/Shanghai
cst: 2022-01-24T15:14:22.159473200+08:00[Asia/Shanghai]
est: 2022-01-24T02:14:22.160472100-05:00[America/New_York]
跟传统java.uti.Date之间的转换
虽然LocalDate, LocalTime, Instant等非常好用,但很多场合还是需要跟Date类型互转,示例:
/**
* LocalDateTime, Instant, java.util.Date互相转换
*/
public class TransferSample {
public static void main(String[] args) {
// localDateTime -> Instant
LocalDateTime localDateTime = LocalDateTime.now();
Instant instant = localDateTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant();
System.out.println(instant.getEpochSecond());
// localDateTime -> java.util.Date
Date date = Date.from(instant);
System.out.println(date);
// Date -> Instant
instant = date.toInstant();
System.out.println(instant);
// Date -> LocalDateTime
localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("Asia/Shanghai"));
System.out.println(localDateTime);
}
}
// output:
1643010530
Mon Jan 24 15:48:50 CST 2022
2022-01-24T07:48:50.341Z
2022-01-24T15:48:50.341
小结
- 新的时间API分离了日期,时间,日期时间,时区,和以毫秒表示的时间
- TemporalAdjuster可满足自定义的日期转换器
- 通过DateTimeFormatter完成时间的转换和格式化
- 使用ZoneId表示具体的时区,以及不同时区之间时间的转换