Lambda表達式只是一顆語法糖?

JDK在不斷升級過程中,要致力解決的問題之一就是讓程序代碼變得更加簡潔。JDK8引入的Lambda表達式在簡化程序代碼方面大顯身手,它用簡明扼要的語法來表達某種功能包含的操作。在程序遍歷訪問集合中元素的場合,運用Lambda表達式可以大大簡化操縱集合的程序代碼。

Lambda表達式的基本用法

我們先看一個程序示例

public class SimpleTest {
    public static void main(String[] args) {
        String [] data={"Tom","Mike","Mary","Linda","Jack"};
        List<String> names=new ArrayList<>(data);

        //方式一:傳統的遍歷集合的方式
        for (String name : names) {
            System.out.println(name);
        }

        //方式二:使用Lambda表達式
        names.forEach((name)-> System.out.println(name));

        //方式三:使用Lambda表達式
        names.forEach(System.out::print);
    }
}

比較3種遍歷集合的代碼,不難發現,使用Lambda表達式可以簡化程序代碼。Lambda表達式的基本語法爲:

(Type param1, Type param2,..., TypeN paramN)-> {
	statment1;
	statment2;
	//...
	return statmentM;
}

從Lambda表達式的基本語法我們可以看出,Lambda表達式可以理解爲一段帶有輸入參數的可執行語句塊,這種語法表達方式也可稱爲函數式表達。
上面示例的SimpleTest類中的方式二的Lambda表達式的完整語法應該是

(String name)->{
	System.out.println(name);
	return;
}

Lambda表達式還有各種簡化版:
(1)參數類型可以省略。在絕大多數情況下,編譯器都可以從上下文環境中聰明地推斷出Lambda表達式的參數類型,例如,對於以上Lambda表達式,編譯器能推斷出name變量的類型爲String,因此Lambda表達式可以簡化爲:

(name)->{
	System.out.println(name);
	return ;
}

(2)當Lambda表達式的參數個數只有一個時,可以省略小括號。以上Lambda表達式可以簡化爲:

name->{
	System.out.println(name);
	return;
}

(3)當Lambda表達式只包含一條語句時,可以省略大括號、語句結尾的分號。此外,當return語句沒有返回值時也可以省略。以上Lambda表達式可以簡化爲:

name-> System.out.println(name);

(4)Lambda表達式中符號“->”後面也可以僅包含一個普通的表達式,語法爲:

(Type1 param1,Type2 param2,...,TypeN paramN)->(expression)

例如:

(int a,int b)->(a*b+2)

示例SimpleTest類中的方式三還通過符號“::”來直接調用println()方法

name.forEach(System.out::println);

用Lambda代替內部類

Lambda表達式的一個重要用武之地是代替內部類。例如下面的示例的InnerTester類中,用3種方式創建了線程。其中方式二和方式三使用了Lambda表達式。

public class InnerTester {
    public static void main(String[] args) {
        //方式一:使用匿名內部類
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        }).start();

        //方式二:使用Lambda表達式
        new Thread(()-> System.out.println("Hello World")).start();

        //方式三:使用Lambda表達式
        Runnable race=()-> System.out.println("Hello World");
        new Thread(race).start();
    }
}

方式二和方式三的Lambda表達式相當於創建了實現Runnable接口的匿名對象,由於Runnable接口的run()方法不帶參數,因此,Lambda表達式的參數列表也相應爲空“()”,Lambda表達式中符號“->”後面的可執行語句塊相當於run()方法的方法體。

Lambda表達式和集合的forEach()方法

從JDK1.5開始,Java集合都實現了java.util.Iterable接口,它的forEach()方法能夠遍歷集合中的每個元素。forEach()方法的完整定義如下:

default void forEach(Consumer<? super T> action)

forEach()方法有一個Consumer接口類型的action參數,它包含了對集合中每個元素的具體操作行爲。action參數所引用的Consumer實例必須實現Consumer接口的accept(T t)方法,在該方法中指定對參數t所執行的具體操作。
例如以下forEach()方法中Lambda表達式相當於Consumer類型的匿名對象,它指定對每個元素的操作爲打印這個元素:

names.forEach((name)->System.out.println(name));

