語法糖
Java語法糖系列,所以首先講講什么是語法糖。語法糖是一種幾乎每種語言或多或少都提供過的一些方便程序員開發代碼的語法,它只是編譯器實現的一些小把戲罷了,編譯期間以特定的字節碼或者特定的方式對這些語法做一些處理,開發者就可以直接方便地使用了。這些語法糖雖然不會提供實質性的功能改進,但是它們或能提高性能、或能提升語法的嚴謹性、或能減少編碼出錯的機會。Java提供給了用戶大量的語法糖,比如泛型、自動裝箱、自動拆箱、foreach循環、變長參數、內部類、枚舉類、斷言(assert)等
斷言(assert)
開啟斷言
-
用java 命令在console 下直接運行class 文件,跟 -ea 啟動參數即可
參考文章
-
單獨給某個程序制定運行參數
-
給整個java 運行環境配置默認參數
一、
語法形式
Java2在1.4中新增了一個關鍵字:assert。在程序開發過程中使用它創建一個斷言(assertion),它的
語法形式有如下所示的兩種形式:
1、assert condition;
這里condition是一個必須為真(true)的表達式。如果表達式的結果為true,那么斷言為真,並且無任何行動
如果表達式為false,則斷言失敗,則會拋出一個AssertionError對象。這個AssertionError繼承於Error對象,
而Error繼承於Throwable,Error是和Exception並列的一個錯誤對象,通常用於表達系統級運行錯誤。
2、asser condition:expr;
這里condition是和上面一樣的,這個冒號后跟的是一個表達式,通常用於斷言失敗后的提示信息,說白了,它是一個傳到AssertionError構造函數的值,如果斷言失敗,該值被轉化為它對應的字符串,並顯示出來。
使用示例:
語法改進
Foreach與變長參數
Foreach和邊長參數都是語法糖。
For Array循環是標准的數組下標循環的語法糖。
For Collection 是迭代器的語法糖。
可變長參數,每次都會初始化一個參數長度數組,並申請內存和賦值。在多次調用的情況這個消耗是沒有必要且明顯的。應當根據提供固定參數個數的方法。
關於返回值的問題。
基本類型拆箱裝箱
true | false的原因
Integer的享元設計,上限取決於業務數字使用范圍。
空指針原因是integer.intValue();這是個對象方法
內部類與資源自動管理
字節碼文件中沒有其他類信息,只有本類方法和類描述,內部類表
自動關閉資源在字節碼文件中還是生成了close方法
泛型(Generic)
支持創建可以按類型進行參數化的類.可以把類型參數看作是使用參數類型時指定的類型占位符,泛型能保證大型應用程序的類型安全和良好的維護性。Java泛型的實現是語法糖,在編譯完成后並沒有保留參數化類型的信息。因此你可以通過反射獲取后添加非泛型數據,當然一般不會這么做。泛型的好處在於:可以定義一類數據集合,進行相同的操作,如果非泛型成員,在編譯時候就可以檢測;可以在聲明時指定具體類型,這樣避免寫過多的子類。
泛型可以在方法和類(接口、抽象類)上聲明。語法為<T> ,T表示泛型,可以是任意字母,一般T便是type類型,E表示元素等。在運行時T已經確定。
數組是協變的,例如:參數類型為Object[],表示可以傳任意數組。如果Sub為Super的子類型,那么數組類型Sub[]就是Super[]的子類型。List<Sub>與List<Super>並沒有什么關系。
如果進行類型轉換,數組在運行時才知道具體的類型,這會導致ArrayStoreException異常,而泛型在編譯期間檢查錯誤,確保為某一類型。
泛型的運行時擦除,導致不能定義泛型數組。因為運行時並不保存泛型類型信息。
泛型用法
List<E>、List<?>與原生List。
泛型的上限與下限
枚舉
創建枚舉類型要使用 enum 關鍵字,隱含了所創建的類型都是 java.lang.Enum 類的子類(java.lang.Enum 是一個抽象類)。枚舉類型符合通用模式 Class Enum<E extends Enum<E>>,而 E 表示枚舉類型的名稱。枚舉類型的每一個值都將映射到 protected Enum(String name, int ordinal) 構造函數中,在這里,每個值的名稱都被轉換成一個字符串,並且序數設置表示了此設置被創建的順序。但是並不推薦顯示調用序數作為枚舉映射列表。
簡介
這個例子描述人類的性別,人類的性別記錄應該為三種男、女、不確定。枚舉的元素是確定的而且在加載類的時候就會初始化為一個對象。枚舉對象跟普通的類對象,沒有任何區別。你可以增加一個方法,做一些操作。我們一般利用枚舉對象是特定做一些除了數據枚舉外的高級操作。
枚舉中可以只有枚舉屬性。
每個枚舉對象會走對應的構造方法。不能new 枚舉對象。通過枚舉的官方定義,枚舉是按照創建順序的。
應用
在使用int常量作為枚舉是,我們通過大寫的名稱對應相應的int值,表述此類使用的多個常量。不同類型的int枚舉類型可以用命名前綴來區分。這種做法性能好,省力,可讀性也很好。但它作為人為的口頭約束。如果沒有了解約束,放任何int值是可能的,甚至用枚舉做運算。外部調用時可能出錯。再者,int枚舉類型並不能很好的表述含義,它至少一個0,1,2,4等等。你不知道它的名稱。
Java的枚舉類型,即天然是int值類型,又支持名稱的意義。而且枚舉作為類,你可以非常方便的拓展需要的成員或者方法。枚舉final並不提供實例化的構造器,讓它完全避免了反射等可能造成的對象實例受限破壞。枚舉的值為順勢的transient,序列化不會造成枚舉常量實例多份的危險。設計非常巧妙。
如果需要用枚舉做映射,實現高性能的類似位圖的枚舉映射數據結構可以參考EnumSet<E extends Enum<E>>、EnumMap<K extends Enum<K>,V>。
語法糖的實現
初始化,枚舉三原色,成員變量name,index
按需要定義普通方法
枚舉中關鍵的屬性
輸出結果
利用枚舉對象實例的確定性質,枚舉元素只有一個就是一個規范的單例,你可以按需定義成員。此種方式可以保證該單例線程安全、防反射攻擊、防止序列化生成新的實例。
枚舉元素的初始化過程
枚舉類編譯后的類中沒有任何構造方法。自然沒辦法進行反射new對象。下面是普通對象的構造方法。
枚舉禁用clone保證唯一性
關於序列化的問題
Java枚舉序列化不會把枚舉對象序列化,只會序列化枚舉的名字,反序列化會把常量作為參數。來保證只有枚舉對象的唯一性。1.12 Serialization of Enum Constants
枚舉的擴展
為了方便使用,在接口中添加default方法,需要jdk1.8
這個寫法調用兩次PLUS,但在編譯期間可以檢測實例是否是枚舉和Operation接口類型。
Lambda
本文介紹了Java SE 8中新引入的lambda語言特性以及這些特性背后的設計思想。這些特性包括:
-
lambda 表達式(又被成為"閉包"或"匿名方法")
-
方法引用和構造方法引用
-
擴展的目標類型和類型推導
-
接口中的默認方法和靜態方法
1. 背景
Java是一門面向對象編程語言。面向對象編程語言和函數式編程語言中的基本元素(Basic Values)都可以動態封裝程序行為:面向對象編程語言使用帶有方法的對象封裝行為,函數式編程語言使用函數封裝行為。但這個相同點並不明顯,因為Java的對象往往比較"重量級":實例化一個類型往往會涉及不同的類,並需要初始化類里的字段和方法。不過有些Java對象只是對單個函數的封裝。
Lambda表達式是一段可以傳遞的代碼
場景一:線程邏輯
場景二:自定義比較器
場景三:事件按鈕回調
幻
2. 函數式接口(Functional interfaces)
可以通過@FunctionalInterface注解來顯式指定一個接口是函數式接口。 我們選擇了"使用已知類型"這條路——因為現有的類庫大量使用了函數式接口,通過沿用這種模式,我們使得現有類庫能夠直接使用lambda表達式。例如下面是Java SE 7中已經存在的函數式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.beans.PropertyChangeListener
除此之外,Java SE 8中增加了一個新的包:java.util.function,它里面包含了常用的函數式接口,例如:
- Predicate<T>——接收T對象並返回boolean
- Consumer<T>——接收T對象,不返回值
- Function<T, R>——接收T對象,返回R對象
- Supplier<T>——提供T對象(例如工廠),不接收值
- UnaryOperator<T>——接收T對象,返回T對象
- BinaryOperator<T>——接收兩個T對象,返回T對象
- IntSupplier,LongBinaryOperator——原始類型(Primitive type)的特化(Specialization)函數式接口
- BiFunction<T, U, R>——接收T對象和U對象,返回R對象
匿名類型最大的問題就在於其冗余的語法。有人戲稱匿名類型導致了"高度問題"(height problem),lambda表達式是匿名方法,它提供了輕量級的語法,從而解決了匿名內部類帶來的"高度問題"。
下面是一些lambda表達式:
第一個lambda表達式接收x和y這兩個整形參數並返回它們的和;第二個lambda表達式不接收參數,返回整數'42';第三個lambda表達式接收一個字符串並把它打印到控制台,不返回值。
lambda表達式的語法由參數列表、箭頭符號->和函數體組成。函數體既可以是一個表達式,也可以是一個語句塊:
- 表達式:表達式會被執行然后返回執行結果。
-
語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣——
- return語句會把控制權交給匿名方法的調用者
- break和continue只能在循環中使用
- 如果函數體有返回值,那么函數體內部的每一條路徑都必須返回值
表達式函數體適合小型lambda表達式,它消除了return關鍵字,使得語法更加簡潔。
lambda表達式也會經常出現在嵌套環境中,比如說作為方法的參數。為了使lambda表達式在這些場景下盡可能簡潔,我們去除了不必要的分隔符。不過在某些情況下我們也可以把它分為多行,然后用括號包起來,就像其它普通表達式一樣。
下面是一些出現在語句中的lambda表達式:
3. 目標類型(Target typing)
對於給定的lambda表達式的類型是由其上下文推導而來,這符合
這就意味着同樣的lambda表達式在不同上下文里可以擁有不同的類型:
第一個lambda表達式() -> "done"是Callable的實例,而第二個lambda表達式則是PrivilegedAction的實例。
編譯器負責推導lambda表達式的類型。它利用lambda表達式所在上下文所期待的類型進行推導,這個被期待的類型被稱為目標類型。lambda表達式只能出現在目標類型為函數式接口的上下文中。
當然,lambda表達式對目標類型也是有要求的。編譯器會檢查lambda表達式的類型和目標類型的方法簽名(method signature)是否一致。當且僅當下面所有條件均滿足時,lambda表達式才可以被賦給目標類型T:
- T是一個函數式接口
- lambda表達式的參數和T的方法參數在數量和類型上一一對應
- lambda表達式的返回值和T的方法返回值相兼容(Compatible)
-
lambda表達式內所拋出的異常和T的方法throws類型相兼容
由於目標類型(函數式接口)已經"知道"lambda表達式的形式參數(Formal parameter)類型,所以我們沒有必要把已知類型再重復一遍。也就是說,lambda表達式的參數類型可以從目標類型中得出:
在上面的例子里,編譯器可以推導出s1和s2的類型是String。此外,當lambda的參數只有一個而且它的類型可以被推導得知時,該參數列表外面的括號可以被省略:
在前三個上下文(變量聲明、賦值和返回語句)里,目標類型即是被賦值或被返回的類型:
數組初始化器和賦值類似,只是這里的"變量"變成了數組元素,而類型是從數組類型中推導得知:
方法參數的類型推導要相對復雜些:目標類型的確認會涉及到其它兩個語言特性:重載解析(Overload resolution)和參數類型推導(Type argument inference)。
重載解析會為一個給定的方法調用(method invocation)尋找最合適的方法聲明(method declaration)。由於不同的聲明具有不同的簽名,當lambda表達式作為方法參數時,重載解析就會影響到lambda表達式的目標類型。編譯器會通過它所得之的信息來做出決定。如果lambda表達式具有顯式類型(參數類型被顯式指定),編譯器就可以直接 使用lambda表達式的返回類型;如果lambda表達式具有隱式類型(參數類型被推導而知),重載解析則會忽略lambda表達式函數體而只依賴lambda表達式參數的數量。
如果在解析方法聲明時存在二義性(ambiguous),我們就需要利用轉型(cast)或顯式lambda表達式來提供更多的類型信息。如果lambda表達式的返回類型依賴於其參數的類型,那么lambda表達式函數體有可能可以給編譯器提供額外的信息,以便其推導參數類型。
在上面的代碼中,ps的類型是List<Person>,所以ps.stream()的返回類型是Stream<Person>。map()方法接收一個類型為Function<T, R>的函數式接口,這里T的類型即是Stream元素的類型,也就是Person,而R的類型未知。由於在重載解析之后lambda表達式的目標類型仍然未知,我們就需要推導R的類型:通過對lambda表達式函數體進行類型檢查,我們發現函數體返回String,因此R的類型是String,因而map()返回Stream<String>。絕大多數情況下編譯器都能解析出正確的類型,但如果碰到無法解析的情況,我們則需要:
- 使用顯式lambda表達式(為參數p提供顯式類型)以提供額外的類型信息
- 把lambda表達式轉型為Function<Person, String>
- 為泛型參數R提供一個實際類型。(.<String>map(p -> p.getName()))
lambda表達式本身也可以為它自己的函數體提供目標類型,也就是說lambda表達式可以通過外部目標類型推導出其內部的返回類型,這意味着我們可以方便的編寫一個返回函數的函數:
類似的,條件表達式可以把目標類型"分發"給其子表達式:
在無法確認目標類型時,轉型表達式(Cast expression)可以顯式提供lambda表達式的類型:
除此之外,當重載的方法都擁有函數式接口時,轉型可以幫助解決重載解析時出現的二義性。
目標類型這個概念不僅僅適用於lambda表達式,泛型方法調用和"菱形"構造方法調用也可以從目標類型中受益,下面的代碼在Java SE 7是非法的,但在Java SE 8中是合法的:
局部變量
基於詞法作用域的理念,lambda表達式所在上下文中的局部變量不可以重復定義。
在Java SE 7中,編譯器對內部類中引用的外部變量(即捕獲的變量)要求被聲明為final。對於lambda表達式和內部類,我們允許在其中捕獲那些符合有效只讀(Effectively final)的局部變量。
如果一個局部變量在初始化后從未被修改過,那么它就符合有效只讀的要求,換句話說,加上final后也不會導致編譯錯誤的局部變量就是有效只讀變量。
為什么要禁止這種行為呢?因為這樣的lambda表達式很容易引起race condition。lambda expressions close over values, not variables:
lambda表達式不支持修改捕獲變量的另一個原因是我們可以使用更好的方式來實現同樣的效果:使用規約(reduction)。java.util.stream包提供了各種通用的和專用的規約操作(例如sum、min和max),就上面的例子而言,我們可以使用規約操作(在串行和並行下都是安全的)來代替forEach:
sum()等價於下面的規約操作:
規約需要一個初始值(以防輸入為空)和一個操作符(在這里是加號),然后用下面的表達式計算結果:
規約也可以完成其它操作,比如求最小值、最大值和乘積等等。如果操作符具有可結合性(associative),那么規約操作就可以容易的被並行化。所以,與其支持一個本質上是並行而且容易導致race condition的操作,我們選擇在庫中提供一個更加並行友好且不容易出錯的方式來進行累積(accumulation)。
4. 方法引用(Method references)
方法引用有很多種,它們的語法如下:
- 靜態方法引用:ClassName::methodName
- 實例上的實例方法引用:instanceReference::methodName
- 超類上的實例方法引用:super::methodName
- 類型上的實例方法引用:ClassName::methodName
- 構造方法引用:Class::new
- 數組構造方法引用:TypeName[]::new
構造方法也可以通過new關鍵字被直接引用:
數組的構造方法引用的語法則比較特殊,為了便於理解,你可以假想存在一個接收int參數的數組構造方法。參考下面的代碼:
5. 默認方法和靜態接口方法(Default and static interface methods)
向函數式接口里增加默認方法,所有實現接口的類都自動繼承這個默認方法並調用。
下面的例子展示了如何向Iterator接口增加默認方法skip:
除了默認方法,Java SE 8還在允許在接口中定義靜態方法。這使得我們可以從接口直接調用和它相關的輔助方法(Helper method),而不是從其它的類中調用(之前這樣的類往往以對應接口的復數命名,例如Collections)。比如,我們一般需要使用靜態輔助方法生成實現Comparator的比較器,在Java SE 8中我們可以直接把該靜態方法定義在Comparator接口中:
比如說下面的代碼:
冗余代碼實在太多了!
有了lambda表達式,我們可以去掉冗余的匿名類:
盡管代碼簡潔了很多,但它的抽象程度依然很差:開發者仍然需要進行實際的比較操作(而且如果比較的值是原始類型那么情況會更糟),所以我們要借助Comparator里的comparing方法實現比較操作:
在類型推導和靜態導入的幫助下,我們可以進一步簡化上面的代碼:
我們注意到這里的lambda表達式實際上是getLastName的代理(forwarder),於是我們可以用方法引用代替它:
最后,使用Collections.sort這樣的輔助方法並不是一個好主意:它不但使代碼變的冗余,也無法為實現List接口的數據結構提供特定(specialized)的高效實現,而且由於Collections.sort方法不屬於List接口,用戶在閱讀List接口的文檔時不會察覺在另外的Collections類中還有一個針對List接口的排序(sort())方法。
默認方法可以有效的解決這個問題,我們為List增加默認方法sort(),然后就可以這樣調用:
此外,如果我們為Comparator接口增加一個默認方法reversed()(產生一個逆序比較器),我們就可以非常容易的在前面代碼的基礎上實現降序排序。
Lambda(類庫篇——Streams API,Collector和並行)
對於 anyMatch(Predicate) 和 findFirst() 這些急性求值操作,我們可以使用短路(short-circuiting)來終止不必要的運算。以下面的流水線為例:
盡管並行是顯式的,但它並不需要成為侵入式的。利用 parallelStream() ,我們可以輕松的把之前重量求和的代碼並行化:
下面的代碼源自JDK中的 Class 類型( getEnclosingMethod 方法),這段代碼會遍歷所有聲明的方法,然后根據方法名稱、返回類型以及參數的數量和類型進行匹配:
通過使用流,我們不但可以消除上面代碼里面所有的臨時變量,還可以把控制邏輯交給類庫處理。通過反射得到方法列表之后,我們利用 Arrays.stream 將它轉化為 Stream ,然后利用一系列過濾器去除類型不符、參數不符以及返回值不符的方法,然后通過調用 findFirst 得到 Optional<Method> ,最后利用 orElseThrow 返回目標值或者拋出異常。
相對於未使用流的代碼,這段代碼更加緊湊,可讀性更好,也不容易出錯。
流操作特別適合對集合進行查詢操作。假設有一個"音樂庫"應用,這個應用里每個庫都有一個專輯列表,每張專輯都有其名稱和音軌列表,每首音軌表都有名稱、藝術家和評分。
假設我們需要得到一個按名字排序的專輯列表,專輯列表里面的每張專輯都至少包含一首四星及四星以上的音軌,為了構建這個專輯列表,我們可以這么寫:
我們可以用流操作來完成上面代碼中的三個主要步驟——識別一張專輯是否包含一首評分大於等於四星的音軌(使用 anyMatch );按名字排序;以及把滿足條件的專輯放在一個 List 中:
可變的集合操作(Mutative collection operation)
集合上的流操作一般會生成一個新的值或集合。不過有時我們希望就地修改集合,所以我們為集合(例如 Collection , List 和 Map )提供了一些新的方法,比如 Iterable.forEach(Consumer) , Collection.removeAll(Predicate) , List.replaceAll(UnaryOperator) , List.sort(Comparator) 和 Map.computeIfAbsent()。除此之外, ConcurrentMap 中的一些非原子方法(例如 replace 和 putIfAbsent)被提升到 Map 之中。
函數式編程簡介
如果你不知道什么是函數式編程,或者不了解map,filter,reduce這些常用的高階函數。下文是簡單介紹。或者找專業資料查閱。
高階函數:一個函數就接收另一個函數作為參數,這種函數就稱之為高階函數
1.高階函數之map:
此時我們有一個數組和一個接受一個參數並返回一個數的函數。我們需要把這個數組的每一個值在這個函數上走一遍,從而得到一個新數組。此時就需要map了
2.高階函數之reduce:
此時我們有一個數組和一個接受兩個參數並返回一個數的函數。我們需要把這個數組的每兩個值在這個函數上走一遍變成一個值,然后再讓這個值繼續和下一個值走這個函數,最后從而得到一個值。
3.高階函數之filter:
此時我們有一個數組,這個數組里面有我們想要的也有我們不想要的,怎么辦,我們可以下一個函數,讓這些值在這個函數里面走一遍,想要的留下,不想要的去掉,返回一個只有理想數值的數組。此時需要filter
4.高階函數之sort:
這個就是之前數組里面提到的排序函數,這個也是一個高級函數,默認是從低到高。 通常規定,對於兩個元素x和y,如果認為x < y,則返回-1,如果認為x == y,則返回0,如果認為x > y,則返回1,這樣,排序算法就不用關心具體的比較過程,而是根據比較結果直接排序。我們可以傳入一個函數,讓sort從高到低排序