java日期时间详解

一、简介

java中的日期处理一直是个问题,没有很好的方式去处理,所以才有第三方框架的位置比如joda。
文章主要对java日期处理的详解,用1.8可以不用joda。

1. 相关概念

首先我们对一些基本的概念做一些介绍,其中可以将GMT和UTC表示时刻大小等同。

1.1 UT时间
UT反应了地球自转的平均速度。是通过观测星星来测量的。
具体可以看参考1.

1.2 UTC
UTC是用原子钟时间做参考,但保持和UT1在0.9秒内的时间,也就是说定时调整。现在计算机一般用的网络时间协议NTP(Network Time Protocol)是用于互联网中时间同步的标准互联网协议。NTP的用途是把计算机的时间同步到某些时间标准。目前采用的时间标准是世界协调时UTC(Universal Time Coordinated)。如果计算机不联网即使再精确也是不准的,因为UTC会进行调整,而且一般走的时间也是不精确的。附不能上网的电脑如何同步时间资料可以看参考2.

1.3 GMT

Today, GMT is used as the UK’s civil time, or UTC. GMT has been referred to as “UT1", which directly corresponds to the rotation of the Earth, and is subject to that rotation’s slight irregularities. It is the difference between UT1 and UTC that is kept > below 0.9s by the application of leap seconds.

简单点理解就是GMT是完全符合地球自转的时间,也被称为UT1。UTC时间是原子钟时间,当UTC时间比GMT时间相0.9秒的时候,UTC会做调整与GMT一致,也就是说UTC时间和GMT的时间差不会大于0.9秒。

1.4 ISO 8601
一种时间交换的国际格式。
有些接口调用表示UTC/GMT时间的时候用"yyyy-MM-dd'T'HH:mm:ss'Z'"格式显示。
带毫秒格式"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"。
joda中实现如下

// Alternate ISO 8601 format without fractional seconds
private static final String ALTERNATIVE_ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";private static DateFormat getAlternativeIso8601DateFormat() {
        SimpleDateFormat df = new SimpleDateFormat(ALTERNATIVE_ISO8601_DATE_FORMAT, Locale.US);
        df.setTimeZone(new SimpleTimeZone(0, "GMT"));
        return df;
}

1.5 RFC 822
STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES
其中ARPA网络其实就是互联网的前身。
有些地方会用RFC 822里的时间格式,格式如下

 date-time = [ day "," ] date time ; dd mm yy
                                                     ; hh:mm:ss zzz
//第二个相当于现在格式
"EEE, dd MMM yyyy HH:mm:ss z"

阿里oss里面有些头设置采用该格式。
joda中实现如下

// RFC 822 Date Format
private static final String RFC822_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z";
private static DateFormat getRfc822DateFormat() {
        SimpleDateFormat rfc822DateFormat =
                new SimpleDateFormat(RFC822_DATE_FORMAT, Locale.US);
        rfc822DateFormat.setTimeZone(new SimpleTimeZone(0, "GMT"));
        return rfc822DateFormat;
}

在4,5中创建SimpleDateFormat的Locale.US可以决定格式字符串某些字符的代替用哪个语言,比如EEE等

SimpleDateFormat df1=new SimpleDateFormat("GGGG yyyy/MMMM/dd HH:mm:ss EEE aaa zzzz",Locale.CHINA);
SimpleDateFormat df2=new SimpleDateFormat("GGGG yyyy/MMMM/dd HH:mm:ss EEE aaa zzzz",Locale.US);
//公元 2016/三月/27 23:32:10 星期日 下午 中国标准时间
//AD 2016/March/27 23:32:10 Sun PM China Standard Time

1.6 gregorian Calendar, julian Calendar
这是两种历法,我们一般用的通用的gregorian Calendar
扩展可以看参考内容

二、 相关类型的分析比较

1. jdk1.8之前

主要的类有记录时间戳的Date,时间和日期进行转换的Calendar,用来格式化和解析时间字符串的DateFormat

1.1 java.util.Date

使用前要注意时间表示的规则。

