1. 程式人生 > >Java8 In Action-1.基礎知識

Java8 In Action-1.基礎知識

Java8帶來的最重要的改變是引入了函數語言程式設計的思想,在多核和處理大型資料集的計算應用下,帶來了如Lambda(匿名函式) 、流、預設方法等核心特性,比之前的命令式更適應新的體系架構.

  1. 新的程式設計概念 1.1流處理 流是一系列資料項,一次只生成一項.程式可以從輸入流中一個一個讀取資料項,然後以同樣的方式將資料項寫入輸出流。一個程式的輸出流很可能是另一個程式的輸入流。 Stream API:java.util.stream 這裡的概念可以類比於Linux中的管道流(|). 如以下命令: cat file1 file2 | tr “[A-Z]” “[a-z]” | sort | tail -3 處理流程如下: 操作流的Unix命令
    Stream就是一系列T型別的專案,Stream API的很多方法可以連結起來形成一個複雜的流水線,把這樣的流變成那樣的流,甚至可以是並行流,因為java8可以透明的把輸入的不相關部分拿到幾個cpu上去分別執行Stream操作流水線.

1.2用行為引數化把程式碼傳遞給方法 通過API來傳遞程式碼,Java 8增加了把方法(你的程式碼)作為引數傳遞給另一個方法的能力,Stream API就是構建在通過傳遞程式碼使操作行為實現引數化的思想上的.

1.3並行與共享的可變資料 Java 8的流實現並行比Java現有的執行緒API更容易. 沒有共享的可變資料+將方法和函式即程式碼傳遞給其他方法的能力是我們平常所說的函數語言程式設計正規化的基石.與此相反,在指令式程式設計正規化中,你寫的程式則是一系列改變狀態的指令。

2.流 幾乎每個Java應用都會製造和處理集合。但集合用起來並不總是那麼理想。 場景:從一個User列表中篩選年齡大於20,然後按性別分組.

public class User {

    private String name;
    private String sex;
    private Integer age;

    public User(String name, String sex, Integer age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
    
   getter/setter()....
 
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

package com.h.java8;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by John on 2018/9/23.
 */
public class TestMain {

    public static void main(String[] args) {
        List<User> userList = Arrays.asList(new User("1", "m", 18), new User("2", "f", 21), new User("3", "m", 27));
        /**
         *  Java8前常見的套路程式碼
         */
        Map<String, List<User>> map1 = new HashMap<>();
        List<User> list = null;
        for (User u : userList) {
            if (u.getAge() > 20) {
                list = map1.get(u.getSex());
                if (Objects.isNull(list)) {
                    list = new ArrayList<>();
                    list.add(u);
                    map1.put(u.getSex(), list);
                } else {
                    list.add(u);
                }
            }
        }
        for (Map.Entry<String, List<User>> entry : map1.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        System.out.println("=================================");
        /**
         * java8之後
         */
        Map<String, List<User>> map = userList.stream().filter(u -> u.getAge() > 20).collect(Collectors.groupingBy(User::getSex));
        map.forEach((k, v) -> System.out.println(k + "=>" + v));

    }
}

和Collection API相比,Stream API處理資料的方式非常不同.用集合的話,你得自己去做迭代的過程。你得用for-each迴圈一個個去迭代元素,然後再處理元素。我們把這種資料迭代的方法稱為外部迭代。相反,有了Stream API,你根本用不著操心迴圈的事情。資料處理完全是在庫內部進行的。我們把這種思想叫作內部迭代。 Collection主要是為了儲存和訪問資料,而Stream則主要用於描述對資料的計算。

3.預設方法,新增default關鍵字 Java 8中加入預設方法主要是為了支援庫設計師,讓他們能夠寫出更容易改進的介面。例如,在Java 8之前,需要使用Collections.sort()對集合排序,而現在可以直接對List呼叫sort方法。它是用Java 8 List介面中如下所示的預設方法實現的,它會呼叫List.sort靜態方法:

 default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

這意味著List的任何實體類都不需要顯式實現sort,而在以前的Java版本中,除非提供了sort的實現,否則這些實體類在重新編譯時都會失敗。不過慢著,一個類可以實現多個介面,不是嗎?那麼,如果在好幾個接口裡有多個預設實現,是否意味著Java中有了某種形式的多重繼承?是的,在某種程度上是這樣。 Java8從函數語言程式設計中引入的兩個核心思想:將方法和Lambda作為一等值,以及在沒有可變共享狀態時,函式或方法可以有效、安全地並行執行。前面說到的新的Stream API把這兩種思想都用到了。

二.通過行為引數化傳遞程式碼 行為引數化就是可以幫助你處理頻繁變更的需求的一種軟體開發模式。一言以蔽之,它意味著拿出一個程式碼塊,把它準備好卻不去執行它。這個程式碼塊以後可以被你程式的其他部分呼叫,這意味著你可以推遲這塊程式碼的執行。例如,你可以將程式碼塊作為引數傳遞給另一個方法,稍後再去執行它。這樣,這個方法的行為就基於那塊程式碼被引數化了。 場景:根據Apple的某些屬性(比如它是綠色的嗎?重量超過150克嗎?)來返回一個boolean值.我們把它稱為謂詞(即一個返回boolean值的函式)。 2.1策略設計模式. 定義一個介面來對選擇標準建模:

public interface ApplePredicate{
boolean test (Apple apple);
}

定義多個實現代表不同的選擇標準:

public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
	return apple.getWeight() > 150;//僅僅選出重的蘋果
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
	return "green".equals(apple.getColor());//僅僅選出綠蘋果
}
}

在這裡插入圖片描述 根據抽象條件篩選蘋果:

public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
	List<Apple> result = new ArrayList<>();
	for(Apple apple: inventory){
		if(p.test(apple)){  //謂詞物件封裝了測試蘋果的條件
			result.add(apple);
		}
	}
return result;
}

