首頁 > Java > java教程 > 主體

Java8 新特性之 Lambda 表達式

黄舟
發布: 2017-02-23 10:34:57
原創
1335 人瀏覽過

摘要: lambda表達式是Java8帶給我們的幾個重量級新功能之一,借用lambda表達式,可以讓我們的Java程式設計更簡潔。本文是Java8新特性的第一篇,將探討行為參數化、lambda表達式,以及方法參考。

lambda表達式是java8帶給我們的幾個重量級新特性之一,借用lambda表達式,可以讓我們的java程式設計更加簡潔。最近新的專案摒棄了1.6的版本,全面基於java8進行開發,本文是java8新特性的第一篇,將探討行為參數化、lambda表達式,以及方法引用。

一. 行為參數化

行為參數化簡單的說就是函數的主體只包含模板類別通用程式碼,而一些會隨著業務場景而變化的邏輯則以參數的形式傳遞到函數之中,採用行為參數化可以讓程式更加的通用,以因應頻繁變更的需求。

考慮一個業務場景,假設我們需要透過程式對蘋果進行篩選,我們先定義一個蘋果的實體:

/**
 * 苹果实体
 *
 * @author zhenchao.wang 2016-09-17 12:49
 * @version 1.0.0
 */
public class Apple {
    /** 编号 */
    private long id;
    /** 颜色 */
    private Color color;
    /** 重量 */
    private float weight;
    /** 产地 */
    private String origin;
    public Apple() {
    }
    public Apple(long id, Color color, float weight, String origin) {
        this.id = id;
        this.color = color;
        this.weight = weight;
        this.origin = origin;
    }
    // 省略getter和setter
}
登入後複製

用戶最開始的需求可能只是簡單的希望能夠透過程序篩選出綠色的蘋果,於是我們可以很快的通過程序實現:

/**
 * 筛选绿苹果
 *
 * @param apples
 * @return
 */
public static List<Apple> filterGreenApples(List<Apple> apples) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (Color.GREEN.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}
登入後複製

如果過了一段時間用戶提出了新的需求,希望能夠通過程序篩選出紅色的蘋果,於是我們又針對性的添加了篩選紅色蘋果的功能:

/**
 * 筛选红苹果
 *
 * @param apples
 * @return
 */
public static List<Apple> filterRedApples(List<Apple> apples) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (Color.RED.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}
登入後複製

更好的實現是把顏色作為一個參數傳遞到函數中,這樣就可以應付以後用戶提出的各種顏色篩選請求了:

/**
 * 自定义筛选颜色
 *
 * @param apples
 * @param color
 * @return
 */
public static List<Apple> filterApplesByColor(List<Apple> apples, Color color) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (color.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}
登入後複製

這樣設計了之後,再也不用擔心用戶的顏色篩選需求變化了,但是不幸的是,某一天使用者提了一個需求要求能夠選擇重量達到某一標準的蘋果,有了前面的教訓,我們也把重量的標準作為參數傳遞給篩選函數,於是得到:

/**
 * 筛选指定颜色,且重要符合要求
 *
 * @param apples
 * @param color
 * @param weight
 * @return
 */
public static List<Apple> filterApplesByColorAndWeight(List<Apple> apples, Color color, float weight) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (color.equals(apple.getColor()) && apple.getWeight() >= weight) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}
登入後複製

這樣透過傳遞參數的方式真的好嗎?如果篩選條件越來越多,組合模式越來越複雜,我們是不是需要考慮到所有的情況,並針對每一種情況都有相應的應對策略呢,並且這些函數僅僅是篩選條件的部分不一樣,其餘部分都是相同的模板程式碼(遍歷集合),這時候我們就可以將行為 參數化 ,讓函數只保留模板程式碼,而把篩選條件抽離出來當做參數傳遞進來,在在java8之前,我們透過定義一個過濾器介面來實現:

/**
 * 苹果过滤接口
 *
 * @author zhenchao.wang 2016-09-17 14:21
 * @version 1.0.0
 */
@FunctionalInterface
public interface AppleFilter {
    /**
     * 筛选条件抽象
     *
     * @param apple
     * @return
     */
    boolean accept(Apple apple);
}
/**
 * 将筛选条件封装成接口
 *
 * @param apples
 * @param filter
 * @return
 */
public static List<Apple> filterApplesByAppleFilter(List<Apple> apples, AppleFilter filter) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (filter.accept(apple)) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}
登入後複製

