1. 程式人生 > >填坑:Java 中的日期轉換

填坑:Java 中的日期轉換

允許 tld add joda 格式 apache 問題 format 隔離

我們之前討論過時間,在Java 中有一些方法會出現橫線?比如Date 過期方法。

參考文章:知識點:java一些方法會有橫線?以Date 過期方法為例

Java中的日期和時間處理方法

  • Date類(官方不再推薦使用,官方解釋Date類不利於國際化,推薦使用Calendar類)
  • Calendar類
  • DateFormat類 使用此類來時間初始化

我們發現,時間toLocalString 會有橫線:

vo.setSubmitDate(new Date().toLocaleString());

可以改為:

vo.setSubmitDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance(TimeZone.getTimeZone("GMT+08:00")).getTime()));

在最近的項目中,用SimpleDateFormat出現一些奇奇怪怪的BUG:

  • 1.結果值不對:轉換的結果值經常和預期不同。
  • 2.內存泄漏: 由於轉換的結果值不對,後續的一些操作會導致系統內存泄漏,頻繁觸發GC(垃圾回收),造成系統不可用。

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-10-24 12:10:24")); }

  • 1.定義一個日期"yyyy-mm-dd HH:mm:ss"的pattern, 也就是我們這個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會線程不安全呢?

在SimpleDateFormat源碼中,所有的格式化和解析都需要通過一個中間對象Calendar進行轉換,而這將會出現線程不安全的操作

比如當多個線程操作同一個Calendar的時候後來的線程會覆蓋先來線程的數據,那最後其實返回的是後來線程的數據,這樣就導致我們上面所述的BUG的產生

為什麽會出現這麽多問題呢?

因為SimpleDateFormat線程不安全,很多人都會寫個Util類,然後把SimpleDateFormat定義成全局的一個常量,所有線程都共享這個常量:

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

public static Date formatDate(String date) throws ParseException {

    returndayFormat.parse(date);
    
}

3.如何避坑

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

3.1 新建SimpleDateFormat

原因:所有線程都共用一個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();
    returndayFormat.parse(date); 
    
}

private static SimpleDateFormatgetSimpleDateFormat() {     
    SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();

    if(simpleDateFormat == null){ 
    
    simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd  HH:mm:ss") simpleDateFormatThreadLocal.set(simpleDateFormat); 
        
    }
    
    returnsimpleDateFormat; 
    
}

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) {

returntime.format(DateTimeFormatter.ofPattern(pattern)); 
    
}

localDateTime不僅解決了線程安全的問題,同樣也提供了一些其他的運算比如加減天數:

//日期加上一個數,根據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()); 
    
}

【公眾號】:一只阿木木

填坑:Java 中的日期轉換