在這裡插入圖片描述

2.2使用匿名內部類:進一步改進,減少囉嗦的程式碼 匿名類和 區域性類(塊中定義的類)差不多,但匿名類沒有名字。它允許你同時 宣告並例項化一個類。換句話說,它允許你隨用隨建。

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
	public boolean test(Apple apple){
		//直接內聯引數化filterapples方法的行為
		return "red".equals(apple.getColor());
	}
});
public class MeaningOfThis{
    public final int value = 4;

    public void doIt(){
        int value = 6;
        Runnable r = new Runnable() {
            public final int value = 5;
            @Override
            public void run() {
                int value = 10;
                System.out.println(this.value);
            }
        };
        r.run();
    }

    public static void main(String[] args) {
        MeaningOfThis testMain = new MeaningOfThis();
        testMain.doIt();
    }
}
/***輸出5,因為this指的是包含它的Runnable,而不是外面的類MeaningOfThis***/

2.3使用Lambda 表示式

List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

2.4將List型別抽象化

public interface Predicate<T>{
	boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
	List<T> result = new ArrayList<>();
	for(T e: list){
		if(p.test(e)){
		result.add(e);
	}
}
	return result;
}

行為引數化,就是一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力。它是一個很有用的模式, 類似於策略設計模式,它能夠輕鬆地適應不斷變化的需求。這 種模式可以把一個行為(一段程式碼)封裝起來,並通過傳遞和使用建立的行為(例如對Apple的不同謂詞)將方法的行為引數化。

三.Lambda表示式 3.1什麼是Lambda表示式 簡潔地表示可傳遞的匿名函式的一種方式:它沒有名稱,但它有引數列表、函式主體、返回型別,可能還有一個可以丟擲的異常列表. 匿名-我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫得少而想得多! 函式-我們說它是函式,是因為Lambda函式不像方法那樣屬於某個特定的類。但和方法一樣,Lambda有引數列表、函式主體、返回型別,還可能有可以丟擲的異常列表。 傳遞-Lambda表示式可以作為引數傳遞給方法或儲存在變數中 簡潔-無需像匿名類那樣寫很多模板程式碼 在這裡插入圖片描述 Lambda的基本語法:

  • (parameters) -> expression
  • (parameters) -> { statements; }

在這裡插入圖片描述

3.2在哪裡以及如何使用Lambda? Lambda表示式需要配合函式式介面使用.函式式介面就是隻定義一個抽象方法的介面.這裡要注意:介面現在還可以擁有預設方法(即在類沒有對方法進行實現時,其主體為方法提供預設實現的方法)。哪怕有很多預設方法,只要介面只定義了一個抽象方法,它就仍然是一個函式式介面.用函式式介面可以幹什麼呢?Lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供實現,並把整個表示式作為函式式介面的例項(具體說來,是函式式介面一個具實現的例項)[email protected]標註用於表示該介面會設計成一個函式式介面.

3.3函式描述符 函式式介面的抽象方法的簽名基本上就是Lambda表示式的簽名。我們將這種抽象方法叫作函式描述符.

3.4把Lambda付諸實踐:環繞執行模式 場景:資源處理(例如處理檔案或資料庫)時一個常見的模式就是開啟一個資源,做一些理,然後關閉資源。這個設定和清理階段總是很類似,並且會圍繞著執行處理的那些重要程式碼。就是所謂的環繞執行(execute around)模式.

public static String processFile() throws IOException {
    //Java 7中帶資源的try語句,它已經簡化了程式碼,因為你不需要顯式地關閉資源
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    	return br.readLine();//這就是做有用工作的那行程式碼
   }
}
@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}
public class TestMain {

    public static void main(String[] args) throws IOException {
        /**
         * 向函式式介面傳遞Lambda表示式,靈活應對需求
         */
        //獲取第一行資料
        String s0 = processFile(br -> br.readLine());
        System.out.println(s0);
        //獲取前兩行資料
        String s1 = processFile(br -> br.readLine() + br.readLine());
        System.out.println(s1);
    }

    public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("D://data.txt"))) {
            return p.process(br);//處理BufferedReader物件
        }
    }
}

3.5Java API中的函式式介面(java.util.function) 在這裡插入圖片描述 在這裡插入圖片描述