假定有一個Person類具有name屬性和age屬性,並且提供了相應的getName()、getAge()、setName()和setAge()方法,參見示例

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

下面的EachTester類創建了一個存放Person對象的List列表,並且在遍歷這個列表時,指定了更加複雜的操作行爲:先把每個Person對象的age屬性加1,然後打印這個Person對象的信息。

public class EachTester {
    public static void main(String[] args) {
        List<Person> persons=new ArrayList<Person>(){
            {
                add(new Person("Tom",21));
                add(new Person("Mike",32));
                add(new Person("Linda",19));
            }
        };
        persons.forEach((Person p)->{//Lambda表達式相當於是consumer類型的匿名對象
            //指定對每個元素的具體操作
            p.setAge(p.getAge()+1);
            System.out.println(p.getName()+":"+p.getAge());
        });
    }
}

以上Lambda表達式相當於創建了一個Consumer類型的匿名對象,並實現了Consumer接口的accept(T t)方法,此處傳給accept(T t)方法的參數爲Person對象。在Lambda表達式中符號“->”後的可執行語句塊相當於accept(T t)方法的方法體。

用Lambda表達式對集合進行排序

下面的示例SortTester類提供了3種爲集合排序的方式。其中方式二和方式三都採用了Lambda表達式。

public class SortTester {
    public static void main(String[] args) {
        String[] data={"Tom","Mike","Mary","Linda","Jack"};
        List<String> names= Arrays.asList(data);
        
        //方式一:通過創建匿名的Comparator實例來排序
        Comparator<String> cp=new Comparator<String>(){
          @Override
          public int compare(String s1, String s2){
              return (s1.compareTo(s2));
          }  
        };
        Collections.sort(names,cp);
        
        //方式二:用Lambda表達式來排序
        Comparator<String> sortByName=(String s1,String s2)->(s1.compareTo(s2));
        Collections.sort(names,sortByName);
        
        //方式三:用Lambda表達式來排序
        Collections.sort(names,(String s1,String s2)->(s1.compareTo(s2)));
        
        names.forEach(System.out::println);
    }
}

以上方式二和方式三的Lambda表達式相當於創建了Comparator類型的匿名對象。由於Comparator接口的compare(T o1,T o2)方法有兩個參數,所以Lambda表達式也有兩個相應的參數(String s1,String s2),Lambda表達式中符號“->”後面的表達式“s1.compareTo(s2)”相當於compare(T o1,T o2)的方法體。

Lambda表達式和Stream API聯合使用

Java的輸入和輸出中ByteArrayInputStream(字節輸入流)採用了適配器模式,它爲字節數組提供了流接口,使得程序可以按照操作流的方式來訪問字節數組。從JDK8開始,專門抽象出了java.util.stream.Stream流接口,它可以充當Java集合的適配器,使得程序能夠按照操作流的方式來訪問集合中的元素。
Stream 接口提供了一組功能強大的操縱集合的方法:

  • filter(Predicate<?super T> predicate): 對集合中的元素進行過濾,返回包含符合條件的元素的流
  • forEach(Consumer<? super T> action): 遍歷集合中的元素
  • limit(long maxSize) : 返回參數maxSize所指定個數的元素
  • max(Comparator<? super T> comparator): 根據參數指定的比較規則,返回集合中最大的元素
  • min(Comparator<? super T> cmparator): 根據參數指定的比較規則,返回集合中最小的元素
  • sorted(): 對集合中的元素自然排序
  • sorted(Comparator<? super T> comparator): 根據參數指定的比較規則,對集合中的元素排序
  • mapToInt(ToIntFunction<? super T> mapper): 把當前的流映射爲int類型的流,返回一個IntStream對象。ToIntFunction接口類型的參數指定映射方式。ToIntFunction接口有一個返回值爲int類型的applyAsInt(T value)方法,該方法把參數value映射爲int類型的方式
  • mapToLong(ToLongFunction<? super T> mapper): 把當前的流映射爲long類型的流,返回一個LongStream對象。ToLongFunction接口類型的參數指定映射方式。ToLongFunction接口有一個返回值爲long類型的applyAsLong(T value)方法,該方法把參數value映射爲long類型的方式
  • toArray(): 返回包含集合中所有元素的對象數組
    Collection接口的stream()方法返回一個Stream對象,程序可以通過這個Stream對象操縱集合中的元素。
    下面示例的ColTester類先創建了包含Person對象的ArrayList列表,接着調用它的stream()方法得到一個流,接着再對流中元素進行過濾和排序等操作。
public class ColTester {
    public static void main(String[] args) {
        List<Person> persons=new ArrayList<Person>(){
            { //匿名類初始化代碼
                add(new Person("Tom", 21));
                add(new Person("Mike", 32));
                add(new Person("Linda", 19));
                add(new Person("Mary", 29));
            }
        };

        persons.stream()
                .filter(p -> p.getAge()>20) //過濾條件爲年齡大於20
                .forEach(p -> System.out.println(p.getName()+":"+p.getAge()));

        persons.stream()
                .sorted((p1,p2)->(p1.getAge()-p2.getAge()))//按照年齡排序
                .limit(3)    //取出3個元素
                .forEach(p -> System.out.println(p.getName()+":"+p.getAge()));

        int maxAge=persons.parallelStream() //獲得並行流
                            //把包含Person對象的流映射爲保存其age屬性的int類型流
                            .mapToInt(p -> p.getAge())
                            .max()
                            .getAsInt();
        System.out.println("Max Age:"+maxAge);

    }
}

在ColTester類的main()方法中,persons.stream()方法返回一個Stream對象,接下來調用Stream對象的filter()、sorted()和forEach()方法時,傳入的都是Lambda表達式。
Stream接口的filter()方法的完整聲明爲:

Stream<T> filter(Predicate<? super T> predicate)

以上filter()方法有一個Predicate類型參數,用來指明過濾數據的條件。Predicate接口的test(T t)方法判斷參數t是否符合過濾條件,如果符合就返回true,否則返回false。以上程序代碼“filter(p->p.getAge()>20)”通過Lamda表達式創建了一個Predicate匿名對象,把它傳給filter()方法。Lambda表達式中符合“->”後面的表達式“p.getAge()>20”相當於是test(T t)方法的方法體。
Collection接口的parallelStream()方法返回一個採用並行處理機制的Stream對象。當集合中有大批量數據時,爲了提高處理集合中元素的效率,可以調用此方法來得到Stream對象,它的內部實現會開啓多個線程來併發處理數據。
在ColTester類的main()方法中,還調用persons.parallelStream()方法得到一個並行流,接下來再調用流的mapToInt()方法,把當前存放Person對象的流映射爲存放其age屬性的int類型的流(即IntStream類型對象),然後再調用IntStream流的max()方法返回最大值,該返回值是空指針安全的OptionalInt類型的對象,再調用OptionalInt對象的getAsInt()方法得到OptionalInt對象所包裝的int基本類型的值。

Lambda表達式可操縱的變量作用域

Lambda表達式可以訪問它所屬的外部類的變量,包括外部類的實例變量、靜態變量和局部變量。此外,Lambda表達式還可以引用this關鍵字,this關鍵字實際上引用的是外部類的實例。下面示例ScopeTester演示了Lambda表達式對各種變量及this關鍵字的引用

public class ScopeTester {
    int var1=0; //實例變量
    public void test(){
        String [] data={"Tom","Mike","Mary"};
        List<String> names= Arrays.asList(data);

        char var2=',';
        //以下這行代碼編譯出錯,不允許改變var2最終變量的值
        //var2='';

        //使用lambda表達式
        names.forEach((name)->{
            var1++;   //訪問並修改實例變量var1
            //通過this訪問實例變量var1,訪問局部變量var2
            System.out.println(this.var1+":"+name+var2);
        });
    }

    public static void main(String[] args) {
        new ScopeTester().test();
    }
}

在以上示例中,var1變量時ScopeTester類的實例變量,var2變量時test()方法的局部變量,在Lambda表達式中可以直接訪問這兩個變量,還可以通過“this.var1”的方式來訪問var1實例變量。
值得注意的是,在Lambda表達式中訪問的局部變量必須符合以下兩個條件之一:

  • 條件一: 最終局部變量,即用final修飾的局部變量
  • 條件二: 實際上最終局部變量,即雖然沒有被final修飾,但在程序中不會改變局部變量的值
    例如以上ScopeTester類中的“var2=‘ ’;”語句試圖修改var2局部變量的值,這會導致編譯錯誤。因爲Lambda表達式會訪問var2局部變量,所以編譯器不允許修改var2變量的值。

