java的TimeUtils或者DateUtils的編寫心得


一、幾種常見的日期和時間類介紹

   介紹時間工具類不可避免必須要去觸碰幾個常見的日期和時間類,所以就簡單介紹一下。

1、jdk1.8之前的日期時間類

a、Date類

  我們可以通過new的方式生成一個Date對象,構造函數有參的和無參的,無參的是獲取當前的系統的時間,Date這個類有不少過期的方法,而且Date是線程不安全的,所以當你需要考慮線程安全的情況時,Date其實使用起來有一定的局限。Date類中有fastTime成員變量,所以對於一個Date來說,存有時間戳的,這就給各種日期和時間對象的的轉換提供了可能。

b、Calendar類

  這是一個抽象類,其有多個子類補充了很多其他的功能。Calendar翻譯過來就是日歷,所以我們可以獲取日期哪一年、哪一個月和哪一天,以及計算和獲取某個月的第一天等等。Calendar也是線程不安全的,其中的set方法其實可以修改Calendar中的成員變量。

c、SimpleDateFormat

  在java8之前我們一般習慣使用SimpleDateFormat這個類來進行時間轉換成字符串的操作。但是這個類是線程不安全的,這就意味着我們在轉換時候存在着轉換風險,當然我們有解決的方法,一個是使用本地變量ThreadLocal存放SimpleDateFormat,如果使用Map來存放,其實在生成的時候還是有線程安全的問題,這里我們使用悲觀鎖來做一下限制,當然也可以使用線程安全的Map(Hashtable、synchronizedMap、ConcurrentHashMap)。

下面使用synchronized加鎖:

/** 鎖對象 */
    private static final Object lockObj = new Object();

    /** 存放不同的日期模板格式的sdf的Map */
    private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();

    /**
     * 返回一個ThreadLocal的sdf,每個線程只會new一次sdf
     * 
     * @param pattern
     * @return
     */
    private static SimpleDateFormat getSdf(final String pattern) {
        ThreadLocal<SimpleDateFormat> tl = sdfMap.get(pattern);

        // 生成的時候我們需要去考慮線程問題,Map並沒有做線程處理,我們可以
        if (tl == null) {
            synchronized (lockObj) {
                tl = sdfMap.get(pattern);
                if (tl == null) {
                    // 只有Map中還沒有這個pattern的sdf才會生成新的sdf並放入map
                    System.out.println("put new sdf of pattern " + pattern + " to map");

                    // 這里是關鍵,使用ThreadLocal<SimpleDateFormat>替代原來直接new SimpleDateFormat
                    tl = new ThreadLocal<SimpleDateFormat>() {

                        @Override
                        protected SimpleDateFormat initialValue() {
                            System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);
                            return new SimpleDateFormat(pattern);
                        }
                    };
                    sdfMap.put(pattern, tl);
                }
            }
        }

        return tl.get();
    }

    /**
     * ThreadLocal的原理是,獲取一個靜態變量的副本,這個其實是犧牲空間換取時間的案例
     * 
     * @param date
     * @param pattern
     * @return
     */
    public static String format(Date date, String pattern) {
        return getSdf(pattern).format(date);
    }

    public static Date parse(String dateStr, String pattern) throws ParseException {
        return getSdf(pattern).parse(dateStr);
    }

當然我們也可以使用線程安全的map:

private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new Hashtable<String, ThreadLocal<SimpleDateFormat>>();

d、sql包中其實也有幾個時間的類  java.sql.Date/Time/Timestamp

  首先這幾個類繼承自util包中的Date類,相當於將java.util.Date分開表示了。Date表示年月日等信息。Time表示時分秒等信息。Timestamp多維護了納秒,可以表示納秒。平時用的不是很多。也是線程不安全的類。

2、java8以后的時間日期類 

在java8以后新增加了date-time包

a、Instant

   這個類在java8之前和之后的時間日期類中都提供了轉換的方法,這樣就能很明確的通過Instant這個中間變量實現,java8之前和之后的時間日期類的相互轉換。但是我們需要注意的是,Instant主要維護的是秒和納秒字段,可以表示納秒范圍,如果不符合轉換條件,就會拋出異常。