原始型別特化 Java型別要麼是引用型別(比如Byte、Integer、Object、List),要麼是原始型別(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能繫結到引用型別。這是由泛型內部的實現方式造成的。因此,在Java裡有一個將原始型別轉換為對應的引用型別的自動拆裝箱機制。但這在效能方面是要付出代價的。裝箱後的值本質上就是把原始型別包裹起來,並儲存在堆裡。因此,裝箱後的值需要更多的記憶體,並需要額外的記憶體搜尋來獲取被包裹的原始值。 Java 8為我們前面所說的函式式介面帶來了一個專門的版本,以便在輸入和輸出都是原始型別時避免自動裝箱的操作。比如,在下面的程式碼中,使用IntPredicate就避免了對值1000進行裝箱操作,但要是用Predicate<Integer>就會把引數1000裝箱到一個Integer物件中:

public interface IntPredicate{
	boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);//true,無裝箱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);//false,裝箱

一般來說,針對專門的輸入引數型別的函式式介面的名稱都要加上對應的原始型別字首,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function介面還有針對輸出引數型別的變種:ToIntFunction<T>、IntToDoubleFunction等。 在這裡插入圖片描述 任何函式式介面都不允許丟擲受檢異常(checked exception)。如果你需要Lambda表示式來丟擲異常,有兩種辦法:定義一個自己的函式式介面,並宣告受檢異常,或者把Lambda 包在一個try/catch塊中,顯示捕獲受檢異常.

3.6型別檢查、型別推斷以及限制 當我們第一次提到Lambda表示式時,說它可以為函式式介面生成一個例項。然而,Lambda表示式本身並不包含它在實現哪個函式式介面的資訊.Lambda的型別是從使用Lambda的上下文推斷出來的。上下文(比如,接受它傳遞的方法的引數,或接受它的值的區域性變數)中Lambda表示式需要的型別稱為目標型別,利用目標型別來檢查一個Lambda是否可以用於某個特定的上下文. 在這裡插入圖片描述

同樣的Lambda,不同的函式式介面 特殊的void相容規則 如果一個Lambda的主體是一個語句表示式, 它就和一個返回void的函式描述符相容(當然需要引數列表也相容)。例如,以下兩行都是合法的,儘管List的add方法返回了一個boolean,而不是Consumer上下文(T -> void)所要求的void:

// Predicate返回了一個boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一個void
Consumer<String> b = s -> list.add(s);

3.7型別推斷 Java編譯器會從上下文(目標型別)推斷出用什麼函式式介面來配合Lambda表示式,這意味著它也可以推斷出適合Lambda的簽名,因為函式描述符可以通過目標型別來得到。這樣做的好處在於,編譯器可以瞭解Lambda表示式的引數型別,這樣就可以在Lambda語法中省去標註引數型別。換句話說,Java編譯器會像下面這樣推斷Lambda的引數型別:

//沒有型別推斷
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有型別推斷
Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
//當Lambda僅有一個型別需要推斷的引數時,引數名稱兩邊的括號也可以省略

3.8使用區域性變數 Lambda表示式允許使用自由變數(不是引數,而是在外層作用域中定義的變數),就像匿名類一樣。 它們被稱作捕獲Lambda。例如:下面的Lambda捕獲了portNumber變數.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

Lambda可以沒有限制地捕獲(也就是在其主體中引用)例項變數和靜態變數。但區域性變數必須顯式宣告為final,或事實上是final。換句話說,Lambda表示式只能捕獲指派給它們的區域性變數一次。(注:捕獲例項變數可以被看作捕獲最終區域性變數this), 下面的程式碼無法編譯,因為portNumber變數被賦值兩次:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

對區域性變數的限制 為什麼區域性變數有這些限制。第一,例項變數和區域性變數背後的實現有一個關鍵不同。例項變數都儲存在堆中,而區域性變數則儲存在棧上。如果Lambda可以直接訪問區域性變數,而且Lambda是在一個執行緒中使用的,則使用Lambda的執行緒,可能會在分配該變數的執行緒將這個變數收回之後,去訪問該變數。因此,Java在訪問自由區域性變數時,實際上是在訪問它的副本,而不是訪問原始變數。如果區域性變數僅僅賦值一次那就沒有什麼區別了——因此就有了這個限制。 第二,這一限制不鼓勵你使用改變外部變數的典型指令式程式設計模式(我們會在以後的各章中 解釋,這種模式會阻礙很容易做到的並行處理)。

3.9方法引用 方法引用讓你可以重複使用現有的方法定義,並像Lambda一樣傳遞它們。在一些情況下,比起使用Lambda表示式,它們似乎更易讀,感覺也更自然。方法引用可以被看作僅僅呼叫特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個Lambda代表的只是“直接呼叫這個方法”,那最好還是用名稱來呼叫它,而不是去描述如何呼叫它。事實上,方法引用就是讓你根據已有的方法實現來建立Lambda表示式。 在這裡插入圖片描述 你可以把方法引用看作針對僅僅涉及單一方法的Lambda的語法糖,因為你表達同樣的事情時要寫的程式碼更少了.

如何構建方法引用 方法引用主要有三類:

  • 指向靜態方法的方法引用(例如Integer的parseInt方法,寫作Integer::parseInt)
  • 指向任意型別例項方法的方法引用( 例如String 的length 方法, 寫作String::length)
  • 指向現有物件的例項方法的方法引用(假設你有一個區域性變數expensiveTransaction用於存放Transaction型別的物件,它支援例項方法getValue,那麼你就可以寫expensiveTransaction::getValue)

請注意,還有針對建構函式、陣列建構函式和父類呼叫(super-call)的一些特殊形式的方法引用.

建構函式引用 對於一個現有建構函式,你可以利用它的名稱和關鍵字new來建立它的一個引用:ClassName::new。它的功能與指向靜態方法的引用類似。

//建構函式引用指向預設的Apple()建構函式,等價於 Supplier<Apple> c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
呼叫Supplier的get方法將產生一個新的Apple
Apple a1 = c1.get();

//指向Apple(Integer weight)的建構函式引用,等價於 Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

//指向Apple(String color,Integer weight)的建構函式引用
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);
public class Fruit {
    protected String color;
    protected Integer weight;
    public Fruit(Integer weight) {
        this.weight = weight;
    }
    @Override
    public String toString() {
        return "Fruit{" +
                "color='" + color + '\'' +
                ", weight=" + weight +
                '}';
    }
}

public class Apple extends Fruit {
    public Apple(Integer weight) {
        super(weight);
    }
}

public class Orange extends Fruit{
    public Orange(Integer weight) {
        super(weight);
    }
}

public class TestMain {

    public static Map<String,Function<Integer,Fruit>> map = new HashMap<>();

    static {
        map.put("apple", Apple::new);
        map.put("orange",Orange::new);
    }

    public static void main(String[] args) throws IOException {
        Fruit apple = getFruit("apple", 123);
        System.out.println(apple);
    }

    public static Fruit getFruit(String fruit,Integer weight){
        return map.get(fruit).apply(weight);
    }
}

