轉自:https://blog.csdn.net/qiubabin/article/details/70256683
下面對幾個常用的特性做下重點說明。
一、Lambda表達式
1.1 函數式編程
百科介紹:http://baike.baidu.com/link?url=LL9X3-SoS4XJGgdzrXvURuKEGm6ad5zY1NLDxDygjTaSRnEZ0Bp3wqX0QgkB7fjPwMSQS1tLfqdRMKUhNti7MH7DEK7JQ_lXcs9k6LXHT1A9dSJW8uwJMONJcvXY53h6myMCkqjL3IqW8QRgbdNDl_
函數編程非常關鍵的幾個特性如下:
(1)閉包與高階函數
函數編程支持函數作為第一類對象,有時稱為 閉包或者 仿函數(functor)對象。實質上,閉包是起函數的作用並可以像對象一樣操作的對象。
與此類似,FP 語言支持 高階函數。高階函數可以用另一個函數(間接地,用一個表達式) 作為其輸入參數,在某些情況下,它甚至返回一個函數作為其輸出參數。這兩種結構結合在一起使得可以用優雅的方式進行模塊化編程,這是使用 FP 的最大好處。
(2)惰性計算
在惰性計算中,表達式不是在綁定到變量時立即計算,而是在求值程序需要產生表達式的值時進行計算。延遲的計算使您可以編寫可能潛在地生成無窮輸出的函數。因為不會計算多於程序的其余部分所需要的值,所以不需要擔心由無窮計算所導致的 out-of-memory 錯誤。
(3)沒有“副作用”
所謂"副作用"(side effect),指的是函數內部與外部互動(最典型的情況,就是修改全局變量的值),產生運算以外的其他結果。函數式編程強調沒有"副作用",意味着函數要保持獨立,所有功能就是返回一個新的值,沒有其他行為,尤其是不得修改外部變量的值。
綜上所述,函數式編程可以簡言之是: 使用不可變值和函數, 函數對一個值進行處理, 映射成另一個值。這個值在面向對象語言中可以理解為對象,另外這個值還可以作為函數的輸入。
1.2 Lambda表達式
1.2.1 語法
完整的Lambda表達式由三部分組成:參數列表、箭頭、聲明語句;
(Type1 param1, Type2 param2, ..., TypeN paramN) -> { statment1; statment2; //............. return statmentM;}
1. 絕大多數情況,編譯器都可以從上下文環境中推斷出lambda表達式的參數類型,所以參數可以省略:
(param1,param2, ..., paramN) -> { statment1; statment2; //............. return statmentM;}
2、 當lambda表達式的參數個數只有一個,可以省略小括號:
param1 -> { statment1; statment2; //............. return statmentM;}
3、 當lambda表達式只包含一條語句時,可以省略大括號、return和語句結尾的分號:
param1 -> statment
這個時候JVM會自動計算表達式值並返回,另外這種形式還有一種更簡寫法,方法引用寫法,具體可以看下面的方法引用的部分。
1.2.2 函數接口
函數接口是只有一個抽象方法的接口, 用作 Lambda 表達式的返回類型。
接口包路徑為java.lang.function,然后接口類上面都有@FunctionalInterface這個注解。下面列舉幾個較常見的接口類。
這些函數接口在使用Lambda表達式時做為返回類型,JDK定義了很多現在的函數接口,實際自己也可以定義接口去做為表達式的返回,只是大多數情況下JDK定義的直接拿來就可以用了。而且這些接口在JDK8集合類使用流操作時大量被使用。
1.2.3 類型檢查、類型推斷
Java編譯器根據 Lambda 表達式上下文信息就能推斷出參數的正確類型。 程序依然要經過類型檢查來保證運行的安全性, 但不用再顯式聲明類型罷了。 這就是所謂的類型推斷。Lambda 表達式中的類型推斷, 實際上是 Java 7 就引入的目標類型推斷的擴展。
有時候顯式寫出類型更易讀,有時候去掉它們更易讀。沒有什么法則說哪種更好;對於如何讓代碼更易讀,程序員必須做出自己的選擇。
1.2.4 局部變量限制
Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。 它們被稱作捕獲Lambda。 Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須顯式聲明為final,或事實上是final。
為什么局部變量有這些限制?
(1)實例變量和局部變量背后的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之后,去訪問該變量。因此, Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什么區別了——因此就有了這個限制。
(2)這一限制不鼓勵你使用改變外部變量的典型命令式編程模式。
1.2.5 使用示例
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
long num = list.stream().filter( a -> a > 4 ).count();
System.out.println(num);
上面這段是統計list中大於4的值的個數,使用的lambda表達式為a-> a> 4
,這里參數a沒有定義類型,會自動判斷為Integer類型,而這個表達式的值會自動轉化成函數接口Predicate對應的對象(filter方法定義的輸入參數類型),至於stream及相關的操作則是下面要說的流操作。它們經常一起配合進行一起數據處理。
二、流
2.1 流介紹
流是Java API的新成員,它允許你以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷數據集的高級迭代器。此外,流還可以透明地並行處理,你無需寫任何多線程代碼了!
2.2 使用流
類別 | 方法名 | 方法簽名 | 作用 |
---|---|---|---|
篩選切片 | filter | Stream<T> filter(Predicate<? super T> predicate) | 過濾操作,根據Predicate判斷結果保留為真的數據,返回結果仍然是流 |
distinct | Stream<T> distinct() | 去重操作,篩選出不重復的結果,返回結果仍然是流 | |
limit | Stream<T> limit(long maxSize) | 截取限制操作,只取前 maxSize條數據,返回結果仍然是流 | |
skip | Stream<T> skip(long n) | 跳過操作,跳過n條數據,取后面的數據,返回結果仍然是流 | |
映射 | map | <R> Stream<R> map(Function<? super T, ? extends R> mapper) | 轉化操作,根據參數T,轉化成R類型,返回結果仍然是流 |
flatMap | <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) | 轉化操作,根據參數T,轉化成R類型流,這里會生成多個R類型流,返回結果仍然是流 | |
匹配 | anyMatch | boolean anyMatch(Predicate<? super T> predicate) | 判斷是否有一條匹配,根據Predicate判斷結果中是否有一條匹配成功 |
allMatch | boolean allMatch(Predicate<? super T> predicate) | 判斷是否全都匹配,根據Predicate判斷結果中是否全部匹配成功 | |
noneMatch | boolean noneMatch(Predicate<? super T> predicate) | 判斷是否一條都不匹配,根據Predicate判斷結果中是否所有的都不匹配 | |
查找 | findAny | Optional<T> findAny() | 查找操作, 查詢當前流中的任意元素並返回Optional |
findFirst | Optional<T> findFirst() | 查找操作, 查詢當前流中的第一個元素並返回Optional | |
歸約 | reduce | T reduce(T identity, BinaryOperator<T> accumulator); | 歸約操作,同樣兩個類型的數據進行操作后返回相同類型的結果。比如兩個整數相加、相乘等。 |
max | Optional<T> max(Comparator<? super T> comparator) | 求最大值,根據Comparator計算的比較結果得到最大值 | |
min | Optional<T> min(Comparator<? super T> comparator) | 求最小值,根據Comparator計算的比較結果得到最小值 | |
匯總統計 | collect | <R, A> R collect(Collector<? super T, A, R> collector) | 匯總操作,匯總對應的處理結果。這里經常與 |
count | long count() | 統計流中數據數量 | |
遍歷 | foreach | void forEach(Consumer<? super T> action) | 遍歷操作,遍歷執行Consumer 對應的操作 |
上面是Stream API的一些常用操作,按場景結合lambda表達式調用對應方法即可。至於Stream的生成方式,Stream的of方法或者Collection接口實現類的stream方法都可以獲得對應的流對象,再進一步根據需要做對應處理。
另外上述方法如果返回是Stream對象時是可以鏈式調用的,這個時候這個操作只是聲明或者配方,不產生新的集合,這種類型的方法是惰性求值方法;有些方法返回結果非Stream類型,則是及早求值方法。
“為什么要區分惰性求值和及早求值? 只有在對需要什么樣的結果和操 作有了更多了解之后, 才能更有效率地進行計算。 例如, 如果要找出大於 10 的第一個數字, 那么並不需要和所有元素去做比較, 只要找出第一個匹配的元素就夠了。 這也意味着可以在集合類上級聯多種操作, 但迭代只需一次。這也是函數編程中惰性計算的特性,即只在需要產生表達式的值時進行計算。這樣代碼更加清晰,而且省掉了多余的操作。
這里還對上述列表操作中相關的Optional與Collectors類做下說明。
Optional類是為了解決經常遇到的NullPointerException出現的,這個類是一個可能包含空值的容器類。用Optional替代null可以顯示說明結果可能為空或不為空,再使用時使用isPresent方法判斷就可以避免直接調用的空指針異常。
Collectors類是一個非常有用的是歸約操作工具類,工具類中的方法常與流的collect方法結合使用。比如
groupingBy方法可以用來分組,在轉化Map時非常實用;partitioningBy方法可以用來分區(分區可以當做一種特殊的分組,真假值分組),joining方法可以用來連接,這個應用在比如字符串拼接的場景。
2.3 並行流
Collection接口的實現類調用parallelStream方法就可以實現並行流,相應地也獲得了並行計算的能力。或者Stream接口的實現調用parallel方法也可以得到並行流。並行流實現機制是基於fork/join 框架,將問題分解再合並處理。
不過並行計算是否一定比串行快呢?這也不一定。實際影響性能的點包括:
(1)數據大小輸入數據的大小會影響並行化處理對性能的提升。 將問題分解之后並行化處理, 再將結果合並會帶來額外的開銷。 因此只有數據足夠大、 每個數據處理管道花費的時間足夠多
時, 並行化處理才有意義。
(2) 源數據結構
每個管道的操作都基於一些初始數據源, 通常是集合。 將不同的數據源分割相對容易,這里的開銷影響了在管道中並行處理數據時到底能帶來多少性能上的提升。
(3) 裝箱
處理基本類型比處理裝箱類型要快。
(4) 核的數量
極端情況下, 只有一個核, 因此完全沒必要並行化。 顯然, 擁有的核越多, 獲得潛在性能提升的幅度就越大。 在實踐中, 核的數量不單指你的機器上有多少核, 更是指運行時你的機器能使用多少核。 這也就是說同時運行的其他進程, 或者線程關聯性( 強制線程在某些核或 CPU 上運行) 會影響性能。
(5) 單元處理開銷
比如數據大小, 這是一場並行執行花費時間和分解合並操作開銷之間的戰爭。 花在流中
每個元素身上的時間越長, 並行操作帶來的性能提升越明顯
實際在考慮是否使用並行時需要考慮上面的要素。在討論流中單獨操作每一塊的種類時, 可以分成兩種不同的操作: 無狀態的和有狀態的。無狀態操作整個過程中不必維護狀態, 有狀態操作則有維護狀態所需的開銷和限制。如果能避開有狀態, 選用無狀態操作, 就能獲得更好的並行性能。 無狀態操作包括 map、filter 和 flatMap, 有狀態操作包括 sorted、 distinct 和 limit。這種理解在理論上是更好的,當然實際使用還是以測試結果最為可靠 。
三、方法引用
方法引用的基本思想是,如果一個Lambda代表的只是“直接調用這個方法”,那最好還是用名稱來調用它,而不是去描述如何調用它。事實上,方法引用就是讓你根據已有的方法實現來創建Lambda表達式。但是,顯式地指明方法的名稱,你的代碼的可讀性會更好。所以方法引用只是在內容中只有一個表達式的簡寫。
當 你 需 要使用 方 法 引用時 , 目 標引用 放 在 分隔符::前 ,方法 的 名 稱放在 后 面 ,即ClassName :: methodName
。例如 ,Apple::getWeight
就是引用了Apple類中定義的方法getWeight。請記住,不需要括號,因為你沒有實際調用這個方法。方法引用就是Lambda表達式(Apple a) -> a.getWeight()
的快捷寫法。
這里有種情況需要特殊說明,就是類的構造函數情況,這個時候是通過ClassName::new
這種形式創建Class構造函數對應的引用,例如:
四、默認方法
4.1 介紹
為了以兼容方式改進API,Java 8中加入了默認方法。主要是為了支持庫設計師,讓他們能夠寫出更容易改進的接口。具體寫法是在接口中加default關鍵字修飾。
4.2 使用說明
默認方法由於是為了避免兼容方式改進API才引入,所以一般正常開發中不會使用,除非你也想改進API,而不影響老的接口實現。當然在JDK8有大量的地方用到了默認方法,所以對這種寫法有一定的了解還是有幫助的。
采用默認方法之后,你可以為這個方法提供一個默認的實現,這樣實體類就無需在自己的實現中顯式地提供一個空方法,而是默認就有了實現。
4.3 注意事項
由於類可以實現多個接口,也可以繼承類,當接口或類中有相同函數簽名的方法時,這個時候到底使用哪個類或接口的實現呢?
這里有三個規則可以進行判斷:
(1) 類中的方法優先級最高。類或父類中聲明的方法的優先級高於任何聲明為默認方法的優先級。
(2) 如果無法依據第一條進行判斷,那么子接口的優先級更高:函數簽名相同時,優先選擇擁有最具體實現的默認方法的接口,即如果B繼承了A,那么B就比A更加具體。
(3) 最后,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式地選擇使用哪一個默認方法的實現。不然編譯都會報錯。
五、方法參數反射
JDK8 新增了Method.getParameters方法,可以獲取參數信息,包括參數名稱。不過為了避免.class文件因為保留參數名而導致.class文件過大或者占用更多的內存,另外也避免有些參數( secret/password)泄露安全信息,JVM默認編譯出的class文件是不會保留參數名這個信息的。
這一選項需由編譯開關 javac -parameters 打開,默認是關閉的。在Eclipse(或者基於Eclipse的IDE)中可以如下圖勾選保存:
六、日期/時間改進
1.8之前JDK自帶的日期處理類非常不方便,我們處理的時候經常是使用的第三方工具包,比如commons-lang包等。不過1.8出現之后這個改觀了很多,比如日期時間的創建、比較、調整、格式化、時間間隔等。
這些類都在java.time包下。比原來實用了很多。
6.1 LocalDate/LocalTime/LocalDateTime
LocalDate為日期處理類、LocalTime為時間處理類、LocalDateTime為日期時間處理類,方法都類似,具體可以看API文檔或源碼,選取幾個代表性的方法做下介紹。
now相關的方法可以獲取當前日期或時間,of方法可以創建對應的日期或時間,parse方法可以解析日期或時間,get方法可以獲取日期或時間信息,with方法可以設置日期或時間信息,plus或minus方法可以增減日期或時間信息;
6.2 TemporalAdjusters
這個類在日期調整時非常有用,比如得到當月的第一天、最后一天,當年的第一天、最后一天,下一周或前一周的某天等。
6.3 DateTimeFormatter
以前日期格式化一般用SimpleDateFormat類,但是不怎么好用,現在1.8引入了DateTimeFormatter類,默認定義了很多常量格式(ISO打頭的),在使用的時候一般配合LocalDate/LocalTime/LocalDateTime使用,比如想把當前日期格式化成yyyy-MM-dd hh:mm:ss的形式:
-
LocalDateTime dt = LocalDateTime.now();
-
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
-
System.out.println(dtf.format(dt));
七、參考資料
官方教程:http://docs.oracle.com/javase/tutorial/
《Java 8實戰》
《Java 8函數式編程》