以Date類為例,看一下源碼:

public static Date from(Instant instant) {
        try {
            return new Date(instant.toEpochMilli());
        } catch (ArithmeticException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    /**
     * Converts this {@code Date} object to an {@code Instant}.
     * <p>
     * The conversion creates an {@code Instant} that represents the same
     * point on the time-line as this {@code Date}.
     *
     * @return an instant representing the same point on the time-line as
     *  this {@code Date} object
     * @since 1.8
     */
    public Instant toInstant() {
        return Instant.ofEpochMilli(getTime());
    }

b、Clock

   有獲取當前時間的方法,也可以獲取當前Instant,Clock是有時區或者說時區偏移量的。Clock是一個抽象類,其內部有幾個子類繼承自Clock。

幾個抽象方法:

public abstract ZoneId getZone();
public abstract Clock withZone(ZoneId zone);
public long millis() {
    return instant().toEpochMilli();
}
public abstract Instant instant();

c、ZoneId/ZoneOffset/ZoneRules

   ZoneId和ZoneOffset都是用來代表時區的偏移量的,一般ZoneOffset表示固定偏移量,ZoneOffset 表示與UTC時區偏移的固定區域(即UTC時間為標准),不跟蹤由夏令時導致的區域偏移的更改;ZoneId 表示可變區偏移,表示區域偏移及其用於更改區域偏移的規則夏令時。這里舉一個簡單的例子,美國東部時間,我們可以使用zoneId來表示,應為美國使用的是冬令時和夏令時的時候時間是有區別的,和中國的時差會有一個小時的差別。而ZoneRules 跟蹤區域偏移如何變化,時區的真正規則定義在ZoneRules中,定義了什么時候多少偏移量。

常用的幾個:

//美東時間
    public static final String TIMEZONE_EST_NAME = "US/Eastern";
    public static final ZoneId TIMEZONE_EST = ZoneId.of(TIMEZONE_EST_NAME);
//北京時間
    public static final String TIMEZONE_GMT8_NAME = "GMT+8";
    public static final ZoneId TIMEZONE_GMT8 = ZoneId.of(TIMEZONE_GMT8_NAME); 
    public static final ZoneOffset BEIJING_ZONE_OFFSET =ZoneOffset.of("+08:00");
public static final ZoneOffset STATISTIC_ZONE_OFFSET =ZoneOffset.of("+03:00"); private static final ZoneId NEW_YORK_ZONE_ID = ZoneId.of("America/New_York"); private static final ZoneId SHANGHAI_ZONE_ID = ZoneId.of("Asia/Shanghai");

d、LocalDateTime/LocalTime/LocalDate/ZoneDateTime

LocalDateTime/LocalTime/LocalDate都沒有時區的概念,其中LocalDate主要是對日期的操作,LocalTime主要是對時間的操作,LocalDateTime則是日期和時間都會涉及:

jshell> LocalDate.now()
$46 ==> 2018-07-07

jshell> LocalDate.of(2018, 3, 30)
$47 ==> 2018-03-30

jshell> LocalTime.now()
$48 ==> 00:32:06.883656

jshell> LocalTime.of(12,43,12,33333);
$49 ==> 12:43:12.000033333

jshell> LocalDateTime.now()
$50 ==> 2018-07-07T00:32:30.335562400

jshell> LocalDateTime.of(2018, 12, 30, 12,33)
$51 ==> 2018-12-30T12:33

jshell> LocalDateTime.of(LocalDate.now(), LocalTime.now())
$52 ==> 2018-07-07T00:40:38.198318200

而ZoneDateTime會帶有時區和偏移量的,可以看看他的成員變量:

 /**
     * The local date-time.
     */
    private final LocalDateTime dateTime;
    /**
     * The offset from UTC/Greenwich.
     */
    private final ZoneOffset offset;
    /**
     * The time-zone.
     */
    private final ZoneId zone;

可以看出這是集合了時間時區和偏移量的新的時間類。

二、常用的TimeUtils或者DateUtils的編寫

import java.text.ParseException;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Hashtable;
import java.util.Map;

/**
 * Created by hehuaichun on 2018/10/22.
 */
public class TimeUtils {
    /**
     * 考慮港股和美股 采用GMT-1時區來確定報表日 即T日的報表包含北京時間T日9時至T+1日9時的數據
     */
    public static final ZoneId TIMEZONE_GMT_1 = ZoneId.of("GMT-1");
    public static final String TIMEZONE_EST_NAME = "US/Eastern";
    public static final ZoneId TIMEZONE_EST = ZoneId.of(TIMEZONE_EST_NAME);
    public static final String TIMEZONE_GMT8_NAME = "GMT+8";
    public static final ZoneId TIMEZONE_GMT8 = ZoneId.of(TIMEZONE_GMT8_NAME);

    /**
     * 常用時間轉換格式
     */
    public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DATE_NO_GAP_FORMAT = "yyyyMMdd";
    public static final String DATE_GAP_FORMAT = "yyyy-MM-dd";
    public static final String TIME_HH_MM_FORMAT = "HHmm";
    public static final Map<String, DateTimeFormatter> DATE_TIME_FORMAT_MAP = new Hashtable<String, DateTimeFormatter>() {
        {
            put(TIME_FORMAT, DateTimeFormatter.ofPattern(TIME_FORMAT));
            put(DATE_NO_GAP_FORMAT, DateTimeFormatter.ofPattern(DATE_NO_GAP_FORMAT));
            put(DATE_GAP_FORMAT, DateTimeFormatter.ofPattern(DATE_GAP_FORMAT));
            put(TIME_HH_MM_FORMAT, DateTimeFormatter.ofPattern(TIME_HH_MM_FORMAT));
        }
    };

    /**
     * 根據format的格式獲取相應的DateTimeFormatter對象
     *
     * @param format 時間轉換格式字符串
     * @return
     */
    public static DateTimeFormatter getDateTimeFormatter(String format) {
        if (DATE_TIME_FORMAT_MAP.containsKey(format)) {
            return DATE_TIME_FORMAT_MAP.get(format);
        } else {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
            DATE_TIME_FORMAT_MAP.put(format, formatter);
            return formatter;
        }
    }

    /**
     * 獲取當前日期的開始時間
     *
     * @param zoneId 時間偏移量
     * @return
     */
    public static LocalDateTime todayStart(ZoneId zoneId) {
        return startOfDay(0, zoneId);
    }

    /**
     * 獲取當前的ZoneDateTime
     *
     * @param zoneId 時區偏移量
     * @return
     */
    public static ZonedDateTime now(ZoneId zoneId) {
        return ZonedDateTime.now(zoneId);
    }

    /**
     * 獲取當前日期的開始時間ZonedDateTime
     *
     * @param date   日期
     * @param zoneId 時區偏移量
     * @return
     */
    public static ZonedDateTime localDateToZoneDateTime(LocalDate date, ZoneId zoneId) {
        return date.atStartOfDay(zoneId);
    }

    /**
     * 獲取當前日期的開始時間
     *
     * @param dateTime
     * @return
     */
    public static LocalDateTime startOfDay(ZonedDateTime dateTime) {
        return dateTime.truncatedTo(ChronoUnit.DAYS).toLocalDateTime();
    }

    /**
     * 獲取今天后的指定天數的開始時間
     *
     * @param plusDays 當前多少天后
     * @param zoneId   時區偏移量
     * @return
     */
    public static LocalDateTime startOfDay(int plusDays, ZoneId zoneId) {
        return startOfDay(now(zoneId).plusDays(plusDays));
    }

    /**
     * 獲取指定日期的后幾個工作日的時間LocalDate
     *
     * @param date 指定日期
     * @param days 工作日數
     * @return
     */
    public static LocalDate plusWeekdays(LocalDate date, int days) {
        if (days == 0) {
            return date;
        }
        if (Math.abs(days) > 50) {
            throw new IllegalArgumentException("days must be less than 50");
        }
        int i = 0;
        int delta = days > 0 ? 1 : -1;
        while (i < Math.abs(days)) {
            date = date.plusDays(delta);
            DayOfWeek dayOfWeek = date.getDayOfWeek();
            if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY) {
                i += 1;
            }
        }
        return date;
    }

    /**
     * 獲取指定日期的后幾個工作日的時間ZoneDateTime
     *
     * @param date
     * @param days
     * @return
     */
    public static ZonedDateTime plusWeekdays(ZonedDateTime date, int days) {
        return plusWeekdays(date.toLocalDate(), days).atStartOfDay(date.getZone());
    }

    /**
     * 獲取當前月份的第一天的時間ZoneDateTime
     *
     * @param zoneId
     * @return
     */
    public static ZonedDateTime firstDayOfMonth(ZoneId zoneId) {
        return now(zoneId).withDayOfMonth(1);
    }


    /**
     * 將Date轉成指定時區的Date
     *
     * @param date
     * @return
     */
    public static Date dateToDate(Date date, ZoneId zoneId) {
        LocalDateTime dt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        return toDate(ZonedDateTime.of(dt, zoneId));
    }

    /**
     * 將LocalDate轉成Date
     *
     * @param date
     * @return
     */
    public static Date toDate(LocalDate date) {
        return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }

    /**
     * ZonedDateTime 轉換成Date
     *
     * @param dateTime
     * @return
     */
    public static Date toDate(ZonedDateTime dateTime) {
        return Date.from(dateTime.toInstant());
    }

    /**
     * String 轉換成 Date
     *
     * @param date
     * @param format
     * @return
     * @throws ParseException
     */
    public static Date stringToDate(String date, String format, ZoneId zoneId) throws ParseException {
        DateTimeFormatter formatter = getDateTimeFormatter(format).withZone(zoneId);
        Instant instant = Instant.from(formatter.parse(date));
        return Date.from(instant);
    }

    /**
     * 將Date轉成相應的時區的localDate
     *
     * @param date
     * @param zoneId
     * @return
     */
    public static LocalDate toLocalDate(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).toLocalDate();
    }

    /**
     * 將Instant轉成指定時區偏移量的localDate
     *
     * @param instant
     * @param zoneId
     * @return
     */
    public static LocalDate toLocalDate(Instant instant, ZoneId zoneId) {
        return instant.atZone(zoneId).toLocalDate();
    }

    /**
     * 將Instant轉換成指定時區偏移量的localDateTime
     * @param instant
     * @param zoneId
     * @return
     */
    public static LocalDateTime toLocalDateTime(Instant instant, ZoneId zoneId){
        return instant.atZone(zoneId).toLocalDateTime();
    }

    /**
     * 將Instant轉成系統默認時區偏移量的LocalDateTime
     * @param instant
     * @return
     */
    public static LocalDateTime toLocalDateTime(Instant instant){
        return toLocalDateTime(instant, ZoneId.systemDefault());
    }

    /**
     * 將ZoneDateTime 轉成 指定時區偏移量的LocalDateTime
     * @param zonedDateTime  時間
     * @param zoneId         指定時區偏移量
     * @return
     */
    public static LocalDateTime toLocalDateTime(ZonedDateTime zonedDateTime, ZoneId zoneId){
        return zonedDateTime.toInstant().atZone(zoneId).toLocalDateTime();
    }

    /**
     *將ZoneDateTime 轉成 LocalDateTime
     * @param zonedDateTime
     * @return
     */
    public static LocalDateTime toLocalDateTime(ZonedDateTime zonedDateTime){
       return zonedDateTime.toLocalDateTime();
    }

    /**
     * String 轉成 ZoneDateTime
     * 需要類似 yyyy-MM-dd HH:mm:ss 需要日期和時間信息完整信息
     *
     * @param date
     * @param format
     * @param zoneId
     * @return
     */
    public static ZonedDateTime stringToZoneDateTime(String date, String format, ZoneId zoneId) {
        DateTimeFormatter formatter = getDateTimeFormatter(format).withZone(zoneId);
        return ZonedDateTime.parse(date, formatter);
    }


    /**
     * 將時間戳long轉成ZonedDateTime
     *
     * @param timeStamp
     * @param zoneId
     * @return
     */
    public static ZonedDateTime longToZoneDateTime(long timeStamp, ZoneId zoneId) {
        return ZonedDateTime.from(Instant.ofEpochMilli(timeStamp).atZone(zoneId));
    }

    /**
     * 兩個時區的zoneDateTime相互轉換
     *
     * @param zonedDateTime 需要轉換的如期
     * @param zoneId        轉換成的ZoneDateTime的時區偏移量
     * @return
     */
    public static ZonedDateTime zonedDateTimeToZoneDateTime(ZonedDateTime zonedDateTime, ZoneId zoneId) {
        return ZonedDateTime.ofInstant(zonedDateTime.toInstant(), zoneId);
    }

    /**
     * Date 轉成 指定時區偏移量的ZoneDateTime
     * @param date
     * @param zoneId
     * @return
     */
    public static ZonedDateTime toZonedDateTime(Date date, ZoneId zoneId){
        return date.toInstant().atZone(zoneId);
    }

    /**
     * LocaldateTime 轉成 指定時區偏移量的ZonedDateTime
     * @param localDateTime  本地時間
     * @param zoneId         轉成ZonedDateTime的時區偏移量
     * @return
     */
    public static ZonedDateTime toZonedDateTime(LocalDateTime localDateTime, ZoneId zoneId){
        return localDateTime.atZone(zoneId);
    }

    /**
     * Date裝換成String
     *
     * @param date   時間
     * @param format 轉化格式
     * @return
     */
    public static String dateToString(Date date, String format, ZoneId zoneId) {
        DateTimeFormatter formatter = getDateTimeFormatter(format).withZone(zoneId);
        return formatter.format(date.toInstant());
    }

    /**
     * ZoneDateTime 轉換成 String
     *
     * @param dateTime
     * @param zoneId   localDateTime所屬時區
     * @return
     */
    public static String zoneDateTimeToString(ZonedDateTime dateTime, String format, ZoneId zoneId) {
        DateTimeFormatter formatter = getDateTimeFormatter(format).withZone(zoneId);
        return dateTime.format(formatter);
    }

    /**
     * LocalDateTime 轉成 String
     *
     * @param localDateTime
     * @param format
     * @return
     */
    public static String localDateTimeToString(LocalDateTime localDateTime, String format){
        DateTimeFormatter formatter = getDateTimeFormatter(format);
        return localDateTime.format(formatter);
    }

    /**
     * 將ZonedDateTime轉成時間戳long
     *
     * @parm zonedDateTime
     * @return
     */
    public static long zoneDateTimeToLong(ZonedDateTime zonedDateTime) {
        return zonedDateTime.toInstant().toEpochMilli();
    }

    /**
     * 將LocalDateTime轉成時間戳long
     * @param localDateTime
     * @param zoneId
     * @return
     */
    public static long toLong(LocalDateTime localDateTime, ZoneId zoneId){
       return zoneDateTimeToLong(localDateTime.atZone(zoneId));
    }


}

 

三、編寫的總結

1、首先我們盡量使用線程安全的時間類,也就是說盡量使用java8以后的幾種時間類。從源碼可以看出,java8之后的幾個時間類是用final修飾的,線程安全

2、我們要利用好靜態變量,也就是不僅在使用的全局變量的時候需要線程安全的問題,還需要考慮時間空間的代價。其實SimpleDateTimeFormat和DateTImeFormatter的生成會消耗不少資源。

3、Date類中含有時間戳,Instant有兩個成員變量seconds和nanos變量,這樣Instant就作為一個中間量,作用很大,不僅提供了java8之前的時間類和java8之后時間類的轉換,還提供了其他很多有用的方法。

4、ZonedDateTime含有時區偏移變量,其他的如Date、LocalDateTime、LocalDate、LocalTime、Instant都不含有時區偏移變量,但是Date想要轉換成java8之后的時間類或者使用DateTimeFormatter轉換時,需要提供ZoneId或者ZoneOffset,類似這種 date.toInstant().atZone(zoneId)。整體思路實現Date -> Instant -> ZonedDateTime -> 其他時間類。

5、ZonedDateTime是有日期和時間的,我們在DateTimeFormatter的時候需要注意, ZonedDateTime轉成字符串非常的靈活,但是字符串轉成ZonedDateTime的時候需要提供類似yyyy-MM-dd HH:mm:ss這種格式。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM