为什么format(System.currentTimeMill

2023-06-28  本文已影响0人  沉默的小象

System.currentTimeMillis() 获取的是从伦敦时间1970年1月1日 00:00:00 到现在的总时间,单位是毫秒。今年是2023年,那么这段时间转为年,应该是53左右。用代码验证下:

public class CurrentMills {
    public static void main(String[] args) {

        long total = System.currentTimeMillis();
        //估算,不考虑闰年。
        //注意365 * 24 * 60 * 60 * 1000已经超过int最大值了,所以用365L转为long
        float year_num = total * 1.0f / (365L * 24 * 60 * 60 * 1000);
        System.out.println(year_num);
    }
}

输出是:

53.52703

通过上面的测试,基本可以确认是从伦敦时间1970年1月1日 00:00:00 到现在的总时间。
那么按常理,格式化之后,应该显示的是伦敦时间,比北京时间早8小时。测试下:

public class CurrentMills {
    public static void main(String[] args) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = dateFormat.format(System.currentTimeMillis());
        System.out.println(dateStr);
    }
}

看下输出结果:


image.png

再看看电脑当前的北京时间:


image.png

伦敦时间应该比现在早8小时,所以应该输出的是早上6点。这里为什么输出的是14点?

我们设置时区试下:

import java.util.TimeZone;

public class CurrentMills {
    public static void main(String[] args) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); //在这里设置时区
        String dateStr = dateFormat.format(System.currentTimeMillis());
        System.out.println(dateStr);
    }
}

看下输出时间:


image.png

果然是早上6点了。看来是和时区设置有关。

SimpleDateFormat的默认时区是哪个?

打印出来看看:

public class CurrentMills {
    public static void main(String[] args) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(dateFormat.getTimeZone().getID());
        System.out.println(dateFormat.getTimeZone().getDisplayName());
    }
}

输出:

Asia/Shanghai
中国标准时间

看到输出Shanghai,惊讶了吧,北京和上海都是属于东八区,我们现在都说北京时间,那为什么显示的是Shanghai 而不是Beijing了?这篇文章timeZone为什么是Asia/Shanghai,而不是Asia/Beijing做了解答,简单说: 当年在分配时区(timeZone)的时候,还是在1949年之前,那时候,上海的洋人比较多,北京地位还不如上海。

我们来分析下,这个时区到底在什么时候设置进去的?

先看format方法,看看里面有没有设置时区。

 //Format.java

    public final String format (Object obj) {
        return format(obj, new StringBuffer(), new FieldPosition(0)).toString();
    }
    
    //抽象方法,在子类DateFormat中实现
    public abstract StringBuffer format(Object obj,
                    StringBuffer toAppendTo,
                    FieldPosition pos); 

//DateFormat.java 
    public final StringBuffer format(Object obj, StringBuffer toAppendTo,
                                     FieldPosition fieldPosition)
    {
        if (obj instanceof Date)
            return format( (Date)obj, toAppendTo, fieldPosition );
        else if (obj instanceof Number) //代码走了这个else if
            return format( new Date(((Number)obj).longValue()),
                          toAppendTo, fieldPosition );
        else
            throw new IllegalArgumentException("Cannot format given Object as a Date");
    }

执行了new Date(long),我们看看Date的构造器:

    public Date(long date) {
        fastTime = date;
    }

Date的构造器并没有做是什么,看来时区不是在Date的构造器里面设置的。继续看DateFormat.java

    public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
                                        FieldPosition fieldPosition);

抽象方法,看子类SimpleDateFormat.java的实现:

    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);  //这里有个calendar实例,也就是说SimpleDateFormat是持有Calendar对象的。

        boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

接着看Calendar.java的setTime()方法:

    public final void setTime(Date date) {
        setTimeInMillis(date.getTime());
    }

    public void setTimeInMillis(long millis) {
        // If we don't need to recalculate the calendar field values,
        // do nothing.
        if (time == millis && isTimeSet && areFieldsSet && areAllFieldsSet
            && (zone instanceof ZoneInfo) && !((ZoneInfo)zone).isDirty()) {  //这里有个zone,八成是时区
            return;
        }
        time = millis;
        isTimeSet = true;
        areFieldsSet = false;
        computeFields();
        areAllFieldsSet = areFieldsSet = true;
    }

看看这个zone的定义:

    //The TimeZone used by this calendar. Calendar uses the time zone data to translate between locale and GMT time.

    private TimeZone        zone;

果然是时区。我们理一下,SimpleDateFormat持有Calendar对象,Calendar又持有TimeZone对象,所以SimpleDateFormat初始化的时候,如果初始化了Calendar,那么TimeZone就会有默认值。我们看看SimpleDateFormat的构造器:

    public SimpleDateFormat(String pattern)
    {
        this(pattern, Locale.getDefault(Locale.Category.FORMAT));
    }

    public SimpleDateFormat(String pattern, Locale locale)
    {
        if (pattern == null || locale == null) {
            throw new NullPointerException();
        }

        initializeCalendar(locale); //找到目标了
        this.pattern = pattern;
        this.formatData = DateFormatSymbols.getInstanceRef(locale);
        this.locale = locale;
        initialize(locale);
    }

    private void initializeCalendar(Locale loc) {
        if (calendar == null) {
            assert loc != null;
            // The format object must be constructed using the symbols for this zone.
            // However, the calendar should use the current default TimeZone.
            // If this is not contained in the locale zone strings, then the zone
            // will be formatted using generic GMT+/-H:MM nomenclature.
            calendar = Calendar.getInstance(TimeZone.getDefault(), loc);
        }
    }

果然,初始化了calendar,并且是单例的。接着看看TimeZone的getDefault()方法:

    public static TimeZone getDefault() {
        return (TimeZone) getDefaultRef().clone();
    }

    static TimeZone getDefaultRef() {
        TimeZone defaultZone = defaultTimeZone;
        if (defaultZone == null) {
            // Need to initialize the default time zone.
            defaultZone = setDefaultZone();
            assert defaultZone != null;
        }
        // Don't clone here.
        return defaultZone;
    }

    //defaultTimeZone的定义
    private static volatile TimeZone defaultTimeZone;

defaultTimeZone的赋值有两个地方:

    private static synchronized TimeZone setDefaultZone() {
        TimeZone tz;
        // get the time zone ID from the system properties
        String zoneID = AccessController.doPrivileged(
                new GetPropertyAction("user.timezone"));

        // 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 = AccessController.doPrivileged(
                    new GetPropertyAction("java.home"));
            try {
                zoneID = getSystemTimeZoneID(javaHome);
                if (zoneID == null) {
                    zoneID = GMT_ID;
                }
            } catch (NullPointerException e) {
                zoneID = GMT_ID;
            }
        }

        // Get the time zone for zoneID. But not fall back to
        // "GMT" here.
        tz = getTimeZone(zoneID, false);

        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;
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
                public Void run() {
                    System.setProperty("user.timezone", id);
                    return null;
                }
            });

        defaultTimeZone = tz;  //这里是赋值
        return tz;
    }

    public static void setDefault(TimeZone zone)
    {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new PropertyPermission
                               ("user.timezone", "write"));
        }
        defaultTimeZone = zone; //这里是赋值
    }

这个defaultTimeZone和"user.timezone"有关。可以推理是获取本地属性。例如我在中国,就是中国标准时间。

上一篇下一篇

猜你喜欢

热点阅读