java8簡易學習階段性總結(一)
java8簡易學習階段性總結(一)
寫在前面:本文講簡易講解java8中的介面新特性、lambda表示式、函式介面、方法引用、streamAPI、資料並行化操作、級聯表示式和柯里化、Map的操作、時間日期API
測試所用到的程式碼地址:https://github.com/Blankwhiter/java8
本文參考引用 [1]: https://www.cnblogs.com/snowInPluto/p/5981400.html
[2]: http://www.importnew.com/10360.html
一、介面新特性
1.預設方法:java8中可以使用default關鍵字,為介面新增非抽象的方法實現。在實現擁有default方法的介面時,可以直接使用該預設方法。示例如下:
帶有預設方法的介面:
/**
* 測試java8介面新特性(預設方法/擴充套件方法)
*/
public interface DefaultInterface {
/**
* 加法
* @param a
* @param b
* @return
*/
default int add(int a,int b){
return a+b;
}
}
測試預設方法介面:
/**
* 測試預設方法
*/
public class TestDefaultMethod implements DefaultInterface {
/**
* 呼叫DefaultInterface的預設方法
*/
public void useDefaultInterface(){
System.out.println("加法結果 : "+add(1, 2));
}
public static void main(String[] args) {
TestDefaultMethod main = new TestDefaultMethod();
main.useDefaultInterface();
}
}
在此處我們還需要多考慮2個問題:
1.多重繼承的衝突問題
當一個方法從多個介面同時引入的時候,到底會使用哪個介面的問題? 示例如下:
編寫重寫DefaultInterface的add方法的介面
/**
* 測試多重繼承的衝突問題
*/
public interface DefaultOverwriteInterface extends DefaultInterface {
/**
* 預設多加100來區分DefaultInterface的add
* @param a
* @param b
* @return
*/
@Override
default int add(int a, int b){
return a + b+100;
}
}
測試多繼承介面
/**
* 測試一個方法在多個介面同時引入問題
*/
public class TestDefaultOverwriteMethod implements DefaultInterface,DefaultOverwriteInterface {
/**
* 方法執行的結果: 重寫 加法結果 : 103
* @param args
*/
public static void main(String[] args) {
TestDefaultOverwriteMethod testDefaultOverwriteMethod = new TestDefaultOverwriteMethod();
System.out.println("重寫 加法結果 : "+testDefaultOverwriteMethod.add(1, 2));
}
}
從結果來看 它遵從了優先選取最具體的實現原則
2.當類中 同時繼承了一個類已有了介面實現同樣的方法的問題
編寫具有相同方法的父類:
public class DefaultExist {
/**
* 預設加200來區分其他的add方法
* @param a
* @param b
* @return
*/
public int add(int a,int b){
return a + b + 200;
}
}
測試子類實現同個方法:
/**
* 測試當類中 同時繼承了一個類已有了介面實現同樣的方法的問題
*/
public class TestDefaultExistMethod extends DefaultExist implements DefaultInterface,DefaultOverwriteInterface {
/**
* 方法執行的結果:繼承 實現 加法結果 : 203
* @param args
*/
public static void main(String[] args) {
TestDefaultExistMethod testDefaultExistMethod = new TestDefaultExistMethod();
System.out.println("繼承 實現 加法結果 : "+testDefaultExistMethod.add(1, 2));
}
}
從結果看 遵從類優先的原則:在繼承一個類的同時實現了另外一個介面的,而介面和該類中有相同的實現方法的時候,會以類的方法作為最優先的。
當然如果你想避免這樣的問題,可以在介面上方
2.靜態方法:Java8中可以在介面中宣告靜態方法,並加以實現。示例如下:
帶有靜態實現方法的介面:
/**
* 測試java8介面新特性(靜態方法)
*/
public interface StaticInterface {
/**
* 減法
* @param a
* @param b
* @return
*/
static int reduce(int a,int b){
return a - b;
}
}
測試靜態方法的介面:
/**
* 測試靜態方法介面
*/
public class TestStaticMethod implements StaticInterface {
/**
* 呼叫了StaticInterface介面中的reduce靜態方法
* @param args
*/
public static void main(String[] args) {
System.out.println("減法結果 : "+StaticInterface.reduce(4, 2));
}
}
3.@FunctionalInterface註解
Java 8為函式式介面引入了一個新註解@FunctionalInterface,主要用於編譯級錯誤檢查,加上該註解,當你寫的介面不符合函式式介面定義的時候,編譯器會報錯。那什麼是函式式介面?所謂的函式式介面,當然首先是一個介面,然後就是在這個接口裡面只能有一個抽象方法。符合了單一職責原則。還幫助解決了預設上述的覆蓋問題。
待用FunctionalInterface註解的介面:
@FunctionalInterface
public interface FuncInterface {
void test(String txt);
}
測試使用帶註解的介面應用於lambda表示式:
/**
*測試FunctionalInterface在lambda表示式的應用
*/
public class TestFuncInterfaceMethod {
public static void main(String[] args) {
//System.out::println 等同於 str->System.out.println(str);
FuncInterface fun = System.out::println;
fun.test("測試成功");
}
}
二、lambda表示式
“lambda表示式”是一段可以傳遞的程式碼,因為他可以被執行一次或多次。
lambda常見的幾種形式:
// 1. 不需要引數,返回值為 5
() -> 5
// 2. 接收一個引數(數字型別),返回其2倍的值
x -> 2 * x
// 3. 接受2個引數(數字),並返回他們的差值
(x, y) -> x – y
// 4. 接收2個int型整數,返回他們的和
(int x, int y) -> x + y
// 5. 接受一個 string 物件,並在控制檯列印,不返回任何值(看起來像是返回void)
(String s) -> System.out.print(s)
這裡需要多提一點的是:對於lambdab表示式外部的變數,其訪問許可權的粒度與匿名物件的方式非常類似。你能夠訪問區域性對應的外部區域的區域性final變數,以及成員變數和靜態變數。
三、函式介面
函式介面 可以理解成一段行為的抽象,即一段行為作為引數進行傳遞。接下來主要介紹Predicate、 Consumer、 Function、 Supplier 、UnaryOperator、 BinaryOperator 基礎函式介面:
介面 | 引數 | 返回型別 | 描述 | 功能 |
---|---|---|---|---|
Predicate<T> | T | boolean | 用於判別一個物件。比如求一個人是否為男性 | 用於判斷物件是否符合某個條件,經常被用來過濾物件。 |
Consumer<T> | T | void | 用於接收一個物件進行處理但沒有返回,比如接收一個人並列印他的名字 | 消費者 |
Function<T, R> | T | R | 轉換一個物件為不同型別的物件 | 將一個物件轉換為另一個物件,比如說要裝箱或者拆箱某個物件。 |
Supplier<T> | None | T | 提供一個物件 | 提供者 |
UnaryOperator<T> | T | T | 接收物件並返回同類型的物件 | 接收和返回同類型物件,一般用於對物件修改屬性 |
BinaryOperator<T> | (T, T) | T | 接收兩個同類型的物件,並返回一個原型別物件 | 可以理解為合併物件 |
測試示例如下:
/**
* 測試函式介面
*/
public class TestFucntionInterfaceMethod {
public static void main(String[] args) {
//斷言函式介面 編寫一個判斷數字是否大於5的函式介面。有輸入引數,返回Boolean
Predicate<Integer> predicate = integer -> integer>5;
boolean result = predicate.test(10);
System.out.println("判斷結果是"+result);
//消費者函式介面 編寫一個拼接字串的的函式介面。只有輸入引數,無返回型別
Consumer<String> consumer = str-> System.out.println("傳入的字串是"+str);
consumer.accept("running");
//方法函式介面 編寫一個將字串轉化成數字的函式介面。有輸入引數,返回第二個引數資料型別
Function<String,Integer> function = x->Integer.parseInt(x);
Integer data = function.apply("10");
System.out.println("返回的結果是"+data);
//提供者函式介面 編寫一個提供字串的函式介面。沒有輸入引數,返回引數型別
Supplier<String> supplier = ()-> "這裡提供了一串字串";
System.out.println("提供者介面得到的結果是"+supplier.get());
//一元函式介面 編寫一個當前輸入數字加1的函式介面。有輸入引數,返回資料,輸入引數與輸出引數型別一致
UnaryOperator<Integer> unaryOperator = x-> x + 1;
Integer addResult = unaryOperator.apply(1);
System.out.println("一元函式介面得到的結果是"+addResult);
//二元函式介面 編寫一個兩個數加法的函式介面。有兩個輸入引數,一個返回資料,輸入引數與輸出引數型別一致
BinaryOperator<Integer> binaryOperator = (x,y)->x+y;
Integer applyResult = binaryOperator.apply(1, 2);
System.out.println("二元函式介面得到的結果是"+applyResult);
}
}
當然除了上面所描述的幾個常見的介面函式,java8中還包含其他許多函式介面,讀者可以根據需要查詢API進行實驗。
四、方法引用
方法引用通過方法的名字來指向一個方法,是lambda表示式的一種簡寫形式。下個表格列舉出常見的方法引用形式:
型別 | 示例 |
---|---|
引用靜態方法 | ContainingClass::staticMethodName |
引用某個物件的例項方法 | containingObject::instanceMethodName |
引用某個型別的任意物件的例項方法 | ContainingType::methodName |
引用構造方法 | ClassName::new |
示例程式碼如下:
商品類實體:
/**
* 商品類
*/
public class Product {
/**
* 商品名稱
*/
private String name = "筆記本";
/**
* 商品數量 預設100
*/
private int num = 100;
public Product(){}
public Product(String name){
this.name = name;
}
/**
* 展示商品名稱
* @param product
*/
public static void showProductName(Product product){
System.out.println("商品名稱:"+product);
}
/**
* 銷售商品 返回剩餘.說明非靜態方法中,預設第一個不可見引數是類本身的例項. public int sales(int num)==>public int sales(Product this,int num)
* @param num
* @return
*/
public int sales(int num){
this.num -= num;
return this.num;
}
@Override
public String toString() {
return this.name;
}
}
測試方法引用:
import java.util.function.*;
/**
* 測試方法引用
*/
public class TestMethodReferenceMethod {
public static void main(String[] args) {
//靜態方法的方法引用
Consumer<Product> consumer = Product::showProductName;
Product product = new Product();
consumer.accept(product);
//非靜態方法的方法引用
Function<Integer,Integer> function = product::sales;
//==》Function<Integer,Integer> function = product::sales;
// 由於輸入 輸出引數一致 故等同於 1.UnaryOperator<Integer> unaryOperator = product::sales;
// 2. IntUnaryOperator intUnaryOperator = product::sales;
// 3.也等用於 下方的使用類名來方法引用
Integer surplus = function.apply(10);
System.out.println("剩餘數量是"+surplus);
//使用類名來方法引用
BiFunction<Product,Integer,Integer> biFunction = Product::sales;
Integer surplus2 = biFunction.apply(product, 20);
System.out.println("剩餘數量是"+surplus2);
//建構函式的方法引用
Supplier<Product> supplier= Product::new;
Product product1 = supplier.get();
System.out.println("建立了"+product1);
//帶有引數的建構函式的方法引用
Function<String,Product> function1 = Product::new;
Product product2 = function1.apply("冰箱");
System.out.println("再次建立了"+product2);
}
}
五、streamAPI
java8中對Collection做了相當大的優化。這便是引入了stream。它遵循“做什麼,而不是怎麼去做”的原則。只需要描述需要做什麼,而不用考慮程式是怎樣實現的。這樣大大方便了我們的開發過程。
Stream 的方法分為兩類。一類叫惰性求值,一類叫及早求值。
判斷一個操作是惰性求值還是及早求值很簡單:只需看它的返回值。如果返回值是 Stream,那麼是惰性求值。其實可以這麼理解,如果呼叫惰性求值方法,Stream 只是記錄下了這個惰性求值方法的過程,並沒有去計算,等到呼叫及早求值方法後,就連同前面的一系列惰性求值方法順序進行計算,返回結果。
通用形式為:
Stream.惰性求值.惰性求值. ... .惰性求值.及早求值
整個過程和建造者模式有共通之處。建造者模式使用一系列操作設定屬性和配置,最後調 用一個 build 方法,這時,物件才被真正建立。
接下來介紹常用操作:
1.collect(toList())
collect(toList()) 方法由 Stream 裡的值生成一個列表,是一個及早求值操作。可以理解為 Stream 向 Collection 的轉換。
注意這邊的 toList() 其實是 Collectors.toList(),因為採用了靜態倒入,看起來顯得簡潔。
List<String> collected = Stream.of("a", "b", "c")
.collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
2.map
如果有一個函式可以將一種型別的值轉換成另外一種型別,map 操作就可以使用該函式,將一個流中的值轉換成一個新的流。
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
map 方法就是接受的一個 Function 的匿名函式類,進行的轉換。
3.filter
遍歷資料並檢查其中的元素時,可嘗試使用 Stream 中提供的新方法 filter。
List<String> beginningWithNumbers =
Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);
filter 方法就是接受的一個 Predicate 的匿名函式類,判斷物件是否符合條件,符合條件的才保留下來。
4.flatMap
flatMap 方法可用 Stream 替換值,然後將多個 Stream 連線成一個 Stream。
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
flatMap 最常用的操作就是合併多個 Collection。
5.max和min
Stream 上常用的操作之一是求最大值和最小值。Stream API 中的 max 和 min 操作足以解決這一問題。
List<Integer> list = Lists.newArrayList(3, 5, 2, 9, 1);
int maxInt = list.stream()
.max(Integer::compareTo)
.get();
int minInt = list.stream()
.min(Integer::compareTo)
.get();
assertEquals(maxInt, 9);
assertEquals(minInt, 1);
這裡有 2 個要點需要注意:
1.max 和 min 方法返回的是一個 Optional 物件(對了,和 Google Guava 裡的 Optional 物件是一樣的)。Optional 物件封裝的就是實際的值,可能為空,所以保險起見,可以先用 isPresent() 方法判斷一下。Optional 的引入就是為了解決方法返回 null 的問題。 2.Integer::compareTo 也是屬於 Java 8 引入的新特性,叫做 方法引用(Method References)。在這邊,其實就是 (int1, int2) -> int1.compareTo(int2) 的簡寫,可以自己查閱瞭解,這裡不再多做贅述。
6.reduce
reduce 操作可以實現從一組值中生成一個值。在上述例子中用到的 count、min 和 max 方法,因為常用而被納入標準庫中。事實上,這些方法都是 reduce 操作。
上圖展示了 reduce 進行累加的一個過程。具體的程式碼如下:
int result = Stream.of(1, 2, 3, 4).reduce(0, (acc, element) -> acc + element);
assertEquals(10, result);
注意 reduce 的第一個引數,這是一個初始值。0 + 1 + 2 + 3 + 4 = 10。
如果是累乘,則為:
int result = Stream.of(1, 2, 3, 4)
.reduce(1, (acc, element) -> acc * element);
assertEquals(24, result);
因為任何數乘以 1 都為其自身嘛。1 * 1 * 2 * 3 * 4 = 24。
Stream 的方法還有很多,這裡列出的幾種都是比較常用的。Stream 還有很多通用方法,具體可以查閱 Java 8 的 API 文件。
https://docs.oracle.com/javase/8/docs/api/
7.資料並行化操作
Stream 的並行化也是 Java 8 的一大亮點。資料並行化是指將資料分成塊,為每塊資料分配單獨的處理單元。這樣可以充分利用多核 CPU 的優勢。
並行化操作流只需改變一個方法呼叫。如果已經有一個 Stream 物件,呼叫它的 parallel() 方法就能讓其擁有並行操作的能力。如果想從一個集合類建立一個流,呼叫 parallelStream() 就能立即獲得一個擁有並行能力的流。
int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
.parallel()
.map(s -> s.length())
.reduce(Integer::sum)
.get();
assertEquals(sumSize, 21);
這裡求的是一個字串列表中各個字串長度總和。
如果你去計算這段程式碼所花的時間,很可能比不加上 parallel() 方法花的時間更長。這是因為資料並行化會先對資料進行分塊,然後對每塊資料開闢執行緒進行運算,這些地方會花費額外的時間。並行化操作只有在 資料規模比較大 或者 資料的處理時間比較長 的時候才能體現出有事,所以並不是每個地方都需要讓資料並行化,應該具體問題具體分析。
這裡多提有點的就是並行流使用的執行緒池是ForkJoinPool.ommonPool,預設的執行緒數是當前機器的cpu個數,如果讀者想修改預設的執行緒數使用System.serProperty(“java.util.concurrent.ForkJoinPool.parallelism”,“執行緒數”);。示例如下:
System.serProperty("java.util.concurrent.ForkJoinPool.parallelism","10");
int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
.parallel