天天看點

lambda表達式與函數式接口詳解

一、簡介

java8于2014年釋出,相比于java7,java8新增了非常多的特性,如lambda表達式、函數式接口、方法引用、預設方法、新工具(編譯工具)、Stream API、Date Time API、Optional等 。 目前很多公司的老産品依然使用的java7,甚至開發人員開發新産品時依然沒有選擇更新, 寫關于java8系列文章的目的在于梳理和分享java8新增的主要特性,開發時也可以用作參考。

lambda表達式是java8新增的主要特性之一,lambda表達式又稱閉包或匿名函數,主要優點在于簡化代碼、增強代碼可讀性、并行操作集合等。至于是否使用,有的同學覺得不适應,有的同學欲罷不能,見仁見智~

技多不壓身,本文将采用由淺入深的方式,講解java8 lambda表達式的文法及使用,并附帶代碼進行示範。

二、lambda文法

lambda的基本文法:

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

lambda表達式的特性:

1、可選類型聲明: 無需聲明參數類型,編譯器即可自動識别

2、可選的參數圓括号: 僅有一個參數時圓括号可以省略

3、可選的大括号:主體隻包含一個語句時可省略大括号

4、可選的傳回關鍵字:主體隻包含一個表達式傳回值并省略大括号時,編譯器會自動return傳回值;有大括号時,需要顯式指定表達式return了一個數值

特性示例:

//1、無參數,傳回值1
() -> 1 
//2、無參數,無傳回值
() -> System.out.print("Java8 lambda.");
//3、1個參數,參數類型為數字,傳回值為其值的5倍
x ->  5 * x 
//4、2個參數,參數類型均為數字,傳回值為其內插補點
(x, y) -> x - y
//5、2個參數,指定參數類型均為int型,傳回值為其內插補點 
(int x, int y) -> x - y  
//6、1個參數,指定參數類型為String ,無傳回值
(String str) -> System.out.print(str)
           

三、java8 lambda使用示例

前面我們講到lambda表達式的文法和特性,那麼在java8中如何使用lambda表達式呢?我們先以用幾個示例來展現lambda表達式在java8中的使用。

3.1 java Runnable接口的lambda實作

用lambdah代替匿名類是java8中lambda的常用形式,本文以開發同學經常使用的Runnable接口匿名類為示例,示範如何用lambda表達式來代替匿名類:

在java8之前:

new Thread(new Runnable()
   {
    @Override
    public void run()
    {
         System.out.println("No use lambda.");
    }
   }).start();
           

在java8之後:

可以看到,java8中利用lambda表達式大大簡化了代碼編寫。

此處簡要提下,用lambda表達式代替匿名類的關鍵在于,匿名類實作的接口使用了java.lang.FunctionalInterface注解,且隻有一個待實作的抽象接口方法,如Runnable接口:

@FunctionalInterface
  public interface Runnable {
      public abstract void run();
  }
           

3.2 java List疊代的lambda實作

開發同學經常會使用到集合類,并對集合類對象進行疊代,以實作業務邏輯。

java8中,集合類的頂層接口java.lang.Iterable定義了一個forEach方法:

/* @param action The action to be performed for each element
     * @throws NullPointerException if the specified action is null
     * @since 1.8
     */
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
           

forEach方法可以疊代集合的所有對象,其參數為Consumer對象,Consumer類位于java.util.function包下,我們看下其定義:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
           

到此已經很容易聯想到,我們可以采用lambda表達式來實作java8集合的疊代邏輯,下面我們進行示例:

在java8之前:

List<Integer> features = Arrays.asList(1,2);
    for (Integer feature : features) {
         System.out.println(feature);
     }
           

在java8之後:

List<Integer> features = Arrays.asList(1,2);
features.forEach(n -> System.out.println(n));
           

上述邏輯還可以用java8的方法引用來表示:

List<Integer> features = Arrays.asList(1,2);
features.forEach(System.out::println);
           

方法引用也是java8的新特性,由::操作符标示

四、函數式接口

在上一節中我們提到:“用lambda表達式代替匿名類的關鍵在于,匿名類實作的接口使用了java.lang.FunctionalInterface注解,且隻有一個待實作的抽象接口方法”, 這裡的接口便是函數式接口。

函數式接口(Functional Interface)是java8新增的特性,它是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口。函數式接口可以被隐式轉換為lambda表達式。

Runnable接口是在JDK1.8之前已經存在的接口,在JDK1.8中加入了@FunctionalInterface注解,表示将其定義為一個函數式接口。在JDK1.8中定義的函數式接口還有:

ava.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
           

DK1.8新增加的函數式接口有java.util.function包下的接口,典型的如上一節中提到的Consumer接口,感興趣的讀者可以閱讀JDK1.8的源碼,在此不逐個列出,在下一節本文還會列舉java.util.function包中典型的函數式接口的使用。

到這裡,可以總結出,java8中用lambda表達式代替匿名内部類,本質上是将接口定義為函數式接口,并将函數式接口隐式轉換為lambda表達式、

五、典型函數式接口的使用

上一節我們了解了java8函數式接口的概念和定義方法,本節再列舉java.util.function幾個典型的函數式接口的使用,加深下函數式接口與lambda表達式結合的了解。
           

5.1 Predicate接口

5.1.1 Predicate接口的基本用法

Predicate接口适合用于過濾,測試對象是否符合某個條件,Predicate接口源碼如下:

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
    
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
   
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
  
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}
           

可以看到,Predicate接口待實作的唯一抽象方法是 boolean test(T t) 方法。我們用Predicate接口實作從整數型數組中過濾正數:

public static void main(String[] args)
    {  
        List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
        filter(numbers, n -> n > 0);
    }
    
    public static void filter(List<Integer> numbers, Predicate<Integer> condition)
    {
        for (Integer number : numbers)
        {
            if (condition.test(number))
            {
                System.out.println("Eligible number: " + number);
            }
        }
    }
           

運作結果如下:

Eligible number: 4
Eligible number: 5
           

對數組的疊代,還可以使用Stream API的方式:

public static void main(String[] args)
{
    List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
    numbers.stream().filter(n -> n > 0).forEach(n -> System.out.println("Eligible number: " + n));
}
           

上面的代碼采用Stream API + Predicate接口 + Consumer接口的方式實作了同樣的功能,代碼量大大減少。Stream API(java.util.stream)同樣是java8的新特性,将真正的函數式程式設計風格引入到java語言中,進一步簡化了代碼。