Lambda表達式中的方法引用

下面的兩種Lambda表達式是等價的:

names.forEach((name)->System.out.println(name));
或者:
names.forEach(System.out::println);

在編譯器能根據上下文來推斷Lambda表達式的參數的場合,可以在Lambda表達式中省略參數,直接通過“::”符合來引用方法。方法引用的語法格式有以下3種:

第一種方式:objectName::instanceMethod   //引用實例方法
第二種方式:ClassName::staticMethod      //引用靜態方法
第三種方式:ClassName::instanceMethod    //引用實例方法

下面舉例說明:

x->System.out.println(x)    等同於   System.out::println   //引用實例方法
(x,y)->Math.max(x,y)        等同於   Math::max             //引用靜態方法
x->x.toLowerCase()          等同於   String::toLowerCase   //引用實例方法   

對於第三種方式,爲什麼可以通過類名來訪問實例方法呢?這是因爲聰明的編譯器會根據上下文來推斷到底引用哪個對象的實例方法。
在Lambda表達式中,對構造方法的應用的語法如下:

ClassName::new

例如,以下兩種Lambda表達式等價:

x->new BigDecimal(x) 等同於: BigDecimal::new

函數式接口(FunctionalInterface)

在JDK8中定義了一個Annotation類型的函數式接口FunctionalInterface:

public @interface FunctionalInterface

Lambda表達式只能賦值給聲明爲函數式接口的Java類型的變量。上面示例中的Consumer、Runnable、Comparator、Predicate接口都標註爲函數式接口,因此可以接受Lambda表達式,例如,一下代碼把Lambda表達式賦值給一個Runnable類型的變量,這是合法的:

Runnable race=()->System.out.println("Hello World");

String類沒有標註爲函數式接口。以下代碼試圖把Lambda表達式賦值給一個String類型的變量,這是非法的,會導致編譯錯誤:

String str=()->{return "hello".toUpperCase();};

只要查閱Java API文檔,就能瞭解一個java類是否被標註爲函數式接口。例如,在Java API文檔中,Runnable接口的聲明如下,由此可以看出它被標註爲函數式接口:

@FunctionalInterface
public interface Runnable

Java語法糖

語法糖(syntactic sugar)也稱爲糖衣語法,指在計算機語言中添加的某種語法,這種語法雖然沒有顯著地增加語言的功能,但是能方便程序員編程,提高程序的可讀性,簡化程序,減少程序出錯的機會。
Java語言在不斷髮展的過程中,也陸陸續續地加入了一些語法糖。Lambda表達式就是一顆典型的語法糖。除了Lambda表達式,Java語言中常用的語法糖還包括以下幾種:
(1)泛型與類型擦除: Java語言中的泛型只在Java源代碼中存在,在編譯後的字節碼中,會被替換成原生類型,並且在相應的地方自動加入強制類型轉換代碼。對於運行中的Java程序來說,ArrayList<Integer> 和ArrayList<String>是同一個類。這種在編譯時去除泛型的過程稱爲類型擦除。所以說泛型技術實際上是Java語言的一顆語法糖。
(2)自動裝箱和拆箱:自動裝箱和拆箱實現了基本數據類型與包裝數據類型之間的隱式轉換。
(3)foreach循環語句:foreach語句從JDK5開始映入的,可以簡化遍歷數組和集合的代碼。
(4)方法的數目可變參數:可變參數可以簡化對方法的定義和調用
(5)枚舉類型:有助於更方便地訪問類型相同的一組常量
(6)斷言語句:方便對程序的調試
(7)switch表達式支持枚舉類型和字符串
(8)自動釋放try語句中打開的資源

總結

JDK8引入的Lambda表達式,它本質只是一顆讓編程人員更加得心應手的“語法糖”,它只存在於Java源代碼中,由編譯器把它轉換爲常規的Java類代碼。Lambda表達式優點類似於方法,由參數列表和一個使用這些參數的主體(可以是一個表達式或者一個代碼塊)組成。
Lambda表達式於Stream API 聯合使用,可以方便地操縱集合,完成對集合中元素的過濾和排序等操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章