Java 8-Lambda表達式、方法引用、標準函數接口與流操做、管道操做之間的關係

1.Lambda表達式與接口之間的關係

只要Lambda表達式的聲明形式與接口相一致,在不少狀況下均可以替換接口。見以下代碼html

Thread t1 = new Thread(new Runnable() {
    public void run() {
        System.out.println("hi");
    }
});
t1.start();

Thread t2 = new Thread(() -> System.out.println("hi"));
t2.start();

t1與t2完成相同的功能。t2中的Lambda表達式() -> System.out.println("hi")Runnable接口中的方法public abstract void run();的形式同樣:java

  1. 沒有返回值。
  2. 沒有傳入參數。

下面一個例子中express

String[] arr = {"111","22","3"};
Arrays.sort(arr,new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return o1.length()-o2.length();
    }
});

Arrays.sort(arr, (o1,o2)->o1.length()-o2.length());

(o1,o2)->o1.length()-o2.length()的形式與Compartor中public int compare(String o1, String o2)形式也同樣:數組

  1. 兩個入參。
  2. 一個返回值,且返回值爲int類型。

2.Lambda表達式、匿名內部類與this

Lambda表達式在用法上看起來很像匿名內部類,但其並非匿名內部類。好比,在如下代碼中,在Lambda表達式中不能得到this。oracle

Thread t1 = new Thread(new Runnable() {
    public void run() {
         System.out.println(this);//打印匿名內部類
    }
});
t1.start();

Thread t2 = new Thread(()->{
    //System.out.println(this);//沒法編譯經過
});
t2.start();

觀察以下代碼,會發現Lambda表達式中的this與所處環境有關,在這裏this是對外部對象的引用。app

class Foo{
    Runnable r1 =  ()->{
       System.out.println(this);
    };
    Runnable r2 =  ()->{
        System.out.println(this);
     };
    
    void test(){
        r1.run();
        r2.run();
    }
}
//測試代碼以下
Foo foo = new Foo();
System.out.println(foo);
foo.test();

從輸出能夠看出,輸出了三個對象其實是同一個對象。ide

Foo@87aac27
Foo@87aac27
Foo@87aac27

3. 標準函數式接口與方法引用

3.1 函數式接口

Java8中爲Iterable引入了默認實現方法default void forEach(Consumer<? super T> action)。用法以下:函數

List<String> strs = Arrays.asList("1","222","33");  //List接口間接繼承了Iterable接口,因此strs也會有forEach方法。
strs.forEach(e->System.out.println(e)); //將strs中的每一個元素迭代輸出

爲何能夠將Lambda表達式e->System.out.println(e)做爲Consumer<? super T> action類型的參數。
先看一下Consumer的代碼post

@FunctionalInterface
public interface Consumer<T> {
     void accept(T t);
     //其餘代碼
}

能夠看到void accept(T t)e->System.out.println(e)形式上是一致的,因此能夠將該Lambda表達式做爲輸入參數。
注意:這裏使用了@FunctionalInterface標註該結構爲函數式接口。也能夠本身建立函數式接口。但要注意函數接口只能有一個抽象方法
以下代碼能夠經過:學習

@FunctionalInterface
interface MyFuncInterface{
    void test();
}

但以下代碼卻沒法編譯經過

@FunctionalInterface
interface MyFuncInterface{
    void test();
    void test1();
}

JDK中大量使用了幾個經常使用的標準函數接口。以下所示:

public interface Consumer<T> {//無返回值,消費傳入的T。可接受e->System.out.println(e)或System.out::println
    void accept(T t);
    //其餘代碼
}
public interface Function<T, R> {//將t轉化爲r。可接受e->Integer.parseInt或Integer::parseInt,將String類型轉化爲int
    R apply(T t);
    //其餘代碼
}
public interface Predicate<T> {//根據傳入t判斷真假。可接受x->x>3或String::equals(與傳入String對象比較,返回True或False)
    boolean test(T t);
    //其餘代碼
}
public interface Supplier<T> {//無輸入參數,直接獲取T。可接受()->Arrays.asList("1","2","3"}或
    T get();
}

3.2 方法引用

前面出現的System.out::println就是方法引用。下面的代碼中,strs.forEach的入參類型爲Consumer<? super T> action
前面已經提到可使用e->System.out.println(e)做爲入參,同時咱們知道System.out.println方法簽名中返回值爲void、
無入參也符合要求,因此咱們可使用System.out::println來替代e->System.out.println(e)。注意:要使用::來引用相關
的方法。
···
List strs = Arrays.asList("1","222","33");
strs.forEach(e->System.out.println(e));
strs.forEach(System.out::println);
···
方法引用不只能夠引用jdk中已有類的方法,還能夠引用自定義類的相關方法。好比:

class Foo{
    <T> void myPrintX(T t) { //必須建立Foo對象才能對非static進行方法引用
        System.out.println("x="+t);
    }
    
    static <T> void myPrint(T t) {
        System.out.println("element="+t);
    }
}


//測試代碼
List<String> strs = Arrays.asList("1","222","33");
strs.forEach(Foo::myPrint);
strs.forEach(new Foo()::myPrintX);

輸出結果爲

element=1
element=222
element=33
x=1
x=222
x=33

4.Lambda、方法引用、標準函數接口與Stream

從Java 8起,能夠將集合中數據放入流並進行管道式操做。
管道式操做包含3部分:

  1. 數據源(集合、數組等)
  2. 0個或多箇中間操做(filter、map等)
  3. 終端操做(forEach、collect、average, sum, min, max, and count)。

中間操做產生的仍是流,那麼經過filter獲得的流還能夠繼續進行filter。
終端操做產生的就不是流了(多是一個List、Map或int等),對一個流進行終端操做後,就不能在進行任何其餘中間操做。
對一個流一旦進行完終端操做,就不能再進行中間操做,運行以下代碼

List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream1 = intList.stream().filter(e->e>3);
stream1.forEach(System.out::println);
Stream<Integer> stream2 = stream1.filter(e->e>4);
stream2.forEach(System.out::println);

會提示stream has already been operated upon or closed

4.1 stream的的filter、map方法與Predicate、Function接口

Predicate接口(boolean test(T t))的做用是根據傳入參數t判斷真假。
Function接口(R apply(T t);)的做用是將T類型的t轉換成R類型。
觀察以下代碼:

List<String> strs = Arrays.asList("1","222", null, "33");
Stream<Integer> stream = strs.stream().filter(e -> e != null).map(Integer::parseInt);
stream.forEach(e -> System.out.println(1 + e));

輸出

2
223
34

其中strs.stream().filter(e->e!=null)的filter方法聲明以下Stream<T> filter(Predicate<? super T> predicate);,即這裏須要一個
Predicate<? super T> predicate類型的參數。前面能夠看到Predicate接口中的方法爲boolean test(T t);,即接受一個t返回
boolean值。e->e!=null符合這樣的要求。
strs.stream().filter(e->e!=null).map(Integer::parseInt);中的map方法聲明以下
Stream<R> map(Function<? super T, ? extends R> mapper)
即這裏須要一個Function<? super T, ? extends R> mapper)類型的參數。前面能夠看到Function接口中的方法爲R apply(T t);
即接受一個類型爲T的元素,將其轉換爲元素R。在這裏實際上就是將String類型元素轉化成int類型元素。Integer::parseInt恰好
符合這種要求。

4.2 Optional中map方法與Function接口

從剛纔的例子中,咱們能夠看Function接口的做用能夠將一個類型的轉換成另一個類型。好比

Student s1 = new Student("zhang san");
String name = s1.getName(); //對應的方法引用是Student::getName()

中Student::getName()至關於Student類型轉換成String類型。
以下代碼中,一個Course有不少Student(stuList),每一個Student有均可以getName()。如今想要獲取該Course中某個學生的姓名。
以往的代碼若是使用course.getStuList().get(i).getName()來獲取某個學生的姓名,看起來代碼風格當然流暢,然而卻沒有正確處理:
course1爲null,get(i)爲null,getName爲null的狀況。那麼必須在整個處理過程編寫大量的判斷null的代碼。
可使用Optional進行改進,即保持了流暢的編碼風格,又能夠正確處理null。
如下代碼中:Optional.ofNullable方法能夠將給定值轉化爲Optional類型(可包含表明給定值的Optional對象,也可包含表明null的Optional對象)

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
class Student{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Student(String name) {
        this.name = name;
    }
    
}
class Course{//課程
    private String name;
    private List<Student> stuList;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void addStudent(Student... stus) {
        for (Student student : stus) {
            stuList.add(student);
        }   
    }
    public Student getStu(int i) {
        return stuList.get(i);
    }
    public List<Student> getStuList() {
        return stuList;
    }
    
    public Course(String name) {
        this.name = name;
        stuList = new ArrayList<>();
    }
    
}

public class TestOptional {
    public static void main(String[] args) {
        Course course = new Course("數學");
        Student s0 = new Student("s1");
        Student s1 = new Student(null);
        Student s2 = null;
        course.addStudent(s0, s1, s2);
        String result = findStudent(course, 0);// orElse,當處理過程當中過程當中有一個null時,即返回orElse中的值
        System.out.println("均不爲空狀況下姓名爲:" + result);
        result = findStudent(course, 1);
        System.out.println("student的name爲null的狀況:" + result);
        result = findStudent(course, 2);
        System.out.println("student爲null的狀況:" + result);
        Course courseNull = null;
        result = findStudent(courseNull, 3);
        System.out.println("course爲null的狀況:" + result);
    }

    private static String findStudent(Course course, int i) {
        Optional<Course> courseNullable = Optional.ofNullable(course);
        String result = courseNullable.map(e -> e.getStu(i)).map(Student::getName).orElse("查詢不到");
        return result;
    }
}

