深入學習Java8 Lambda (default method, lambda, function reference, java.util.function 包)


Java 8 Lambda 、MethodReference、function包

       多年前,學校講述C#時,就已經知道有Lambda,也驚喜於它的方便,將函數式編程方式和面向對象式編程基於一身。此外在使用OGNL庫時,也是知道它可以支持Lambda。但是OGNL中的lambda畢竟不是java語言本身支持,操作有諸多不便,使用Lambda不就是為了方便嗎。但是Java中遲遲沒有支持Lambda。直到Java 8的來臨,總算給Java注入了這個新鮮血液。

1、default Method or static method in interface

1.1 default method

       Java 8 之前,為一個已有的類庫增加功能是非常困難的。具體的說,接口在發布之后就已經被定型,除非我們能夠一次性更新所有該接口的實現,否則向接口添加方法就會破壞現有的接口實現。Default method的目標即是解決這個問題,使得接口在發布之后仍能被逐步演化。

       Default method,即接口中聲明的方法是有方法體的方法,並且需要使用default關鍵字來修飾。

       舉個例子:java.util.Collection是線性表數據結構的統一接口,java 8里為它加上4個方法: removeIf(Predicate p)、spliterator()、stream()、parallelStream()。如果沒有default method,

就得對java.util.Collection、java.util.AbstractCollection、java.util.Set 等,還有很多用戶自定義的集合添加這4個方法,如果不添加,這些代碼在jdk8上運行就會失敗。

       而使用default method,就可以完美解決這個問題。只要在java.util.Collection中將這4個新加的方法設置為default即可。

       在引入default方法后,可能會帶來如下問題:

1)一個類ImplementClass直接實現(中間沒有父類)了兩個接口 InterfaceA, InterfaceB,這兩個接口中有同一個方法: void hello()。那么ImplementClass必須重寫方法hello,不然不知道到底繼承哪個,這里不會去管接口中的default

2) 一個類ImplementClassA 直接實現了接口InterfaceA,InterfaceA中定義了一個非default的void hello()方法。有另外一個接口InterfaceB,定義了一個default方法void hello();現在有一個實現類ImplementClassAB,extends了ImplementClassA,implements了InterfaceB, 且ImplementClassAB沒有重寫void hello()方法。那么在調用ImplementClassAB#hello()時,到底是調用的是ImplementClassA#hello(),還是調用的是InterfaceB#hello()呢?

為了解決這個問題,有這么一項原則:類的方法優先調用。所以應該是調用ImplementClassA#hello()。

  

1.2 static method

       與此同時,java8在接口中也引入了static method,它也是有方法體的,需要使用static關鍵字修飾。另外要特別注意的是:如果一個類中定義static方法,那么訪問這個方法可以使用ClassName.staticMethodName、instance.staticMethodName兩種方式來訪問static方法,但是對於接口中定義的靜態方法,只能通過InterfaceName.staticMethodName方式來訪問。

 

2、Lambda

Lambda官方教程地址:

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

2.1 Lambda用在什么地方?

 

Anonymous Classes, shows you how to implement a base class without giving it a name.
Although this is often more concise than a named class, for classes with only one method,
even an anonymous class seems a bit excessive and cumbersome.
Lambda expressions let you express instances of single-method classes more compactly.

從這段話可以看出Lambda主要用於替代匿名類,並以簡約而清晰的方式展現業務處理邏輯。

 

2.2 語法格式

將匿名類的寫法轉換成一行代碼的格式:

(arguments) -> {method body}

 

2.3 demo

下面用一個例子來說明:

    public static interface Computer{
        public double compute(double a, int b);
    }

    public static void executeCompute(List<Integer> list, Computer computer){
        double result = 1D;
        if(list!=null){
            for (Integer i : list) {
                if(i==0){
                    continue;
                }
                result = computer.compute(result, i);
            }
        }
        System.out.println(result);
    }

我們可以在Computer的實現類中完成各種各樣的操作,例如求和、乘積等。如果使用匿名類來實現的話,代碼量會有不少,即便是我們代碼寫的很規范,變量名命名也可以做到見名知意,但是代碼看起來仍舊是比較凌亂的: 

    @Test
    public void test0(){
        Computer getSum = new Computer() {
            @Override
            public double compute(double a, int b) {
                if(b!=0) {
                    return a + b;
                }
                return a;
            }
        };
        executeCompute(nums, getSum);

        Computer getMult = new Computer() {
            @Override
            public double compute(double a, int b) {
                return a * b;
            }
        };
        executeCompute(nums, getMult);
}