透過上面行為抽象化之後,我們可以在具體呼叫的地方設定篩選條件,並將條件作為參數傳遞到方法中:

public static void main(String[] args) {
    List<Apple> apples = new ArrayList<>();
    // 筛选苹果
    List<Apple> filterApples = filterApplesByAppleFilter(apples, new AppleFilter() {
        @Override
        public boolean accept(Apple apple) {
            // 筛选重量大于100g的红苹果
            return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100;
        }
    });
}
登入後複製

上面的行為參數化方式採用匿名類別來實現,這樣的設計在jdk內部也經常採用,例如

java.util.Comparator
登入後複製

java.util.concurrent.Callable
登入後複製

等,使用這一類別介面的時候,我們都可以在具體呼叫的地方用過匿名類別來指定函數的具體執行邏輯,不過從上面的程式碼區塊來看,雖然很極客,但是不夠簡潔,在java8中我們可以透過lambda來簡化:

// 筛选苹果
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
登入後複製

透過lambda表達式極大的精簡了程式碼,下面來學習java的lambda表達式吧~

二. lambda表達式定義

我們可以將lambda表達式定義為一種 簡潔、可傳遞的匿名函數,首先我們需要明確lambda表達式本質上是一個函數,雖然它不屬於某個特定的類,但具備參數列表、函數主體、返回類型,以及能夠拋出異常;其次它是匿名的,lambda表達式沒有具體的函數名稱;lambda表達式可以像參數一樣進行傳遞,從而極大的簡化程式碼的編寫。格式定義如下:

格式一:參數列表-> 表達式
格式二:參數列表-> {表達式集合}
需要注意的是,lambda表達式隱含了return關鍵字,所以在單一的表達式中,我們不需要明確的寫return關鍵字,但是當表達式是一個語句集合的時候,則需要明確添加return,並用花括號

{ }
登入後複製

將多個表達式包圍起來,下面看幾個例子:

//返回给定字符串的长度,隐含return语句
(String s) -> s.length() 
// 始终返回42的无参方法
() -> 42 
// 包含多行表达式,则用花括号括起来
(int x, int y) -> {
    int z = x * y;
    return x + z;
}
登入後複製

三. 依託於函數式介面使用lambda表達式

lambda表達式的使用需要借助於函數式接口,也就是說只有函數式接口出現地方,我們才可以將其用lambda表達式進行簡化。

自訂函數式介面

函數式介面定義為只具備 一個抽象方法 的介面。 java8在介面定義上的改進就是引入了預設方法,使得我們可以在介面中對方法提供預設的實現,但是不管存在多少個預設方法,只要具備一個且只有一個抽象方法,那麼它就是函數式接口,如下(引用上面的AppleFilter):

/**
 * 苹果过滤接口
 *
 * @author zhenchao.wang 2016-09-17 14:21
 * @version 1.0.0
 */
@FunctionalInterface
public interface AppleFilter {
    /**
     * 筛选条件抽象
     *
     * @param apple
     * @return
     */
    boolean accept(Apple apple);
}
登入後複製
AppleFilter
登入後複製
登入後複製

僅包含一個抽象方法

accept(Apple apple)
登入後複製

,依照定義可以將其視為一個函數式接口,在定義時我們為該介面添加了

@FunctionalInterface
登入後複製

注解,用于标记该接口是函数式接口,不过这个接口是可选的,当添加了该接口之后,编译器就限制了该接口只允许有一个抽象方法,否则报错,所以推荐为函数式接口添加该注解。

jdk自带的函数式接口

jdk为lambda表达式已经内置了丰富的函数式接口,如下表所示(仅列出部分):




函数式接口函数描述符原始类型特化
Predicate<T>T -> booleanIntPredicate, LongPredicate, DoublePredicate
Consumer<T>T -> voidIntConsumer, LongConsumer, DoubleConsumer
FuncationT -> RIntFuncation, IntToDoubleFunction, IntToLongFunction, LongFuncation…
Supplier() -> TBooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperatorT -> TIntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator(T, T) -> TIntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate(L, R) -> boolean
BiConsumer(T, U) -> void
BiFunction(T, U) -> R

下面分别就

Predicate<T>
登入後複製

Consumer<T>
登入後複製

Function<T, R>
登入後複製

的使用示例说明。

Predicate<T>

@FunctionalInterface
public interface Predicate<T> {
    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}
登入後複製

Predicate的功能类似于上面的

AppleFilter
登入後複製
登入後複製

,利用我们在外部设定的条件对于传入的参数进行校验,并返回验证结果

boolean
登入後複製

,下面利用

Predicate
登入後複製

对List集合的元素进行过滤:

/**
 * 按照指定的条件对集合元素进行过滤
 *
 * @param list
 * @param predicate
 * @param 
 * @return
 */
public  List filter(List list, Predicate<T> predicate) {
    List newList = new ArrayList();
    for (final T t : list) {
        if (predicate.test(t)) {
            newList.add(t);
        }
    }
    return newList;
}
登入後複製

利用上面的函数式接口过滤字符串集合中的空字符串:

demo.filter(list, (String str) -> null != str && !str.isEmpty());
Consumer<T>
@FunctionalInterface
public interface Consumer<T> {
    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}
登入後複製

Consumer提供了一个accept抽象函数,该函数接收参数,但不返回值,下面利用

Consumer
登入後複製

遍历集合:

/**
 * 遍历集合,执行自定义行为
 *
 * @param list
 * @param consumer
 * @param 
 */
public  void filter(List list, Consumer<T> consumer) {
    for (final T t : list) {
        consumer.accept(t);
    }
}
登入後複製

利用上面的函数式接口,遍历字符串集合,并打印非空字符串:

demo.filter(list, (String str) -> {
        if (StringUtils.isNotBlank(str)) {
            System.out.println(str);
        }
    });
登入後複製

Function<T, R>

@FunctionalInterface
public interface Function<T, R> {
    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}
登入後複製

Funcation执行转换操作,输入是类型T的数据,返回R类型的数据,下面利用

Function
登入後複製

对集合进行转换:

/**
 * 遍历集合,执行自定义转换操作
 *
 * @param list
 * @param function
 * @param 
 * @param 
 * @return
 */
public  List filter(List list, Function<T, R> function) {
    List newList = new ArrayList();
    for (final T t : list) {
        newList.add(function.apply(t));
    }
    return newList;
}
登入後複製

下面利用上面的函数式接口,将一个封装字符串(整型数字的字符串表示)的接口,转换成整型集合:

demo.filter(list, (String str) -> Integer.parseInt(str));
上面这些函数式接口还提供了一些逻辑操作的默认实现,留到后面介绍java8接口的默认方法时再讲吧~

使用过程中需要注意的一些事情

类型推断

在编码过程中,有时候可能会疑惑我们的调用代码会去具体匹配哪个函数式接口,实际上编译器会根据参数、返回类型、异常类型(如果存在)等做正确的判定。

在具体调用时,在一些时候可以省略参数的类型,从而进一步简化代码:

/

/ 筛选苹果
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
// 某些情况下我们甚至可以省略参数类型,编译器会根据上下文正确判断
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
登入後複製

局部变量

上面所有例子我们的lambda表达式都是使用其主体参数,我们也可以在lambda中使用局部变量,如下:

int weight = 100;
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= weight);
登入後複製

该例子中我们在lambda中使用了局部变量weight,不过在lambda中使用局部变量必须要求该变量 显式声明为final或事实上的final ,这主要是因为局部变量存储在栈上,lambda表达式则在另一个线程中运行,当该线程视图访问该局部变量的时候,该变量存在被更改或回收的可能性,所以用final修饰之后就不会存在线程安全的问题。

四. 方法引用

采用方法引用可以更近一步的简化代码,有时候这种简化让代码看上去更加的直观,先看一个例子:

/* ... 省略apples的初始化操作 */
// 采用lambda表达式
apples.sort((Apple a, Apple b) -> Float.compare(a.getWeight(), b.getWeight()));
// 采用方法引用
apples.sort(Comparator.comparing(Apple::getWeight));
登入後複製

方法引用通过

::
登入後複製

将方法隶属和方法自身连接起来,主要分为三类:

静态方法

(args) -> ClassName.staticMethod(args)
登入後複製


转换成

ClassName::staticMethod
登入後複製


参数的实例方法

(args) -> args.instanceMethod()
登入後複製


转换成

ClassName::instanceMethod  // ClassName是args的类型
登入後複製


外部的实例方法

(args) -> ext.instanceMethod(args)
登入後複製


转换成

ext::instanceMethod(args)
登入後複製

 以上就是Java8 新特性之 Lambda 表达式 的内容,更多相关内容请关注PHP中文网(www.php.cn)!



相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!