Java8閉包


閉包在很多語言中都存在,例如C++,C#。閉包允許我們創建函數指針,並把它們作為參數傳遞,Java編程語言提供了接口的概念,接口中可以定義抽象方法,接口定義了API,並希望用戶或者供應商來實現這些方法,很多時候並不是為一些接口創建獨立的實現類,我們通過寫一個匿名的內部類來寫一個內聯的接口實現,匿名內部類使用相當的廣泛,匿名內部類最常見的場景就是事件處理器了,其次匿名內部類還被用於多線程中,寫匿名內部類而不是創建Runable\Callable接口的實現類。

一個匿名內部類就是一個內聯的給定接口的實現,這個實現類的對象作為參數傳遞給一個方法,然后這個方法將在內部調用傳遞過來的實現類的方法,這種接口叫做回調接口,這個方法叫做回調方法。

匿名內部類很多地方都在使用,在使用的同時存在一些問題

  1. 復雜

    這些類代碼的層級看起來很亂很復雜,稱作Vertical Problem
  2. 不能訪問封裝類的非final成員

    this關鍵字變得很迷惑,如果一個匿名類有一個與其封裝類相同的成員名稱,內部類會覆蓋外部的成員變量,在這種情況下,外部成員在匿名類內部是不可見的,甚至不能通過this來訪問。

    實例說明

        public void test() {  
            String variable = "Outer Method Variable";  
            new Thread(new Runnable() {  
                String variable = "Runnable Class Member";  
                public void run() {  
                    String variable = "Run Method Variable";  
                    System.out.println("->" + variable);  
                    System.out.println("->" + this.variable);  
               }  
            }).start();  
        } 
    輸出
        ->Run Method Variable   
        ->Runnable Class Member 

    這個例子很好的說明了上面的兩個問題,而Lambda表達式幾乎解決上面的所有問題,我們討論Lambda表達式之前,讓我們來看看Functional Interfaces

  3. Funcational Interfaces

    一個只有單個方法的接口,這代表了這個方法的契約。
    The Single method cal exist in the form of multiple abstract methods that are inherited from superinterfaces.But in that case the inherited methods should logically represent a single method or it might redundantly declare a method that is provided by classes like Object,e.g.toString

    > interface Runnable{void run();}
    > interface Foo {boolean equals(Object obj);}
    > interface extends Foo{ int compare(String s1,String s2)}
    > interface Comparetor{
          boolean equals(Object obj);
          int compare(T t1,T t2);
       }
    
    > interface Foo( int m(); Object clone();
    大多數回調接口都是Functional Interfaces,例如Runable,Callable,Comparetor等等,以前被稱作SAM(Single Abstract Method) 
  4. Lambda

    1. Lambda表達式實際上就是匿名類,只不過他們的結構更輕量,Lambda表達式看起來像方法。他們有一個正式的參數列表和這參數的塊體表達式。針對上面的例子進行Lambda改造。
        public class TestLambdaExpression{
          public String variable ="Class level variable";
          public static void main(){
              new TestLambdaExpression().test();
          }
          public void test() {  
            String variable = "Method local Variable";  
            new Thread(() {  
                public void run() -> {
                    System.out.println("->" + variable);  
                    System.out.println("->" + this.variable);  
               }  
            }).start();  
          } 
        }
        輸出
         ->Method local Variable
        ->Class level variable
    可以比較一些使用Lambda表達式和使用匿名內部類的區別,我們可以清楚的看出,使用Lambda表達式的方式寫匿名內部類解決了變量可見性的問題,Lambda表達式不允許創建覆蓋變量。 Lambda表達式的語法包括一個參數列表和“->”,為什么選擇這樣的語法形式,因為目前C#和Scala中通常都是這樣的,也算是遵循了Lambda的通用寫法,這樣的語法基本上解決了匿名類的復雜性,同時也顯得非常的靈活,目標接口類型不是一個表達式的一部分。編譯器會幫助推斷Lambda expression的類型與周圍的環境,Lambda表達式必須有一個目標類型,而它們可以適配任意可能的目標類型,當然類型是一個接口的時候,下面的條件必須滿足,才能編譯正確。

    接口應該是一個Funcational interface

    表達式的參數數量和類型必須與Functional interface中聲明的一致

    拋出的異常表達式必須兼容function interface中方法的拋出異常聲明

    返回值類型必須兼容function interface 中方法的返回值類型

    由於編譯器可以通過目標類型的聲明中得知參數類型和個數,所以在Lambda表達式中可以省略類型的聲明。

    例如

    Comparetor c = (s1,s2) –> s1.comparteToIgnoreCase(s2);

    而且,如果目標類型中聲明的方法只有一個參數,那么小括號也可以不寫,例如

    ActionListenr listenr = event –> event.getWhen();

    一個很明顯的問題來了,為什么Lambda不需要指定方法名呢?因為Lambda expression只能用戶Functional interface,而functional interface 只有一個方法。當我們確定一個functional interface來創建Lambda表達式的時候,編譯器可以感知functional interface中的方法的簽名,並且檢查給定的表達式是否匹配。Lambda表達式的語法是上下文相關的,但並非在Java8中才出現,Java 7中添加了diamond operators也有這個概念,通過上下文推斷類型。

    void invoke (Runable r) {r.run()}

    Futrue invoke (Callable c) {return c.compute()}

    Future s = invoke (() –> "done");//這個Lambda Expression顯然是調用了帶有futrue的這個接口

  5. 方法引用

    方法引用被用作引用一個方法而不調用它,Lambda表達式允許我們定義一個匿名的方法,並將它作為Functional Inteface的一個實例。方法引用跟Lambda Expression很想,它們都需要一個目標類型,但是不同的方法引用不提供方法的實現,它們引用一個已經存在的類或者對象的方法。
    1、System::getProperty
    2、"abc"::length
    3、String::length
    4、super::toString
    5、Arraylist::new
    這里引用了一個新的操作符"::"(雙冒號)。目標引用或者說接收者被放在提供者和分隔符的后面,這形成了一個表達式,它能夠引用一個方法。下面通過一個例子來了解這個操作符。
    這是一個Employee數組的排序程序
        
        import java.util.Arrays;  
        import java.util.List;  
        import java.util.concurrent.ExecutionException;  
        import java.util.concurrent.Future;  
        public class MethodReference {  
            public static void main (String[] ar){  
                Employee[] employees = {
    		new Employee("Nick"), 
    		new Employee("Robin"), 
    		new Employee("Josh"), 
    		new Employee("Andy"), 
    		new Employee("Mark")
    	    };  
                System.out.println("Before Sort:");  
                dumpEmployee(employees);  
                Arrays.sort(employees, Employee::myCompare);  
                System.out.println("After Sort:");  
                dumpEmployee(employees);  
            }  
            public static void dumpEmployee(Employee[] employees){  
                for(Employee emp : Arrays.asList(employees)){  
                    System.out.print(emp.name+", ");  
                }  
                System.out.println();  
            }  
        }  
        class Employee {  
            String name;  
            Employee(String name) {  
              this.name = name;  
            }  
            public static int myCompare(Employee emp1, Employee emp2) {  
                return emp1.name.compareTo(emp2.name);  
            }  
        }
    輸出:
    1. Before Sort: Nick, Robin, Josh, Andy, Mark,
    2. After Sort: Andy, Josh, Mark, Nick, Robin,
  6. 構造方法引用

    先看看實例

        public class ConstructorReference {  
            public static void main(String[] ar){  
                MyInterface in = MyClass::new;  
                System.out.println("->"+in.getMeMyObject());  
            }  
        }  
        interface MyInterface{  
            MyClass getMeMyObject();  
        }  
        class MyClass{  
            MyClass(){}  
        } 
    輸出
    ->com.MyClass@34e5307e
    這看起來有點神奇吧,這個接口和這個類除了接口中聲明的方法的返回值是MyClass類型的,沒有任何關系。這個例子又激起了我心中的另一個問題:怎樣實例化一個帶參數的構造方法引用?看看下面的程序:
    public class ConstructorReference {  
        public static void main(String[] ar){  
            EmlpoyeeProvider provider = Employee::new;  
            Employee emp = provider.getMeEmployee("John", 30);  
            System.out.println("->Employee Name: "+emp.name);  
            System.out.println("->Employee Age: "+emp.age);  
       }  
    }  
    interface EmlpoyeeProvider{  
        Employee getMeEmployee(String s, Integer i);  
    }  
    class Employee{  
        String name;  
        Integer age;  
        Employee (String name, Integer age){  
            this.name = name;  
            this.age = age;  
        }  
    }
    輸出是:
        ->Employee Name: John  
        ->Employee Age: 30 
  7. Default Method

    Java8中將會引入一個叫做默認方法的概念,早期的Java版本的接口擁有非常的嚴格的接口,接口包含了一些抽象方法的聲明,所有非抽象的實現類必須要提供所有這些抽象方法的實現,甚至是這些方法沒有用或者不合適出現在一些特殊的實現類中。在即將到來的Java 版本中,允許我們在接口中定義方法的默認實現。

        public class DefaultMethods {  
         public static void main(String[] ar){  
          NormalInterface instance = new NormalInterfaceImpl();  
          instance.myNormalMethod();  
          instance.myDefaultMethod();  
         }  
        }  
        interface NormalInterface{  
         void myNormalMethod();  
         void myDefaultMethod () default{  
          System.out.println("-> myDefaultMethod");  
         }  
        }  
        class NormalInterfaceImpl implements NormalInterface{  
         @Override 
         public void myNormalMethod() {  
          System.out.println("-> myNormalMethod");  
         }  
        }  
    輸出
    -> myDefaultMethod
    在這個例子中,ParentInterface 定義了兩個方法,一個是正常的,一個是有默認實現的,子接口只是簡單的反了過來,給第一個方法添加了默認實現,給第二個方法移除了默認實現。
    設想一個類繼承了類 C ,實現了接口 I ,而且 C 有一個方法,而且跟I中的一個提供默認方法的方法是重載兼容的。在這種情況下,C中的方法會優先於I中的默認方法,甚至C中的方法是抽象的時候,仍然是優先的。
    public class DefaultMethods {  
     public static void main(String[] ar){  
      Interfaxe impl = new NormalInterfaceImpl();  
      impl.defaultMethod();  
     }  
    }  
    class ParentClass{  
     public void defaultMethod() {  
      System.out.println("->ParentClass");  
     }  
    }  
    interface Interfaxe{  
     public void defaultMethod() default{  
      System.out.println("->Interfaxe");  
     }  
    }  
    class NormalInterfaceImpl extends ParentClass implements Interfaxe{
    }
    輸出:
       -> ParentClass 
    
    第二個例子是,實現了兩個不同的接口,但是兩個接口中都提供了相同的具有默認實現的方法的聲明。在這種情況下,編譯器將會搞不清楚怎么回事,實現類必須選擇兩個的其中一個實現。這可以通過如下的方式來使用super來搞定。
        public class DefaultMethods {  
         public static void main(String[] ar){  
          FirstInterface impl = new NormalInterfaceImpl();  
          impl.defaultMethod();  
         }  
        }  
        interface FirstInterface{  
         public void defaultMethod() default{  
          System.out.println("->FirstInterface");  
         }  
        }  
        interface SecondInterface{  
         public void defaultMethod() default{  
          System.out.println("->SecondInterface");  
         }  
        }  
        class NormalInterfaceImpl implements FirstInterface, SecondInterface{  
         public void defaultMethod(){  
          SecondInterface.super.defaultMethod();  
         }  
        } 
    
    輸出
        ->SecondInterface 
    


免責聲明!

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



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