In all methods of class Date that accept or return
year, month, date, hours, minutes, and seconds values, the
following representations are used:

  • A year y is represented by the integer y - 1900.
  • A month is represented by an integer from 0 to 11; 0 is January, 1 is February, and so forth; thus 11 is December.
  • A date (day of month) is represented by an integer from 1 to 31 in the usual manner.
  • An hour is represented by an integer from 0 to 23. Thus, the hour from midnight to 1 a.m. is hour 0, and the hour from noon to 1 p.m. is hour 12.
  • A minute is represented by an integer from 0 to 59 in the usual manner.
  • A second is represented by an integer from 0 to 61; the values 60 and 61 occur only for leap seconds and even then only in Java implementations that actually track leap seconds correctly. Because of the manner in which leap seconds are currently introduced, it is extremely unlikely that two leap seconds will occur in the same minute, but this specification follows the date and time conventions for ISO C.
还有这个类有很多过期方法不推荐使用,很多已经被Calendar代替。

1.1.1 构造方法

注释中说The class Date represents a specific instant in time, with millisecond precision.
也就是说这个类代表某个时刻的毫秒值,既然是毫秒值也就说需要有一个参考值。
当我们创建一个Date的时候获取的是哪一个毫秒值?

public Date() {
        this(System.currentTimeMillis());
 }
 public Date(long date) {
      fastTime = date;
}

System.currentTimeMillis()是本地方法,注释为the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.但是注释中也说这个可能会因为操作系统的时间而不准。有些操作系统不一定是用毫秒表示的。这个时间都是用的UTC时间,不和时区有关的,这个无关的意思是同一时刻每个时区下获得的值应该是一致的,可以简单用程序验证一下获取的时间表达内容。

long time = System.currentTimeMillis();
System.out.println(time=(time/1000));
System.out.println("秒:"+ time%60);
System.out.println(time=(time/60));
System.out.println("分钟:"+time%60);
System.out.println(time=(time/60));
System.out.println("小时:"+time%24);

源码解析可以看参考的相关内容
可以理解成和UTC的1970年1月1日零点的差值。而fastTime就是Date类保存这个时刻的变量。

1.1.2 成员变量
Date对象打印出来是本地时间,而构造方法是没有时区体现的。那么哪里体现了时区呢?
下面是Date的成员变量

1.gcal
获取的是以下的对象。其中并没有自定义字段。可以说只是一个gregorian(公历)时间工厂获取CalendarDate的子类。

2.jcal
儒略历相关的对象。
在以下方法中用到

private static final BaseCalendar getCalendarSystem(BaseCalendar.Date cdate) {
        if (jcal == null) {
            return gcal;
        }
        if (cdate.getEra() != null) {
            return jcal;
        }
        return gcal;
    }
    synchronized private static final BaseCalendar getJulianCalendar() {
        if (jcal == null) {
            jcal = (BaseCalendar) CalendarSystem.forName("julian");
        }
        return jcal;
    }

当时间戳在以下情况下用儒略历,并且,在用到的时候会自动设置儒略历,所以在clone的时候也没有这个参数。所以这个可以忽略。

 private static final BaseCalendar getCalendarSystem(int year) {
        if (year >= 1582) {
            return gcal;
        }
        return getJulianCalendar();
    }
    private static final BaseCalendar getCalendarSystem(long utc) {
        // Quickly check if the time stamp given by `utc' is the Epoch
        // or later. If it's before 1970, we convert the cutover to
        // local time to compare.
        if (utc >= 0
            || utc >= GregorianCalendar.DEFAULT_GREGORIAN_CUTOVER
                        - TimeZone.getDefaultRef().getOffset(utc)) {
            return gcal;
        }
        return getJulianCalendar();
    }

3.fastTime
保存了一个时间戳表示时刻。最重要的参数。创建Date就是对这个值的赋值。

4.cdate
保存了时间相关内容,包括时区,语言等

    public static final int FIELD_UNDEFINED = -2147483648;
    public static final long TIME_UNDEFINED = -9223372036854775808L;
    private Era era;
    private int year;
    private int month;
    private int dayOfMonth;
    private int dayOfWeek;
    private boolean leapYear;
    private int hours;
    private int minutes;
    private int seconds;
    private int millis;
    private long fraction;
    private boolean normalized;
    private TimeZone zoneinfo;
    private int zoneOffset;
    private int daylightSaving;
    private boolean forceStandardTime;
    private Locale locale;

5.defalutCenturyStart
这个值可以忽略,在过期方法中用到。

