【日期、时区、时间】Java时区深度剖析

时区的用法看似简单,但是细节比较多,容易混淆,通过本篇笔记,便于记忆。

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.datjre/lib/tzmappings

Time Zone Database,简称tztzinfo,是一组表示地球上各地的时间历史的代码和数据,目前由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获取顺序如下:

  1. user.timezone (system)属性。
  2. 如果user.timezone没有定义,它就会尝试从user.country和java.home (System)属性来得到ID。找到与ID mapping接近的那个。
  3. 如果它没有成功找到一个时间区域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()
北京时间1585884600000Fri Apr 03 11:30:00 CST 2020
美国东部时间,不带夏令时1585931400000Fri Apr 03 11:30:00 GMT-05:00 2020
美国东部时间,带夏令时1585927800000Fri 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()
北京时间1585884600000Fri Apr 03 11:30:00 CST 2020
美国东部时间,不带夏令时1585884600000Thu Apr 02 22:30:00 GMT-05:00 2020

SimpleDateFormat 设置了固定的时区为中国,这样getTime()值总是相同的,但是toString不同,每次根据机器的当前时区打印。因此toString没啥意义,只能用在不关心时区的场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值