?【Java深層系列】「技術盲區」讓我們一起完全吃透針對于時間和日期相關的API指南

技術簡介

java中的日期處理一直是個問題,沒有很好的方式去處理,所以才有第三方框架的位置比如joda。文章主要對java日期處理的詳解,用1.8可以不用joda。

時間概念

首先我們對一些基本的概念做一些介紹,其中可以將GMT和UTC表示時刻大小等同。

UT時間

UT反應了地球自轉的平均速度。是通過觀測星星來測量的。

UTC

UTC是用原子鐘時間做參考,但保持和UT1在0.9秒內的時間,也就是說定時調整。

目前采用的時間標準是世界協調時UTC(Universal Time Coordinated)。如果計算機不聯網即使再精確也是不準的,因為UTC會進行調整,而且一般走的時間也是不精確的。

NTP

現在計算機一般用的網絡時間協議NTP(Network Time Protocol)是用于互聯網中時間同步的標準互聯網協議。用途是把計算機的時間同步到某些時間標準。

GMT(UT1)

GMT是完全符合地球自轉的時間,也被稱為UT1,格林尼治標準時間被用作英國的民用時間,或UTC。GMT被稱為“UT1”,它直接對應于地球的自轉,并受到該自轉輕微不規則的影響。正是UT1和UTC之間的差異通過應用閏秒保持>低于0.9秒。

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;
  }

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"

有些頭設置采用該格式。

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;
}

創建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

gregorian Calendar, julian Calendar:這是兩種歷法,我們一般用的通用的gregorian Calendar。

jdk1.8之前

主要的類有記錄時間戳的Date,時間和日期進行轉換的Calendar,用來格式化和解析時間字符串的DateFormat

java.util.Date

使用前要注意時間表示的規則。

還有這個類有很多過期方法不推薦使用,很多已經被Calendar代替。

構造方法

這個類代表某個時刻的毫秒值,既然是毫秒值也就說需要有一個參考值。

在接受或返回年、月、日期、小時、分鐘和秒值的所有類日期方法中,使用以下表示形式:

年份y由整數y-1900表示。一個月由0到11的整數表示;0是一月,1是二月,依此類推;因此,11月是12月。日期(月的某一天)通常由1到31之間的整數表示。小時由0到23之間的整數表示。因此,從午夜到凌晨1點的時間是0小時,從中午到下午1點的時間是12小時。一分鐘通常由0到59之間的整數表示。第二個由0到61之間的整數表示;值60和61僅在閏秒內出現,甚至僅在實際正確跟蹤閏秒的Java實現中出現。由于目前引入閏秒的方式,在同一分鐘內出現兩個閏秒的可能性極低,但本規范遵循ISO C的日期和時間約定。

當我們創建一個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類保存這個時刻的變量。

成員變量

Date對象打印出來是本地時間,而構造方法是沒有時區體現的。那么哪里體現了時區呢?

下面是Date的成員變量

gcal

獲取的是以下的對象。其中并沒有自定義字段。可以說只是一個gregorian(公歷)時間工廠獲取CalendarDate的子類。

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();
    }

fastTime

保存了一個時間戳表示時刻。最重要的參數。創建Date就是對這個值的賦值。

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;
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;
            }
            ... ...
    }

serialVersionUID

驗證版本一致性的UID

wtb

保存toString格式化用到的值

ttb

保存toString 格式化用到的值

主要方法

image

java.util.Calendar

主要也是其中保存的毫秒值time字段,下面是我們常用的方法,用了默認的時區和區域語言:

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

國內環境默認GregorianCalendar,但是TH-th用的BuddhistCalendar等
一些坑:

set(int,int,int,int,int,int)方法

方法不能設置毫秒值,所以當用getInstance后即使用設置相同的值,最后毫秒值也是不一致的。所以如果有需要,將MILLISECOND清零。

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不會修改更大的字段的值。

java.text.SimpleDateFormat

創建設置pattern字符串,可以表示的格式如下:

image

日期格式是不同步的。建議為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須是外部同步的。

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"); 
        }
 };

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。

jdk1.8的時間類

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 -定義了時區,相對于時區的偏移量,時區規則等。

該包的API提供了大量相關的方法,這些方法一般有一致的方法前綴:

  • of:靜態工廠方法。
  • parse:靜態工廠方法,關注于解析。
  • get:獲取某些東西的值。
  • is:檢查某些東西的是否是true。
  • with:不可變的setter等價物。
  • plus:加一些量到某個對象。
  • minus:從某個對象減去一些量。
  • to:轉換到另一個類型。
  • at:把這個對象與另一個對象組合起來,例如: date.atTime(time)。

相互轉化和Instant

可以看到老的時間日期類里面都有了Instant的轉化。Instant可以說是新舊轉換的中轉站。Instant主要維護了秒和納秒字段,可以表示納秒范圍。當然不支持的話會拋出異常。主要還是java.util.Date轉換成新的時間類。

Clock

提供了訪問當前時間的方法,也可以獲取當前Instant。Clock是持有時區或者時區偏移量的。如果只是獲取當前時間戳,推薦還是用System.currentTimeMillis()

ZoneId/ZoneOffset/ZoneRules

zone id 主要包括兩個方面,一個是相對于對于UTC/Greenwich的固定偏移量相當于一個大時區,另一個是時區內有特殊的相對于UTC/Greenwich偏移量的地區。通常固定偏移量部分可以用ZoneOffset表示,用normalized()判斷是否可以用ZoneOffset表示。判斷主要用到了時區規則ZoneRules。時區的真正規則定義在ZoneRules中,定義了什么時候多少偏移量。使用這種方式是因為ID是固定不變的,但是規則是政府定義并且經常變動。

LocalDateTime/LocalTime/LocalDate/ZoneDateTime

LocalDateTIme/LocalTime/LocalDate都是沒有時區概念的。這句話并不是
說不能根據時區獲取時間,而是因為這些類不持有表示時區的變量。而
ZoneDateTime持有時區和偏移量變量。

這些類都可以對時間進行修改其實都是生成新對象。所以這里的時間類都是天然支持多線程的。

這些時間類中都提供了獲取時間對象,修改時間獲取新的時間對象,格式化時間等。

注意點

LocaDateTime的atZone是調整本地時間的時區的。并不會改變時間。要使用其他時間需要獲取的LocalDateTime.now的時候的就要傳入時區變量。

DateTimeFormatter

時間對象進行格式化時間的需要用到格式化和解析日期和時間的時候需要用到DateTimeFormatter。

擴展及思考

用SimpleDateFormat格式化的時候不要用12小時制即hh,因為很容易導致上午下午不分,比如“2017-01-01 00:00:00“可能就變顯示成”2017-01-01 12:00:00”
::符號

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);
        }
};
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容