时区的用法看似简单,但是细节比较多,容易混淆,通过本篇笔记,便于记忆。
UTC和GMT
首先我们需要先了解一些名词概念,在下文中会有涉及。
格林尼治标准时间(GMT,旧译“格林威治平均时间”或“格林威治标准时间”)是指位于伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能和实际的太阳时相差16分钟。地球每天的自转是有些不规则的,而且正在缓慢减速。所以,格林尼治时间已经不再被作为标准时间使用。现在的标准时间——协调世界时(UTC)——由原子钟提供。
简单来说二者相同,都是标识0时区的时间,但是UTC更精准。
TimeZone
Java每个时间区域都有一个时间区域ID标识符。在J2SE 1.3 and 1.4中,这个ID是个字符串
,是由位于J2SE 安装程序的jre/lib
子目录中的tzmappings
文件这些ID列表。 J2SE 1.3 仅仅只包含tzmappings
文件,但是 J2SE 1.4包含世界不同地区的时间区域jre/lib/zi/
的数据文件。在J2SE 1.4里,sun.util.calendar.ZoneInfo
从这些文件获取DST
规则。J2SE 1.8同时有 jre/lib/tzdb.dat
和jre/lib/tzmappings
。
Time Zone Database
,简称tz
或tzinfo
,是一组表示地球上各地的时间历史的代码和数据,目前由IANA
维护。
IANA
会根据各地政体的变化而定期更新关于时区边界、UTC和夏令时
等的规则。对tz的更新遵循BCP 175流程进行管理。
下面我们来看一些TimeZone的常见api:
1.public static TimeZone getDefault()
这个方法用来获取系统当前的时区。
System.out.println(TimeZone.getDefault());
1)修改本地时间为北京时间
执行结果:
sun.util.calendar.ZoneInfo[id="Asia/Shanghai
",offset=28800000
,dstSavings=0,useDaylight=false,transitions=19,lastRule=null]
上面的输出底层是调用TimeZone 对象实例的toString方法打印的
- id :时区的唯一标识,可以通过标识生成TimeZone 对象
比如 TimeZone beiJing= TimeZone.getTimeZone(“Asia/Shanghai”); - offset:跟UTC时间的差值,单位ms。
本文中的值来自于相差8个时区,即28800000=8*3600*1000
2)修改本地时间为美国东部时间,不带夏令时
注意:修改时,取消
自动调整夏令时时钟
美国夏令时(3月11日至11月7日)
,执行代码日期:2020.4.8,此时美国已启用夏令时。
执行结果:
sun.util.calendar.ZoneInfo[id="GMT-05:00
",offset=-18000000
,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
3)修改本地时间为美国东部时间,带夏令时
注意:修改时,选中
自动调整夏令时时钟
美国夏令时(3月11日至11月7日)
,执行代码日期:2020.4.8,此时美国已启用夏令时。
sun.util.calendar.ZoneInfo[id="America/New_York
",offset=-18000000
,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/New_York,offset=-18000000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]
与不带夏令时相比,地区变为纽约时间了,不再是GMT-05:00;lastRule的值非null
;offset值相同,也就是说这个值和夏令时无关;
getDefault()源码分析
public static TimeZone getDefault() {
return (TimeZone) getDefaultRef().clone();
}
...
private static TimeZone getTimeZone(String ID, boolean fallback) {
//最终调用该方法
TimeZone tz = ZoneInfo.getTimeZone(ID);
if (tz == null) {
tz = parseCustomTimeZone(ID);
if (tz == null && fallback) {
tz = new ZoneInfo(GMT_ID, 0);
}
}
return tz;
}
java.util.TimeZone类中getDefault方法的源代码显示,它最终是会调用sun.util.calendar.ZoneInfo
类的getTimeZone
方法。这个方法为需要的时间区域返回一个作为ID的String参数。这个默认的时间区域ID获取顺序如下:
- user.timezone (system)属性。
- 如果user.timezone没有定义,它就会尝试从user.country和java.home (System)属性来得到ID。找到与ID mapping接近的那个。
- 如果它没有成功找到一个时间区域ID,它就会使用一个"fallback" 的GMT值。换句话说, 如果它没有计算出你的时间区域ID,它将使用GMT作为你默认的时间区域。
toString()源码分析
package sun.util.calendar;
public class ZoneInfo extends TimeZone {
public String toString() {
return getClass().getName() +
"[id=\"" + getID() + "\"" +
",offset=" + getLastRawOffset() +
",dstSavings=" + dstSavings +
",useDaylight=" + useDaylightTime() +
",transitions=" + ((transitions != null) ? transitions.length : 0) +
",lastRule=" + (lastRule == null ? getLastRuleInstance() : lastRule) +
"]";
}
//是ZoneInfo 私有方法
private int getLastRawOffset() {
return rawOffset + rawOffsetDiff;
}
//getRawOffset是TimeZone定义的抽象方法,此处具体实现
public int getRawOffset() {
if (!willGMTOffsetChange) {
return rawOffset + rawOffsetDiff;
}
int[] offsets = new int[2];
getOffsets(System.currentTimeMillis(), offsets, UTC_TIME);
return offsets[0];
}
通过toString()源码,发现,toString()
打印的offset
参数值来源私有方法getLastRawOffset()
,其结果基本等价于公有方法getRawOffset()
。
2.public abstract int getRawOffset();
获取 “时间偏移”。相对于“本初子午线
”的偏移,单位是ms,不包含夏令时
。
3.public static synchronized TimeZone getTimeZone(String ID)
该方法时通过ID来找到对应的TimeZone 对象。
TimeZone localZone = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone china = TimeZone.getTimeZone("GMT+08:00");
System.out.println(localZone);
System.out.println(china);
执行结果:
sun.util.calendar.ZoneInfo[id="Asia/Shanghai
",offset=28800000
,dstSavings=0,useDaylight=false,transitions=19,lastRule=null]
sun.util.calendar.ZoneInfo[id="GMT+08:00
",offset=28800000
,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
通过offset值相同可以判断,二者是同一个时区的不同名称,具体区别还不清楚。
4.查看可用的ID
String[] ids = TimeZone.getAvailableIDs();
for (String id:ids)
System.out.printf(id+", ");
SimpleDateFormat
在J2SE中,大多数日期和时间相关的类都包含时间区域信息,包括那些格式类,如java.text.DateFormat, 因此它们都会被JVM的默认时间区域所影响。然而,在你创建这些类的实例时,你能为它们确保正确的时间区域信息,使得你可以更容易来设置整个JVM的默认时间区域。并且一旦设置好,就可以确保所有的这些类都将使用同一个默认的时间区域。
1.默认使用当前系统时区
SimpleDateFormat 在使用时,如果不指定timezone,默认为当前系统时区
,可以查看源码的构造方法,一层层进去查看
public class SimpleDateFormat extends DateFormat
//通过构造函数,最终会调用该初始化方法
private void initializeCalendar(Locale loc) {
//TimeZone.getDefault()初始化一个calendar
calendar = Calendar.getInstance(TimeZone.getDefault(), loc);
2.parse方法
parse()方法可以把一个字符串日期转化为日期对象(java.util.Date对象实例自身是有时区的),如果不指定时区,则设置为当前系统时区。下面我们通过修改主机电脑的时区来验证。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getDefault());
Date localDate = sdf.parse("2020-4-3 11:30:00");
System.out.println(localDate.getTime());
System.out.println(localDate);
1.修改本地时间为北京时间
(UTC+08:00) 北京,重庆,香港特别行政区,乌鲁木齐
2.修改本地时间为美国东部时间,不带夏令时
3.修改本地时间为美国东部时间,带夏令时
汇总一下结果:
当前系统时区 | long (ms) | Date.toString() |
---|---|---|
北京时间 | 1585884600000 | Fri Apr 03 11:30:00 CST 2020 |
美国东部时间,不带夏令时 | 1585931400000 | Fri Apr 03 11:30:00 GMT-05:00 2020 |
美国东部时间,带夏令时 | 1585927800000 | Fri Apr 03 11:30:00 EDT 2020 |
通过结果可以看出
1.parse()方法的确是区分时区的,相同的字符串,时区不同解析出的long类型的值也不同。
2.java.uti.Date对象的getTime()
方法,也是包含时区
的。
3.Date.toString() 总是会把日期对象按当前系统时区打印,不建议在涉及时区的地方使用,会让人产生迷惑,我们下文有单独章节来解释。
3.format方法
format方法负责把一个日期(java.util.Date对象实例自身是有时区的)对象,按指定格式转化为一个字符串,如果不指定时区,则设置为系统当前时区
。
TimeZone localZone = TimeZone.getTimeZone("America/Los_Angeles");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
sdf.setTimeZone(localZone);
System.out.println(sdf.format(new Date()));
执行时,本地电脑是北京时区
,为2020.4.9 14:33
,构造了一个new Date()对象(北京时区),并转化为美国时区的时间,我们看下结果:
2020-04-08 23:30:36 PDT
这个是美国时间,比北京时间晚了13个小时。
java.uti.Date
Date.toString()总是按当前系统时区来显示
我们看个例子,SimpleDateFormat 设置了固定的时区为中国
TimeZone localZone = TimeZone.getTimeZone("Asia/Shanghai");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(localZone);
Date localDate = sdf.parse("2020-4-3 11:30:00");
System.out.println(localDate.getTime());
System.out.println(localDate);
1.修改本地时间为北京时间
(UTC+08:00) 北京,重庆,香港特别行政区,乌鲁木齐
2.修改本地时间为美国东部时间,不带时区
汇总一下结果:
当前系统时区 | long (ms) | Date.toString() |
---|---|---|
北京时间 | 1585884600000 | Fri Apr 03 11:30:00 CST 2020 |
美国东部时间,不带夏令时 | 1585884600000 | Thu Apr 02 22:30:00 GMT-05:00 2020 |
SimpleDateFormat 设置了固定的时区为中国,这样getTime()值总是相同的,但是toString不同,每次根据机器的当前时区打印。因此toString没啥意义,只能用在不关心时区的场景
。