如果使用Lambda來編寫的話,看起來就簡明清晰了: 

@Test
public void test0(){
    executeCompute(nums, (a,b)->{ return (b!=0) ? (a+b) : a;});
    executeCompute(nums, (a,b)->{ return a * b;} );
}

從上面的demo看出,我們甚至不需要去設置參數的類型,只需要一個參數列表即可。 

如果要處理的參數是復雜類型怎么辦呢?

    /**
     * Operate an Object use an Lambda
     */
    static class Person{
        private int age;
        private String name;
        Person(String name, int age){
            this.name = name;
            this.age = age;
        }
        @Override
        public String toString() {
            return "name: "+name + " age: "+age;
        }

    }


    private static interface InfoGetter {
        public String get(Person p);
        public default String get2(int a, Person p){return "";};
        public default String get3(Person p){return "";};
    }

    static void show(List<Person> persons, InfoGetter infoGetter){
        persons.forEach((person)->{
            System.out.println(infoGetter.get(person));
        });
    }

測試代碼: 

 

    static List<Person> persons = new ArrayList<>();
    static{
        for(int i = 0 ; i< 10; i++) {
            persons.add(new Person("hello_"+i, i));
        }
    }

    @Test
    public void test1(){
        LambdaTests ctx = this;

        InfoGetter getter1= (Person person)->{
            System.out.println(ctx == this);
            Method[] methods = this.getClass().getMethods();
            return person.toString();
        };
}

 

更多demo,參見:

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html

 

2.4 Lambda幾點注意事項(FunctionalInterface)

1、lambda用於替代匿名內部類的實例,它的本質是運行是由JVM產生匿名內部類,並生成一個實例。

2、一個接口(只能是接口),想要使用Lambda來替代,最多只能有一個抽象方法。

3、一個lambda通常用作某個方法的參數

4、lambda內部的this,是產生lambda時的對象。

5、lambda表達式的參數,可以有類型,也可以不指定。

 

2.5 Lambda 與匿名類的區別

1)一個是對象,一個是類。Lamdba表達式,可以看做是一個匿名類的實例。

2)this關鍵字含義不同。在匿名類中訪問外部類的實例,需要使用outterclass.this來引用,而在lambda中可以直接使用this來引用。

 

2.6 Lambda 的單例與多例

上面知道lamdba是一個實例,那么我在一個方法內部,寫上幾個完全一樣的lambda,他們是否是同一個對象呢?什么情況下,你寫的lambda永遠是同一個實例呢?

為了找到答案,改造測試用例如下:

    @Test
    public void test1(){
        LambdaTests ctx = this;

        InfoGetter getter1= (Person person)->{
            System.out.println(ctx == this);
            Method[] methods = this.getClass().getMethods();
            return person.toString();
        };

        InfoGetter getter2= (Person person)->{
            System.out.println(ctx == this);
            return person.toString();
        };

        show(persons, getter1);
        System.out.println(getter1==getter2);

        InfoGetter getter3 = getInfoGetter();
        InfoGetter getter4 = getInfoGetter();
        System.out.println(getter3==getter4);

        InfoGetter getter5 = getInfoGetter("a");
        InfoGetter getter6 = getInfoGetter("b");
        System.out.println(getter3==getter5);
        System.out.println(getter5==getter6);

        InfoGetter getter7 = getInfoGetter("a",1);
        InfoGetter getter8 = getInfoGetter("b",2);
        System.out.println(getter3==getter5);
        System.out.println(getter7==getter8);
    }

    static InfoGetter getInfoGetter(){
        return (p)->"";
    }

    static InfoGetter getInfoGetter(String str){
        System.out.println(str);
        return (p)->{int a = 1; return "" + a;};
    }
    static InfoGetter getInfoGetter(String str, int i){
        System.out.println(str);
        return (p)->str + i;
    }

調試截圖: 

 

從上面的幾個測試用例上,可以得出如下結論:

1)每寫一次lambda表達式,就代表創建一個實例(不管表達式里,會引用什么內容)。

2)想要得到一個單例的lambda實例,可以在static方法中聲明該lambda,並且該lambda方法體中,除了lambda代表的方法的參數外,不能用其他的變量。

 

3、MethodReference

3.1 什么是MethodReference ?