3.10複合Lambda 表示式的有用方法 Java 8的好幾個函式式介面都有為方便而設計的方法。具體而言,許多函式式介面,比如用於傳遞Lambda表示式的Comparator、Function和Predicate都提供了允許你進行復合的方法。這是什麼意思呢?在實踐中,這意味著你可以把多個簡單的Lambda複合成複雜的表示式。比如,你可以讓兩個謂詞之間做一個or操作,組合成一個更大的謂詞。而且,你還可以讓一個函式的結果成為另一個函式的輸入。你可能會想,函式式介面中怎麼可能有更多的方法呢?(畢竟,這違背了函式式介面的定義啊!)竅門在於,我們即將介紹的方法都是預設方法,也就是說它們不是抽象方法。 比較器複合

//逆序,比較器鏈
inventory.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

謂詞複合:謂詞介面包括三個方法:negate、and和or,讓你可以重用已有的Predicate來建立更復雜的謂詞.

Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
 public static void main(String[] args) throws IOException {
        BiFunction<String,Integer,Apple> biFunction = Apple::new;
        List<Apple> list = Arrays.asList(biFunction.apply("red",200),biFunction.apply("green",200),
                biFunction.apply("green",50),biFunction.apply("red",160),biFunction.apply("blue",200));
        //按重量逆序排序,然後再按照顏色排序(逆序+比較器鏈)
        list.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));
        list.forEach(a -> System.out.println(a));
        System.out.println("=======================");
        //篩選出所有顏色不是紅色的蘋果
        Predicate<Apple> redPredict = a -> Objects.equals("red",a.getColor());
        Predicate<Apple> notRedPredict = redPredict.negate();
        List<Apple> appleList = list.stream().filter(notRedPredict).collect(Collectors.toList());
        appleList.forEach(a -> System.out.println(a));
        System.out.println("=======================");
        //進一步組合謂詞,表達要麼是重(150克以上)的紅蘋果,要麼是綠蘋果
        Predicate<Apple> redAndHeavyAppleOrGreen = redPredict.and(a -> a.getWeight() > 150).or(a -> "green".equalsIgnoreCase(a.getColor()));
        appleList = list.stream().filter(redAndHeavyAppleOrGreen).collect(Collectors.toList());
        appleList.forEach(a -> System.out.println(a));
    }

這一點為什麼很好呢?從簡單Lambda表示式出發,你可以構建更復雜的表示式,但讀起來仍然和問題的陳述差不多!請注意,and和or方法是按照在表示式鏈中的位置,從左向右確定優先順序的。因此,a.or(b).and©可以看作(a || b) && c。

3.11函式複合 最後,你還可以把Function介面所代表的Lambda表示式複合起來。Function介面為此配了andThen和compose兩個預設方法,它們都會返回Function的一個例項。

public static void main(String[] args) throws IOException {
        //g(f(x))
        Function<Integer, Integer> f = x -> x + 1;
        Function<Integer, Integer> g = x -> x * 2;
        Function<Integer, Integer> h = f.andThen(g);
        int result = h.apply(1);
        System.out.println(result);//4
        System.out.println("=================");
        //f(g(x))
        Function<Integer, Integer> h1 = f.compose(g);
        result = h1.apply(1);
        System.out.println(result);//3
    }

小結:

  • Lambda表示式可以理解為一種匿名函式:它沒有名稱,但有引數列表、函式主體、返回 型別,可能還有一個可以丟擲的異常的列表。
  • Lambda表示式讓你可以簡潔地傳遞程式碼。
  • 函式式介面就是僅僅聲明瞭一個抽象方法的介面。
  • 只有在接受函式式介面的地方才可以使用Lambda表示式。
  • Lambda表示式允許你直接內聯,為函式式介面的抽象方法提供實現,並且將整個表示式作為函式式介面的一個例項。
  • Java 8自帶一些常用的函式式介面,放在java.util.function包裡,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>和BinaryOperator<T>。
  • 為了避免裝箱操作,對Predicate和Function<T, R>等通用函式式介面的原始型別特化:IntPredicate、IntToLongFunction等。
  • 環繞執行模式(即在方法所必需的程式碼中間,你需要執行點兒什麼操作,比如資源分配和清理)可以配合Lambda提高靈活性和可重用性。
  • Lambda表示式所需要代表的型別稱為目標型別。
  • 方法引用讓你重複使用現有的方法實現並直接傳遞它們。
  • Comparator、Predicate和Function等函式式介面都有幾個可以用來結合Lambda表示式的預設方法。