1. 程式人生 > >在Java中,你真的會日期轉換嗎

在Java中,你真的會日期轉換嗎

1. 什麼是SimpleDateFormat

在java doc對SimpleDateFormat的解釋如下:

SimpleDateFormatis a concrete class for formatting and parsing dates in a locale-sensitive manner. It allows for formatting(date → text), parsing (text → date), and normalization.

SimpleDateFormat是一個用來對位置敏感的格式化和解析日期的實體類。他允許把日期格式化成text,把text解析成日期和規範化。

1.1 使用SimpleDateFormat

simpleDateFormat的使用方法比較簡單:

public static void main(String[] args) throws Exception {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd  HH:mm:ss");
    System.out.println(simpleDateFormat.format(new Date()));
    System.out.println(simpleDateFormat.parse("2018-07-09  11:10:21"));
}

1) 首先需要定義一個日期的pattern,這裡我們定義的是"yyyy-mm-dd HH:mm:ss" ,也就是我們這個simpleDateFormat不管是格式化還是解析都需要按照這個pattern。

2) 對於format需要傳遞Date的物件,會返回一個String型別,這個String會按照我們上面的格式生成。

3) 對於parse需要傳遞一個按照上面pattern的字串,如果傳遞錯誤的pattern會丟擲java.text.ParseException異常,如果傳遞正確的會生成一個Date物件。

附:格式佔位符G年代標誌符y年M月d日h時在上午或下午(1~12)H時 在一天中(0~23)m分s秒S毫秒E星期D一年中的第幾天F一月中第幾個星期幾w一年中第幾個星期W一月中第幾個星期a上午/下午標記符k時在一天中(1~24)K時在上午或下午(0~11)z時區。

2. SimpleDateFormat的隱患

很多初學者,或者一些經驗比較淺的java開發工程師,用SimpleDateFormat會出現一些奇奇怪怪的BUG。

1) 結果值不對:轉換的結果值經常會出人意料,和預期不同,往往讓很多人摸不著頭腦。

2) 記憶體洩漏: 由於轉換的結果值不對,後續的一些操作,如一個迴圈,累加一天處理一個東西,但是生成的日期如果異常導致很大的話,會讓這個迴圈變成一個類似死迴圈一樣導致系統記憶體洩漏,頻繁觸發GC,造成系統不可用。

為什麼會出現這麼多問題呢?因為SimpleDateFormat執行緒不安全,很多人都會寫個Util類,然後把SimpleDateFormat定義成全域性的一個常量,所有執行緒都共享這個常量:

protected static final SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd");

public static Date formatDate(String date) throws ParseException {
    return dayFormat.parse(date);
}

為什麼SimpleDateFormat會執行緒不安全呢,在SimpleDateFormat原始碼中,所有的格式化和解析都需要通過一箇中間物件進行轉換,那就是Calendar,而這個也是我們出現執行緒不安全的罪魁禍首,試想一下當我們有多個執行緒操作同一個Calendar的時候後來的執行緒會覆蓋先來執行緒的資料,那最後其實返回的是後來執行緒的資料,這樣就導致我們上面所述的BUG的產生:

在這裡順便給大家推薦一個架構交流群:375989619,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源。相信對於已經工作和遇到技術瓶頸的碼友,在這個群裡會有你需要的內容。

// Called from Format after creating a FieldDelegate 
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
    // Convert input date to time field list 
    calendar.setTime(date);
    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;
}

3. 如何避坑

對於SimpleDateFormat的解決方法有下面幾種:

3.1 新建SimpleDateFormat

上面出現Bug的原因是因為所有執行緒都共用一個SimpleDateFormat,這裡有個比較好解決的辦法,每次使用的時候都建立一個新的SimpleDateFormat,我們可以在DateUtils中將建立SimpleDateFormat放在方法內部:

public static Date formatDate(String date) throws ParseException {
    SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd");
    return dayFormat.parse(date);
}