Person p = new Person(); 這行代碼里, 我們稱p為對象的引用。那么什么是method 引用呢?故名思議,一個變量指向了一個方法,就稱為方法引用。

官方文檔:https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

java中的method分為static,非static的,構造器本身也是一個方法。要想引用到這些方法,該如何做呢?

kind example
引用到一個static方法

::static方法

例如:Math::abs

引用到一個對象的某個方法

對象::方法

例如:System.out::print

引用到一個類的某個方法

類:方法

例如:PrintWriter::print

引用到一個構造函數

類:new

例如:String::new

  

3.2 何時可以使用MethodReference?

       Lambda用於簡化匿名類的寫法。有時候,我們寫的表達式中,只是調用了另外一個類的某個方法。此時我們連lamdba表達式都可以省略了,使用MethodReference來表示即可。

 

3.3 demo

/***
 * Lambda 的本質是在運行時(編譯時不會產生的)產生一個匿名內部類
 * MethodReference 的本質是產生一個Lambda,並在lambda里調用你指定的方法。
 * 所以如果你要寫的Lambda只是用於調用另外一個方法時,你完全可以用MethodReference來替代的。
 */
public class MethodReferenceTest {

    private static interface MyPrinter {
        public void print(Serializable o);
    }

    private static void printArray(Integer[] arr, MyPrinter printer){
        if(arr==null){
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            printer.print(arr[i]);
            printer.print(" ");
        }
        printer.print("\n");
    }

    @Test
    public void test0(){
        // 原始寫法
        Integer[] arr1 = new Integer[]{12,23,545,2,345,0, -1};
        Comparator<Integer> comparator1 = new Comparator<Integer>() {
            @Override
            public int compare(Integer t1, Integer t2) {
                return t1.compareTo(t2);
            }
        };
        Arrays.sort(arr1, comparator1);
        printArray(arr1, new MyPrinter() {
            @Override
            public void print(Serializable o) {
                System.out.print(o);
            }
        });

        // 使用lambda的寫法
        Integer[] arr2 = new Integer[]{12,23,545,2,345,0, -1};
        Arrays.sort(arr2, (Integer x, Integer y)->{return x.compareTo(y);});
        printArray(arr2, (x)->System.out.print(x));

        // 使用MethodReference的寫法
        Comparator<Integer> comparator3 = Integer::compareTo;
        Integer[] arr3 = new Integer[]{12,23,545,2,345,0, -1};
        Arrays.sort(arr3, comparator3);
        MyPrinter printer = System.out::print;
        printArray(arr3, printer);

        printer.print("test finish\n");

        Comparator<Integer> comparator4 = (Integer x,Integer y)->{return x.compareTo(y);};

    }
}

上面的例子中,聲明了一個接口MyPrinter,然后在printArray方法中使用該接口。那么此后在調用printArray方法時,就可以使用lambda了。當lambda的方法體只是調用某個方法是,可以直接使用method refence來替代,所以就可以直接使用System.out::print來執行。此外,Arrays.sort的第二個參數是一個Comparator接口,此時我們又可以使用lambda來實現一個Comparator了,在我們要實現的Comparator里,只需要調用comparTo方法,所以我們又可以使用Method reference來替代lambda了。 

 

4、java.util.function包

       通過上面的學習知道,只有接口才可能被lambda替代,抽象類是不行的。很多時候,要使用的接口里的方法,也就那一兩個。如果每一次我們想要使用lambda時,都去聲明一個接口豈不很麻煩嗎?好在JDK里內置了可能常用的接口,在java.util.function包下。

       來看看JDK doc里如何描述這個包的:

Functional interfaces (java.util.function包下的這些接口) provide target types (函數的參數,被稱為target) for lambda expressions and method references. Each functional interface has a single abstract method, called the functional method for that functional interface, to which the lambda expression's parameter and return types are matched or adapted.  這個意思再明白不過了。

    在學習這些接口之前,先要知道幾個英文單詞的含義:Nilary 零元,Unary 一元,Binary 二元,Ternary 三元,Quaternary 四元。對於一個算子來說,一個參數,就是一元運算;2個參數就是二元運算。

       在java.util.function包下提供了很多接口(我們可以直接理解為函數),主要分為下面幾類:

1)Predicate 為target type提供斷言。參數 T,返回 boolean。

2)Consumer 消費target type。參數 T,無返回值(void)。

3)Function 對target type做轉換。參數T,返回R。

4)Supplier 供應target,可以理解為target的factory。無參,返回T。

5)UnaryOperator 一元運算。繼承Function接口。參數T,返回T。

6)BinaryOperator 二元運算。參數 T、U,返回R。

java.util.function下的接口最多支持到二元運算。有了這些接口,我們就可以省去創建接口的功夫,而直接使用lambda了。

如果自定義functional interface呢?其實很簡單,定義一個可以用作lambda的接口,然后使用@FunctionalInterface 注解標注即可,當然這個注解並不是必須用的,只是使用了注解后,編譯器會幫你檢查一個FunctionInterface的必要條件。

 

5、綜合Demo

下面就綜合上面這些內容,用一個demo演示一下:

提供一個基礎數據庫表:

    private static class Person {
        String id;
        String name;
        int age;
        Gender gender;

        Person(String id, String name, int age, Gender gender){
            this.id = id;
            this.name = name;
            this.age = age;
            this.gender =gender;
        }

        @Override
        public String toString() {
            return "id: "+id+"\tname: "+name + "\tage: "+age+"\tgender: "+gender;
        }
    }

    enum Gender{
        man,woman
    }

    static Collection<Person> persons = new ArrayList<>();
    static{
        for (int i = 0; i < 100; i++) {
            persons.add(new Person("id_"+i, "name_"+i, i, i%3==0 ? Gender.woman : Gender.man));
        }
    }

篩選出滿足條件的行: 

    public static <Row> Collection<Row> doSelection(Collection<Row> table, Predicate<Row> rowWhere) {
        Predicate isNull = (c) -> c == null;
        if(isNull.test(table)){
            return Collections.EMPTY_LIST;
        }

        Collection<Row> result = new ArrayList<>();
        table.forEach(row -> {
            boolean rowAvailable = row !=null && (rowWhere !=null ? rowWhere.test(row) : true);
            if (rowAvailable) {
                result.add(row);
            }
        });
        return result;
    }

投映出滿足條件的列:

    public static <Row, Column> Collection<Column> doProjection(Collection<Row> table, Predicate<Row> rowWhere, String columnName, BiFunction<Row, String, Column> columnGetter, Predicate<Column> columnPredicate) {
        Predicate isNull = (c) -> c == null;
        if(isNull.test(table)){
            return Collections.EMPTY_LIST;
        }

        Collection<Column> result = new ArrayList<>();
        table.forEach(row -> {
            boolean rowAvailable = row !=null && (rowWhere !=null ? rowWhere.test(row) : true);
            if (rowAvailable) {
                Column column = columnGetter.apply(row, columnName);
                boolean columnAvailable =column!=null && (columnPredicate!=null?columnPredicate.test(column) : true);
                if(columnAvailable) {
                    result.add(column);
                }
            }
        });
        return result;
    }

測試:

    @Test
    public void test0() {
        Collection<Person> selection = doSelection(persons, (person ->person.age>84 && person.gender==Gender.man));
        Consumer<Person> printer =System.out::println;
        selection.forEach(printer);
    }

    @Test
    public void test1() {
        Collection<String> projection = doProjection(persons,
                (Person person) -> {return person.age>84 && person.gender==Gender.man;},
                "name",
                (Person person, String filedName)->{return "name".equals(filedName) ? person.name : "";},
                null
                );
        Consumer<String> printer = System.out::println;
        projection.forEach(printer);
    }

結果: 


id: id_85    name: name_85    age: 85    gender: man
id: id_86    name: name_86    age: 86    gender: man
id: id_88    name: name_88    age: 88    gender: man
id: id_89    name: name_89    age: 89    gender: man
id: id_91    name: name_91    age: 91    gender: man
id: id_92    name: name_92    age: 92    gender: man
id: id_94    name: name_94    age: 94    gender: man
id: id_95    name: name_95    age: 95    gender: man
id: id_97    name: name_97    age: 97    gender: man
id: id_98    name: name_98    age: 98    gender: man
name_85
name_86
name_88
name_89
name_91
name_92
name_94
name_95
name_97
name_98

Process finished with exit code 0

 

6、Lambda 翻譯與運行、性能

Lambad到底是怎樣翻譯的,又是如何保證this執行的是創建lambda的那個上下問題的。翻譯的工作整個程序運行性能有多大的影響?這些問題都將在后續文章補充。

如果來不及等待,可以先參考:http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html


  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM