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 zoneIdlocalDateTime所屬時區 * @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這種格式。