Java8系列 (六) 新的日期和時間API
概述
在Java8之前, 我們一般都是使用 SimpleDateFormat 來解析和格式化日期時間, 但它是執行緒不安全的。
@Test public void test() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { executorService.execute(() -> { try { Date date = sdf.parse("20191103091515"); System.out.println(date.toString()); } catch (ParseException e) { e.printStackTrace(); } }); } executorService.shutdown(); }
多次執行上面這段程式, 會報不同的異常, 下面是其中的一種
Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-3" java.lang.NumberFormatException: empty String at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842) at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.lang.Double.parseDouble(Double.java:538) at java.text.DigitList.getDouble(DigitList.java:169) at java.text.DecimalFormat.parse(DecimalFormat.java:2089) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at com.java8.action.Demo.lambda$test$0(Demo.java:25) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
原因也很簡單, 檢視一下原始碼, 發現 SimpleDateFormat 類繼承了父類 DateFormat 的成員變數 protected Calendar calendar; , 而Calendar 類沒有被 final 修飾, 是可以被修改的。
回到上面這個問題, 看一下 SimpleDateFormat 的解析日期時間的API
進入 establish() 方法裡面看一下
到此, 已經基本明瞭, 因為每次 SimpleDateFormat 解析日期時間都會清空一下它的成員變數 calendar 的值, 所以當多個執行緒併發訪問同一個 SimpleDateformat 時, 就會有執行緒不安全問題。
解決方式也很簡單, 你可以使用 ThreadLocal 類存放 SimpleDateFormat 物件, 讓每個執行緒擁有自己的SimpleDateFormat物件。
/**Map鍵對應不同的解析規則字串, 比如yyyyMMdd*/ private static Map<String, ThreadLocal<SimpleDateFormat>> tl = new HashMap<>();
回到我們今天的主題, 在Java8中引入了新的日期和時間API, 這也是下面要介紹的內容。
新的日期時間類都被 final 修飾, 不存在想上面介紹的老版本API的執行緒不安全問題。
LocalDate、LocalTime和LocalDateTime
LocalDate和LocalTime, LocalDateTime 提供了許多靜態工廠方法來建立它們的例項物件, 並且這三者之間可以很方便的互相進行型別轉換。
@Test public void test() { //靜態方法建立物件 LocalDate ld = LocalDate.of(2019, 10, 3); System.out.println(ld.getYear() + "\t" + ld.getMonth() + "\t" + ld.getDayOfMonth() + "\t" + ld.getDayOfWeek() + "\t" + ld.lengthOfMonth() + "\t" + ld.isLeapYear());//result: 2019 OCTOBER 3 THURSDAY 31 false LocalDate now = LocalDate.now(); System.out.println(now.get(ChronoField.YEAR) + "\t" + now.get(ChronoField.MONTH_OF_YEAR) + "\t" + now.get(ChronoField.DAY_OF_MONTH));//result: 2019 11 3 LocalTime lt = LocalTime.of(20, 44, 12); System.out.println(lt.getHour() + "\t" + lt.getMinute() + "\t" + lt.getSecond());//result: 20 44 12 //解析字串 LocalDate ld2 = LocalDate.parse("2019-10-05");//預設格式: yyyy-MM-dd System.out.println(ld2.toString());//result: 2019-10-05 LocalTime lt2 = LocalTime.parse("20:42:12.828");//預設格式: HH:mm:ss.SSS System.out.println(lt2.toString());//result: 20:42:12.828 //互相進行型別轉換 LocalDateTime ldt = LocalDateTime.of(2019, 10, 5, 21, 12, 10, 888).atZone(ZoneId.of("Asia/Shanghai")).toLocalDateTime(); LocalDateTime ldt2 = LocalDateTime.of(ld2, lt2);// 2019-10-05T20:42:12.828 LocalDateTime ldt3 = ld2.atTime(10, 10, 10);// 2019-10-05T10:10:10 LocalDateTime ldt4 = ld2.atTime(lt2); LocalDateTime ldt5 = lt2.atDate(ld2); LocalDateTime ldt6 = LocalDateTime.parse("2019/10/05 20:20:20.888", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS")); LocalDate ld6 = ldt6.toLocalDate(); LocalTime lt6 = ldt6.toLocalTime(); }
Instant
Instant對時間的建模方式是以UTC時區的1970年1月1日午夜時分開始所經歷的秒數進行計算,它不包含時區資訊。Instant類是為了方便計算機處理日期和時間而設計的。
Instant.now().toEpochMilli() 可以獲取當前時間的時間戳, 另外, Instant 提供了類似 ofEpochMilli() 的方法根據某個時間戳獲取 Instant 例項, isBefore()和isAfter() 則用來比較兩個 Instant 的大小
@Test public void test2() { long milli = Instant.now().toEpochMilli();//獲取當前時間戳 Instant instant = Instant.ofEpochMilli(1572749169937L);//根據某個時間戳獲取Instant例項 Instant instant2 = instant.minusSeconds(1000L); System.out.println(instant.isAfter(instant2));//true }
Duration和Period
Duration 物件用秒和納秒來衡量時間的長短,如果想要對多個時間物件進行日期運算,可以用 Period 類
@Test public void test3() { Duration d1 = Duration.between(LocalDateTime.of(2019, 10, 7, 15, 55, 55, 888), LocalDateTime.now()); Duration d2 = Duration.between(LocalTime.of(17, 55, 10), LocalTime.now()); Duration d3 = Duration.between(Instant.ofEpochMilli(1570544602000L), Instant.now()); System.out.println(d3.toHours());// 612 //Duration物件用秒和納秒來衡量時間的長短,所以入參不能使用LocalDate型別, 否則拋UnsupportedTemporalTypeException: Unsupported unit: Seconds //Duration.between(LocalDate.of(2019, 10, 7), LocalDate.now()); //如果想要對多個時間物件進行日期運算,可以用Period Period p1 = Period.between(LocalDate.of(2018, 8, 30), LocalDate.now()); System.out.println(p1.getYears() + "\t" + p1.getMonths() + "\t" + p1.getDays());// 1 2 4 //工廠方法介紹 Duration threeMinutes = Duration.ofMinutes(3); threeMinutes = Duration.of(3, ChronoUnit.MINUTES); Period tenDays = Period.ofDays(10); Period threeWeeks = Period.ofWeeks(3); Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1); }
Temporal介面
LocalDate、LocalTime、LocalDateTime、Instant類都實現了 Temporal 介面,有很多通用的處理日期和時間的方法,比如plus(), minus(), with()
@Test public void test4() { LocalDate ld = LocalDate.of(2019, 10, 7); //修改時間物件的某個屬性值,返回一個新的物件 LocalDate ld2 = ld.withDayOfYear(365);//2019-12-31 LocalDate ld3 = ld.withDayOfMonth(18);//2019-10-18 LocalDate ld4 = ld.with(ChronoField.MONTH_OF_YEAR, 8);//2019-08-07 //對時間物件進行加減運算 LocalDate ld5 = ld.plusWeeks(2L);//2019-10-21 LocalDate ld6 = ld.minusYears(9L);//2010-10-07 LocalDate ld7 = ld.plus(Period.ofMonths(2));//2019-12-07 LocalDate ld8 = ld.plus(2L, ChronoUnit.MONTHS);//2019-12-07 LocalTime lt = LocalTime.parse("10:10:10.888"); LocalTime lt1 = lt.plus(Duration.ofHours(2L));//12:10:10.888 LocalTime lt2 = lt.plus(120L, ChronoUnit.MINUTES);//12:10:10.888 }
TemporalAdjuster介面
TemporalAdjuster 類提供了更多對日期定製化操作的功能, 諸如將日期調整到下個工作日、本月的最後的一天、今年的第一天等等。
@Test public void test5() { LocalDate ld = LocalDate.of(2019, 10, 7); LocalDate ld1 = ld.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));//2019-10-11 LocalDate ld2 = ld.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));//2019-10-07 LocalDate ld3 = ld.with(TemporalAdjusters.firstDayOfNextMonth());//2019-11-01 //自定義TemporalAdjuster, 來計算下一個工作日所在的日期 LocalDate ld4 = LocalDate.of(2019, 10, 11).with(temporal -> { DayOfWeek now = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); long dayToAdd = now.equals(DayOfWeek.FRIDAY) ? 3L : now.equals(DayOfWeek.SATURDAY) ? 2L : 1L; return temporal.plus(dayToAdd, ChronoUnit.DAYS); });//2019-10-14 //對於經常複用的相同操作,可以將邏輯封裝一個類中 TemporalAdjuster temporalAdjuster = TemporalAdjusters.ofDateAdjuster(temporal -> { DayOfWeek now = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); long dayToAdd = now.equals(DayOfWeek.FRIDAY) ? 3L : now.equals(DayOfWeek.SATURDAY) ? 2L : 1L; return temporal.plus(dayToAdd, ChronoUnit.DAYS); }); }
DateTimeFormatter
DateTimeFormatter 用於進行可定製的日期時間格式化, 功能相當於以前的 SimpleDateFormat 類
@Test public void test6() { //日期轉字串 LocalDate ld = LocalDate.of(2019, 10, 7); String s1 = ld.format(DateTimeFormatter.BASIC_ISO_DATE);//20191007 String s2 = ld.format(DateTimeFormatter.ISO_LOCAL_DATE);//2019-10-07 //字串轉日期 LocalDateTime ld1 = LocalDateTime.parse("2019-10-07 22:22:22.555", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); }
ZoneId時區
ZoneId 是老版本的 TimeZone 的替代品, ZonedDateTime 代表了相對於指定時區的時間點
@Test public void test7() { //LocalDate、LocalDateTime、Instant 轉 ZonedDateTime ZonedDateTime zdt1 = LocalDate.of(2019, 10, 7).atStartOfDay(ZoneId.systemDefault()); ZonedDateTime zdt2 = LocalDateTime.of(2019, 10, 7, 15, 55, 55, 888).atZone(ZoneId.of("Asia/Shanghai")); ZonedDateTime zdt3 = Instant.now().atZone(ZoneId.of("Asia/Yerevan")); //Instant轉LocalDateTime LocalDateTime ldt1 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()); //下面的兩個栗子介紹了ZoneOffset,他是利用和 UTC/格林尼治時間的固定偏差計算時區,但不推薦使用,因為ZoneOffset並未考慮任何夏令時的影響 LocalDateTime ldt2 = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.of("+8")); //LocalDateTime轉Instant Instant instant = LocalDateTime.of(2019, 10, 7, 15, 55, 55).toInstant(ZoneOffset.of("+4")); }
參考資料
Java8 實戰
SimpleDateFormat執行緒不安全及解決辦法
Java進階(七)正確理解Thread Local的原理與適用場景
作者:張小凡
出處:https://www.cnblogs.com/qingshanli/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。如果覺得還有幫助的話,可以點一下右下角的【推薦】。