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

浩宇天尚 發表於 2021-11-28
Java

技術簡介

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 格式化用到的值

主要方法

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

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字串,可以表示的格式如下:

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

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

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