@Deprecated
    public static long parse(String s) {
   ... ...
            // Parse 2-digit years within the correct default century.
            if (year < 100) {
                synchronized (Date.class) {
                    if (defaultCenturyStart == 0) {
                        defaultCenturyStart = gcal.getCalendarDate().getYear() - 80;
                    }
                }
                year += (defaultCenturyStart / 100) * 100;
                if (year < defaultCenturyStart) year += 100;
            }
            ... ...
    }

6.serialVersionUID
验证版本一致性的UID

7.wtb
保存toString格式化用到的值

8.ttb
保存toString 格式化用到的值

1.1.3 主要方法

主要是比较方法和设置方法。由于项目遗留和数据层的原因限制这个类用到还是比较多。

1.2 java.util.Calendar

其实主要也是其中保存的毫秒值time字段

下面是我们常用的方法,用了默认的时区和区域语言

public static Calendar getInstance()
    {
        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }

国内环境默认GregorianCalendar,但是TH-th用的BuddhistCalendar等

一些坑:

  1. set(int,int,int,int,int,int)方法
    方法不能设置毫秒值,所以当用getInstance后即使用设置相同的值,最后毫秒值也是不一致的。所以如果有需要,将MILLISECOND清零。

  2. set,add,get,roll
    set方法不会马上计算时间,指是修改了对应的成员变量,只有get()、getTime()、getTimeInMillis()、add() 或 roll()的时候才会做调整

        //2000-8-31
        Calendar cal1 = Calendar.getInstance();
        cal1.set(2000, 7, 31, 0, 0 , 0);
        //应该是 2000-9-31,也就是 2000-10-1
        cal1.set(Calendar.MONTH, Calendar.SEPTEMBER);
        //如果 Calendar 转化到 2000-10-1,那么现在的结果就该是 2000-10-30
        cal1.set(Calendar.DAY_OF_MONTH, 30);
        //输出的是2000-9-30,说明 Calendar 不是马上就刷新其内部的记录
        System.out.println(cal1.getTime());

也就是说多次设置的时候如果中间有需要调整的时间,但是实际是不会做调整的。所以尽量将无法确定的设置之后不要再进行其他调整,防止最后实际值与正常值不准。

add方法会马上做时间修改
roll与add类似,但是roll不会修改更大的字段的值。

1.3 java.text.SimpleDateFormat

创建设置pattern字符串,可以表示的格式如下

日期格式是不同步的。建议为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须是外部同步的。
SimpleDateFormat 是线程不安全的类,其父类维护了一个Calendar,调用相关方法有可能会修改Calendar。一般不要定义为static变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。 正例:注意线程安全,使用 DateUtils。org.apache.commons.lang.time.DateUtils,也推荐如下处理:

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { 
        @Override 
        protected DateFormat initialValue() { 
                return new SimpleDateFormat("yyyy-MM-dd"); 
        }
 };

1.4 java.sql.Date/Time/Timestamp

这几个类都继承了java.util.Date。
相当于将java.util.Date分开表示了。Date表示年月日等信息。Time表示时分秒等信息。Timestamp多维护了纳秒,可以表示纳秒。

如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

2. jdk1.8的时间类

2.1 总览

1.8增加了新的date-time包,遵循JSR310。核心代码主要放在java.time包下。默认的日历系统用的ISO-8601(基于格里高利历)。
java.time下主要内容包括:

  • java.time -主要包括,日期,时间,日期时间,时刻,期间,和时钟相关的类。
  • java.time.chrono -其他非ISO标准的日历系统可以用java.time.chrono,里面已经定义了一部分年表,你也可以自定义。
  • java.time.format -格式化和解析日期时间的类
  • java.time.temporal -扩展API,主要是提供给写框架和写库的人,允许日期时间相互操作,访问,和调整。字段和单位在这个包下定义。
  • java.time.zone -定义了时区,相对于时区的偏移量,时区规则等。

1.8日期时间api没有并发问题,清晰容易使用。

该包的API提供了大量相关的方法,这些方法一般有一致的方法前缀:

  • of:静态工厂方法。
  • parse:静态工厂方法,关注于解析。
  • get:获取某些东西的值。
  • is:检查某些东西的是否是true。
  • with:不可变的setter等价物。
  • plus:加一些量到某个对象。
  • minus:从某个对象减去一些量。
  • to:转换到另一个类型。
  • at:把这个对象与另一个对象组合起来,例如: date.atTime(time)。

2.2 相互转化和Instant

可以看到老的时间日期类里面都有了Instant的转化。Instant可以说是新旧转换的中转站。Instant主要维护了秒和纳秒字段,可以表示纳秒范围。当然不支持的话会抛出异常。主要还是java.util.Date转换成新的时间类。

2.3 Clock

提供了访问当前时间的方法,也可以获取当前Instant。Clock是持有时区或者时区偏移量的。如果只是获取当前时间戳,推荐还是用System.currentTimeMillis()

2.4 ZoneId/ZoneOffset/ZoneRules

zone id 主要包括两个方面,一个是相对于对于UTC/Greenwich的固定偏移量相当于一个大时区,另一个是时区内有特殊的相对于UTC/Greenwich偏移量的地区。通常固定偏移量部分可以用ZoneOffset表示,用normalized()判断是否可以用ZoneOffset表示。判断主要用到了时区规则ZoneRules。时区的真正规则定义在ZoneRules中,定义了什么时候多少偏移量。使用这种方式是因为ID是固定不变的,但是规则是政府定义并且经常变动。
Time-zone IDs是三种类型

  • 'z'和以'+'/'-'开头的id
  • 固定前缀和offset-style IDs,比如'GMT+2' or 'UTC+01:00',可识别的前缀有GMT,UTC,UT。可以标准化成ZoneOffset,通过normalized方法
  • 基于地区的IDs,需要包含2个或以上的特征,并且不以'UTC', 'GMT', 'UT' '+' 或者'-'开头。通过配置实现,具体可以看ZoneRulesProvider,配置实现了通过ID找到具体的ZoneRules。

2.4 LocalDateTime/LocalTime/LocalDate/ZoneDateTime

LocalDateTIme/LocalTime/LocalDate都是没有时区概念的。这句话并不是说不能根据时区获取时间,而是因为这些类不持有表示时区的变量。而ZoneDateTime持有时区和偏移量变量。
这些类都可以对时间进行修改其实都是生成新对象。所以这里的时间类都是天然支持多线程的。
这些时间类中都提供了获取时间对象,修改时间获取新的时间对象,格式化时间等。
注意点

  • LocaDateTime的atZone是调整本地时间的时区的。并不会改变时间。要使用其他时间需要获取的LocalDateTime.now的时候的就要传入时区变量。

2.5 DateTimeFormatter

时间对象进行格式化时间的需要用到格式化和解析日期和时间的时候需要用到DateTimeFormatter。

三、扩展及思考

  1. 用SimpleDateFormat格式化的时候不要用12小时制即hh,因为很容易导致上午下午不分,比如“2017-01-01 00:00:00“可能就变显示成”2017-01-01 12:00:00”
  2. ::符号
    LocalDateTime的方法
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) {
    Objects.requireNonNull(formatter, "formatter");
    return formatter.parse(text, LocalDateTime::from);
}

parse调用的方法是

public <T> T parse(CharSequence text, TemporalQuery<T> query) {
   ... ...
}

LocalDateTime::from调用的方法是

public static LocalDateTime from(TemporalAccessor temporal) {
    .... ...     
}

其中temporal是LocalDateTime的接口
这里其实大家都有一个疑问就是LocalDateTime::from到底代表什么意思。

LocalDateTime::from
//与下列表示相同
x ->  LocalDateTime.from(x)
//相当于
new TemporalQuery<LocalDateTime>(){
       @Override
        public LocalDateTime queryFrom(TemporalAccessor temporal) {
             return LocalDateTime.from(temporal);
        }
};

具体可以看参考资料

四、参考及扩展资料

1.universal-time

2.系统-不能上网的电脑如何同步时间?

3.JVM源码分析之System.currentTimeMillis及nanoTime原理详解

4.Change From Julian to Gregorian Calendar

4.Java日期时间(Date/Time)(附Date.java源码)

5.everything-about-java-8

6.深入理解Java 8 Lambda

7.Java Date-Time Packages官网资料

8.JSR 310: Date and Time API

9.STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES

10.gmt

未完成待续..

posted @ 2017-12-17 19:22  道常  阅读(6758)  评论(1编辑  收藏  举报