上面這個方法雖然能解決我們的問題但是引入了另外一個問題就是,如果這個方法使用量比較大,有可能會頻繁造成Young gc,整個系統還是會受一定的影響。

3.2 使用ThreadLocal

使用ThreadLocal能避免上面頻繁的造成Young gc,我們對每個執行緒都使用ThreadLocal進行儲存,由於ThreadLocal是執行緒之間隔離開的,所以不會出現執行緒安全問題:

private static ThreadLocal simpleDateFormatThreadLocal = new ThreadLocal<>();

public static Date formatDate(String date) throws ParseException {
    SimpleDateFormat dayFormat = getSimpleDateFormat();
    return dayFormat.parse(date);
}

private static SimpleDateFormatgetSimpleDateFormat() {
    SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
    if (simpleDateFormat == null) {
        simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd  HH:mm:ss")
        simpleDateFormatThreadLocal.set(simpleDateFormat);
    } 
    return simpleDateFormat;
}

3.3 使用第三方工具包

雖然上面的ThreadLocal能解決我們出現的問題,但是第三方工具包提供的功能更加強大,在java中有兩個類庫比較出名一個是Joda-Time,一個是Apache common包

3.3.1 Joda-Time (推薦)

Joda-Time 令時間和日期值變得易於管理、操作和理解。對於我們複雜的操作都可以使用Joda-Time操作,下面我列舉兩個例子,對於把日期加上90天,如果使用原生的Jdk我們需要這樣寫:

Calendar calendar = Calendar.getInstance();
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
SimpleDateFormat sdf = new SimpleDateFormat("E MM/dd/yyyy HH:mm:ss.SSS");
calendar.add(Calendar.DAY_OF_MONTH, 90); System.out.println(sdf.format(calendar.getTime()));

但是在我們的joda-time中只需要兩句話,並且api也比較通俗易懂,所以你為什麼不用Joda-Time呢?

DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);
System.out.println(dateTime.plusDays(90).toString("E MM/dd/yyyy HH:mm:ss.SSS"); 

3.3.2 common-lang包

在common-lang包中有個類叫FastDateFormat,由於common-lang這個包基本被很多Java專案都會引用,所以你可以不用專門去引用處理時間包,即可處理時間,在FastDateFormat中每次處理時間的時候會建立一個calendar,使用方法比較簡單程式碼如下所示:

FastDateFormat.getInstance().format(new Date());

3.4 升級jdk8 (推薦)

在java8中Date這個類中的很多方法包括構造方法都被打上了@Deprecated廢棄的註解,取而代之的是LocalDateTime,LocalDate LocalTime這三個類:

LocalDate無法包含時間;

LocalTime無法包含日期;

LocalDateTime才能同時包含日期和時間。

如果你是Java8,那你一定要使用他,在日期的格式化和解析方面不用考慮執行緒安全性,程式碼如下:

public static String formatTime(LocalDateTime time, String pattern) {
    return time.format(DateTimeFormatter.ofPattern(pattern));
}

當然localDateTime是java8的一大亮點,當然不僅僅只是解決了執行緒安全的問題,同樣也提供了一些其他的運算比如加減天數:

//日期加上一個數,根據field不同加不同值,field為ChronoUnit.* 
public static LocalDateTime plus(LocalDateTime time, long number, TemporalUnit field) {
    return time.plus(number, field);
}

//日期減去一個數,根據field不同減不同值,field引數為ChronoUnit.* 
public static LocalDateTime minu(LocalDateTime time, long number, TemporalUnit field) {
    return time.minus(number, field);
}

最後,如果你擔心使用LocalDateTime 會對你現有的程式碼產生很大的改變的話,那你可以將他們兩進行互轉:

// Date轉換為LocalDateTime 
public static LocalDateTime convertDateToLDT(Date date) {
    return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}

// LocalDateTime轉換為Date 
public static Date convertLDTToDate(LocalDateTime time) {
    return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
}