注意:

  1. Optional的map方法入參爲Function類型,因此map(e->e.getStu(0))map(Student::getName)形式都可執行。
  2. Optional的map方法返回值爲Optional類型,因此能夠以鏈式風格map(e->e.getStu(2)).map(Student::getName)流暢的編寫對應代碼。
  3. 該例子中不考慮stuList爲null的狀況,由於只要建立了Course,默認就建立了stuList。
  4. 這裏沒有對不一樣種的null狀況(student爲null,course爲null)進行處理,返回的結果統一是查詢不到,會形成理解上的混淆。

4.3 stream的mapToInt方法與ToIntFunction函數式接口

List<String> strs = Arrays.asList("1", "222", null, "33");
IntStream intStream = strs1.stream().filter(e -> e != null).mapToInt(e -> e.length());
intStream.forEach(System.out::println);

mapToInt(e -> e.length())的mapToInt方法參數類型爲ToIntFunction<? super T> mapper,查詢源代碼ToIntFunction包含方法
int applyAsInt(T value);,即須要一個方法接受T類型輸入參數,而後將其轉化爲int。在這裏,e -> e.length()起到了這個做用。
代碼的做用就是要將求得流中每一個非null的字符串的長度,而後放入intStream中。

4.4 stream的reduce方法與BinaryOperator函數式接口

int[] arr = {1,2,3,4,5};
int x = 0;
for (int i = 0; i < arr.length; i++) {
    x = x + arr[i];
}
System.out.println(x);

這段代碼每回從數組中取出一個元素,而後與前一個元素相加。最後求的全部元素值的和。這類操做常用,可使用stream中的
reduce方法來簡化實現。

List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> intStream1 = intList.stream();
Optional<Integer> result = intStream1.reduce((a, b) -> a + b);
Integer integer = result.get();
System.out.println(integer);// 15

intStream1的reduce方法入參爲BinaryOperator其繼承自接口BiFunction,內有一方法R apply(T t, U u);,將傳入參t與u進行運算,
而後返回一個結果。(a,b)->a+b就知足如此行事,其中a最開始爲流中第1個元素,b爲第2個元素,a+b之後再賦給a,而後b爲第3個元
素,依次類推。
reduce方法還有另一種形式,能夠指定初始值,以下述代碼指定迭代開始的初始值爲10,即a開始爲10,b爲流中第1個元素 。而後
將a+b放入a,b爲流中第2個元素,而後依次類推。

Integer x = intList.stream().reduce(10, (a, b) -> a + b);
System.out.println(x);

輸出

25

其中的reduce(10, (a, b) -> a + b)類型爲T reduce(T identity, BinaryOperator<T> accumulator);,能夠看到該方法的返回值由identity的類型決定,
在這裏由10來決定,即返回值類型應爲Integer。

4.4 使用Supplier接口生成流

Supplier接口(提供者)的定義以下

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

能夠看到,其經過get方法返回一個對象。咱們能夠將Supplier看成一個生成某個對象的工廠。
爲了對流進行一些管道操做的實驗,且由於流不能反覆操做,咱們須要不斷生成內部元素徹底相同的流。
以下代碼中,經過Supplier<Stream<Integer>> factory = () -> Stream.of(1, 2, 3, 4, 5);聲明Supplier類型變量
factory,經過該factory的get方法就能夠不斷生成流,實際上就是不斷調用() -> Stream.of(1, 2, 3, 4, 5);。而
這段() -> Stream.of(1, 2, 3, 4, 5);Lambda表達式形式上是與Supplier標準函數式接口是一致:無入參,有一個返回值。

Supplier<Stream<Integer>> factory = () -> Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream1 = factory.get();
Stream<Integer> stream2 = factory.get();
System.out.println(stream1 == stream2); // false

4.5 流的扁平化-flatmap

如何抽取二維數組Integer[][] arr1 = {{1,2},{2,3}}每一個元素(排除掉重複的元素),即將一、二、3抽取出來?
可使用flatmap方法。

Integer[][] arr1 = {{1,2},{2,3}};
Stream<Integer[]> t1 = Arrays.stream(arr1);//流中每一個元素是一行(一維數組)
Stream<Integer> flatMap = t1.flatMap(Arrays::stream);//扁平化處理後,流中的每個元素是一個Integer
flatMap.distinct().forEach(System.out::println); //distinct()排除掉重複的元素

不過這種方法對基本類型數組,如int[][]就不起做用。不知道爲什麼?
更多參考資料見:

結論

本文使用了幾個例子展現了Java 8中經常使用函數式接口在流的管道操做中的應用,Lambda表達式、方法引用與函數式接口之間的關係。但願你們之後在使用流的管道操做時,能夠知其然也知其因此然。

參考資料:

Java學習筆記(第8版) 林信良
Java Tutorial中的Lambda ExpressionsAggregate Operations
Java 8 Stream Tutorial

相關文章
相關標籤/搜索