1. 程式人生 > >java8簡易學習階段性總結(一)

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 裡的值生成一個列表,是一個及早求值操作。可以理解為 StreamCollection 的轉換。
注意這邊的 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 中的 maxmin 操作足以解決這一問題。

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 操作可以實現從一組值中生成一個值。在上述例子中用到的 countminmax 方法,因為常用而被納入標準庫中。事實上,這些方法都是 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