你好,我是A哥(YourBatman)。java
好看的代碼,千篇一概!難看的代碼,臥槽臥槽~其實沒有什麼代碼是「史上最爛」的,要有也只有「史上更爛」。程序員
日期是商業邏輯計算的一個關鍵部分,任何企業的程序都須要正確的處理日期時間問題,不然極可能帶來事故和損失。爲此本系列僅着眼於這一個點就寫了好幾篇文章,目的是幫助你係統化的搞定全部問題/難題。sql
平時咱們都熱衷於吐槽同事的代碼有多爛,今天咱們就來玩點狠的:吐槽吐槽JDK,看看它的日期時間API設計獲得底有多爛。編程
說明:本文指的日期時間API是Date/Calendar系列,而非Java 8新的API。畢竟通常咱們稱後者爲JSR 310日期時間,請注意區分哈安全
本文提綱
誠然,Java的API絕大多數設計得都是很是優秀且成功的,不然Java也不可能成爲編程語言界的常青藤,而且還常年霸榜。可是,JDK也有失手的地方,存在設計得很是爛的API,先來了解下。架構
談到對Java API不滿意程度的調研,最出名的當屬2010年國外一個大佬Tiago Fernandez發起的一個頗有意思的投票,投票結果的數據統計圖表以下:app
對橫向標題欄的各個單詞解釋一下,從左到右依次爲:dom
計算最終得分的公式爲:編程語言
Score = (I can live with) + (Painful * 2) + (Crappy * 3) + (Hellish * 4)
按照此公式,計算出各API的得分,畫成直方圖直觀的展現出來:ide
好,排名出來了。從最爛 -> 最好的名次依次爲:
爛歸爛,想想什麼樣的爛API對你的產生影響會是最大的呢?答:很經常使用卻很爛的。假若一個API設計得很爛但你不多用或者幾乎不用接觸,你也不會對它產生很大厭惡感。打個比方,一堆屎自己很臭,但若你並不須要走到它身旁也就聞不到,天然就不會以爲它有多礙眼了。
回到這個統計結果來,EJB 2.x的API設計得最爛這個結果無可厚非,但站在時間維度的如今(2021年)回頭來看,是能夠徹底忽略它了,畢竟如今的咱們絕無可能再接觸到它,再爛又有何干呢?
EJB 2.x這個老古董,相信在看文章的絕大部分同窗都沒見過甚至沒聽過它吧,A哥2015年入行,一上來Spring 4.x嘎嘎就是幹,從未接觸過EJB。
說明:這個統計是2010年作的,那會EJB2.x的使用量還比較大,所以上了「榜首」
XML/DOM設計得也很差,但已徹底被第三庫(如dom4j)取代,後者成爲了事實的標準;AWT/Swing是市場的抉擇,你用Java開發界面纔會用到,不然不會接觸,屬於正常。
最後再看「屈居」第二名的Date/Time/Calendar日期時間API,它就不得了了。畢竟此API有個很大的特色:哪怕到了如今(2021年)依舊很是經常使用。因此,它設計得爛帶來的實際影響是蠻大的。
下面就來具體瞭解下它有哪些坑爹的設計和槽點,一塊兒不吐不快。
java.util.Date被設計爲日期 + 時間的結合體。也就是說若是隻須要日期,或者只須要單純的時間,用Date是作不到的。
@Test public void test1() { System.out.println(new Date()); } 輸出: Fri Jan 22 00:25:06 CST 2021
這就致使語義很是的不清晰,好比說:
/** * 是不是假期 */ private static boolean isHoliday(Date date){ return ...; }
判斷某一天是不是假期,只和日期有關,和具體時間沒有關係。若是代碼這樣寫語義只能靠註釋解釋,方法自己沒法達到自描述的效果,也沒法經過強類型去約束,所以容易出錯。
說明:本文全部例子不考慮時區問題,下同
@Test public void test2() { Date date = new Date(); System.out.println("當前日期時間:" + date); System.out.println("年份:" + date.getYear()); System.out.println("月份:" + date.getMonth()); } 輸出: 當前日期時間:Fri Jan 22 00:25:16 CST 2021 年份:121 月份:0
what?年份是121年,這什麼鬼?月份返回0,這又是什麼鬼?
無奈,看看這兩個方法的Javadoc:
尼瑪,原來 2021 - 1900 = 121是這麼來的。那麼問題來了,爲什麼是1900這個數字呢?
月份,居然從0開始,這是學的誰呢?簡直打破了我認爲的只有index索引值纔是從0開始的認知啊,這種作法很是的不符合人類思惟有木有。
索引值從0開始就算了,畢竟那是給計算機看的無所謂,可是你這月份主要是給人看的呀
oh my god,也就是說我把一個Date日期時間對象傳給你,你居然還能給我改掉,真是太沒安全感可言了。
@Test public void test() { Date currDate = new Date(); System.out.println("當前日期是①:" + currDate); boolean holiday = isHoliday(currDate); System.out.println("是不是假期:" + holiday); System.out.println("當前日期是②:" + currDate); } /** * 是不是假期 */ private static boolean isHoliday(Date date) { // 架設等於這一天才是假期,不然不是 Date holiday = new Date(2021 - 1900, 10 - 1, 1); if (date.getTime() == holiday.getTime()) { return true; } else { // 模擬寫代碼時不注意,使壞 date.setTime(holiday.getTime()); return true; } } 輸出: 當前日期是①:Fri Jan 22 00:41:59 CST 2021 是不是假期:true 當前日期是②:Fri Oct 01 00:00:00 CST 2021
我就像讓你幫我判斷下遮天是不是假期,而後你居然連個人日期都給我改了?過度了啊。這是多麼可怕的事,存在重大安全隱患有木有。
針對這種case,通常來講咱們函數內部操做的參數只能是副本:要麼調用者傳進來的就是副本,要麼內部本身生成一個副本。
在本利中提升程序健壯性只需在isHoliday首行加入這句代碼便可:
private static boolean isHoliday(Date date) { date = (Date) date.clone(); ... }
再次運行程序,輸出:
當前日期是①:Fri Jan 22 00:44:10 CST 2021 是不是假期:true 當前日期是②:Fri Jan 22 00:44:10 CST 2021
bingo。
可是呢,Date做爲高頻使用的API,並不能要求每一個程序員都有這種安全意識,畢竟即便百密也會有一疏。因此說,把Date設計爲一個可變的類是很是糟糕的設計。
來,看看java.util.Date類的繼承結構:
它的三個子類均處於java.sql包內。且先不談這種垮包繼承的合理性問題,直接看下面這個使用例子:
@Test public void test3() { // 居然尚未空構造器 // java.util.Date date = new java.sql.Date(); java.util.Date date = new java.sql.Date(System.currentTimeMillis()); // 按到當前的時分秒 System.out.println(date.getHours()); System.out.println(date.getMinutes()); System.out.println(date.getSeconds()); }
運行程序,暴雷了:
java.lang.IllegalArgumentException at java.sql.Date.getHours(Date.java:187) at com.yourbatman.formatter.DateTester.test3(DateTester.java:65) ...
what?又是一打破認知的結果啊,第一句getHours()就報錯啦。走進java.sql.Date的方法源碼進去一看,握草重寫了父類方法:
還有這麼重寫父類方法的?還有王法嗎?這也算是JDK能幹出來的事?赤裸裸的違背里氏替換原則等衆多設計原則,子類能力居然比父類小,使用起來簡直讓人云裏霧裏。
java.util.Date的三個子類均位於java.sql包內,他們三是經過Javadoc描述來進行分工的:
這麼一來,彷佛能夠「理解」java.sql.Date爲什麼重寫父類的getHours()方法改成拋出IllegalArgumentException異常了,畢竟它只能表示日期嘛。可是這種經過繼承再閹割的實現手法大家接受得了?反正我是不能的~
由於日期時間的特殊性,不一樣的國家地區在同一時刻顯示的日期時間應該是不同的,但Date作不到,由於它底層代碼是這樣的:
也就是說它表示的是一個具體時刻(時間戳),這個數值放在全球任何地方都是如出一轍的,也就是說new Date()和System.currentTimeMillis()沒啥兩樣。
JDK提供了TimeZone表示時區的概念,但它在Date裏並沒有任何體現,只能使用在格式化器上,這種設計着實讓我再一次看不懂了。
關於Date的格式化,站在架構設計的角度來看,首先不得不吐槽的是Date明明屬於java.util包,那麼它的格式化器DateFormat爲毛卻跑到java.text裏去了呢?這種依賴管理的什麼鬼?是否是有點太過於隨意了呢?
另外,JDK提供了一個DateFormat的子類實現SimpleDateFormat專門用於格式化日期時間。可是它卻被設計爲了線程不安全的,一個定位爲模版組件的API居然被設計爲線程不安全的類,實屬瞎整。
就由於這個坑的存在,讓多少初中級工程師淚灑職場,算了說多了都是淚。另外,由於線程不安全問題並不是必現問題,所以在黑盒/白盒測試、功能測試階段均可能測不出來,留下潛在風險。
這就是「靈異事件」:測試環境測試得好好的,爲什麼到線上就出問題了呢?
從JDK 1.1 開始,Java日期時間API彷佛進步了些,引入了Calendar類,而且對職責進行了劃分:
有了Calendar後,原有Date中的大部分方法均標記爲廢棄,交由Calendar代替。
Date終於單純了些:只須要展現日期時間而無需再顧及年月日操做、格式化操做等等了。值得注意的是,這些方法只是被標記爲過時,並未刪除。即使如此,請在實際開發中也必定不要使用它們。
引入了一個Calendar彷佛分離了職責,但Calendar難當大任,設計上依舊存在不少問題。
@Test public void test4() { Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); calendar.set(2021, 10, 1); // -> 依舊是可變的 System.out.println(calendar.get(Calendar.YEAR)); System.out.println(calendar.get(Calendar.MONTH)); System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); } 輸出: 2021 10 1
年月日的處理上彷佛能夠接受沒有問題了。從結果中能夠發現,Calendar年份的傳值不用再減去1900了,這和Date是不同的,不知道這種行爲不一致會不會讓有些人抓狂。
說明:Calendar相關的API是由IBM捐過來的,因此和Date不同貌似也「情有可原」
另外,還有個重點是Calendar依舊是可變的,因此存在不安全因素,參與計算改變值時請使用其副本變量。
總的來講,Calendar在Date的基礎上作了改善,但僅限於修修補補,並未從根本上解決問題。最重要的是Calendar的API使用起來真的很不方便,並且該類在語義上也徹底不符合日期/時間的含義,使用起來更顯尷尬。
總之,不管是Date,仍是Calendar,仍是格式化DateFormat都用着太方便,且存在各式各樣的安全隱患、線程安全問題等等,這是API沒有設計好的地方。
日期時間API屬於基礎API,在各個語言中都是必備的。然而不只僅是Java面臨着API設計很爛的處境,有些其它流行語言同樣如此,涌現出1個(1堆)三方庫比乙方庫設計更好的狀況,好比:
因此說,Java它並不孤單(自我安慰一把)
由於原生的Date日期時間體系存在「七宗罪」,催生了第三方Java日期時間庫的誕生,如大名鼎鼎的Joda-Time的流行甚至一度成爲標配。
對於Java來講,如此重要的API模塊豈能被第三方庫給佔據,開發者本就想簡單的處理個日期時間還得導入第三方庫,使用也太不方便了吧。當時的Java如日中天,所以就開啓了「收編」Joda-Time之旅。
2013年9月份,具備劃時代意義的Java 8大版本正式發佈,該版本帶來了很是多的新特性,其中最引入矚目之一即是全新的日期時間API:JSR 310。
JSR 310規範的領導者是Stephen Colebourne,此人也是Joda-Time的締造者。不客氣的說JSR 310是在Joda-Time的基礎上創建的,參考了其絕大部分的API實現,所以若你以前是Joda-Time的重度使用者,如今遷移到Java 8原生的JSR 310日期時間上來幾乎無縫。
即使這樣,也並不能說JSR 310就徹底等於Joda-Time的官方版本,仍是有些許詫異的,例舉以下:
簡單感覺下JSR 310 API:
@Test public void test5() { System.out.println(LocalDate.now(ZoneId.systemDefault())); System.out.println(LocalTime.now(ZoneId.systemDefault())); System.out.println(LocalDateTime.now(ZoneId.systemDefault())); System.out.println(OffsetTime.now(ZoneId.systemDefault())); System.out.println(OffsetDateTime.now(ZoneId.systemDefault())); System.out.println(ZonedDateTime.now(ZoneId.systemDefault())); System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now())); System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now())); System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now())); }
JSR 310的全部對象都是不可變的,因此線程安全。和老的日期時間API相比,最主要的特徵對好比下:
JSR 310 | Date/Calendar | 說明 |
---|---|---|
流暢的API | 難用的API | API設計的好壞最直接影響編程體驗,前者大大大大優於後者 |
實例不可變 | 實例可變 | 對於日期時間實例,設計爲可變確實不合理也不安全。都不敢放心的傳遞給其它函數使用 |
線程安全 | 線程不安全 | 此特性直接決定了編碼方式和健壯性 |
關於JSR 310日期時間更多介紹此處就不展開了,畢竟前面文章囉嗦過好屢次了。總之它是Java的新一代日期時間API,設計得很是好,幾乎沒有缺點可言,可用於100%替代老的日期時間API。
若是你到如今2021年了還沒擁抱它,那麼請問你還在等啥呢?
日期時間API由於過於經常使用,所以你可能都以爲它絕不起眼。坦白的說,若是你沒有複雜的日期時間需求要處理,如涉及到時區、偏移量、跨時區轉換、國際化顯示等等,那麼可能以爲Date也能將就。
若是你不想作個將就的人,若是你想擁有更好的日期時間編程體驗,棄用Date,擁抱JSR 310吧。
本文所屬專欄:JDK日期時間,後臺回覆專欄名便可獲取所有內容。本文已被https://www.yourbatman.cn收錄。
看完了不必定懂,看懂了不必定會。來,文末3個思考題幫你覆盤:
System.out.println("點個贊吧!"); print_r('關注【BAT的烏托邦】!'); var_dump('點個贊吧!'); NSLog(@"關注【BAT的烏托邦】!"); console.log("點個贊吧!"); print("關注【BAT的烏托邦】!"); printf("點個贊吧!"); cout << "關注【BAT的烏托邦】!" << endl; Console.WriteLine("點個贊吧!"); fmt.Println("關注【BAT的烏托邦】!"); Response.Write("點個贊吧!"); alert("關注【BAT的烏托邦】!"); echo("點個贊吧!");
做者簡介:A哥(YourBatman),Spring Framework/Boot開源貢獻者,Java架構師,愛分享。很是注重基本功修養,底層基礎決定上層建築,才能煥發程序員更強生命力。很是擅長結構化講述專題,抽絲剝繭頗具深度。這些專題也許可能大概是全網最好或獨一份哦,歡迎自取。