【Java 8实战笔记】Lambda表达式

Lambda表达式

利用行为参数化这个概念,就可以编写更为灵活且可重复使用的代码。但同时,使用匿名类来表示不同的行为并不令人满意。Java 8引入了Lambda表达式来解决这个问题。它使你以一种很简洁的表示一个行为或传递代码。

可以将Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

  • 匿名 - 因为它不像普通的方法一样有一个明确的名称。
  • 函数 - 说它是函数是因为Lambda函数不像方法那样属于某个特定的类,但和方法要一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递 - Lambda表达式可以作为参数传递给方法或存储在变量中。
  • 简洁 - 无需像匿名类那样写很多模板代码

使用Lambda的最终结果就是你的代码变得更清晰、灵活。打比方,利用Lambda表达式,可以更为简洁地自定义一个Comparator对象。

Lambda表达式由参数、箭头和主体组成

对比以下两段代码:

Comparator <Apple> byWeight = new Comparator<Apple>(){
    public int compare(Apple a1,Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
};
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

代码看起来更清晰了。基本上只传递了真正需要的代码(compare方法的主体)。

Lambda表达式由三个部分,如图 Lambda表达式由参数、箭头和主体组成 所示

  • 参数列表 - 这里它采用了Comparator中compare方法的参数,两个Apple。
  • 箭头 - 箭头 -> 把参数列表与Lambda主体分隔开。
  • Lambda主体 - 比较两个Apple的重量。表达式就是Lambda的返回值。

Lambda 的基本语法是:
(parameters) -> expression(parameters) -> { statements; }

函数式接口

之前的Predicate<T>就是一个函数式接口,因为Predicate仅仅定义了一个抽象方法:

public interface Predicate<T>{
    boolean test(T t);
}

简而言之,函数式接口就是只定义一个抽象方法的接口。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。

函数式接口的抽象方法的签名

把Lambda付诸实践:环绕执行模式

要从一个文件中读取一行所需的模板代码:

public static String processFile() throws IOException{
    try (BufferedReader br =
            new BufferedReader(new FileReader("data.txt"))){
        return br.readLine();
    }
}

现在这段代码的局限性在于只能读文件的第一行,如果想要返回头两行,甚至是返回使用最频繁的词。这时需要把processFile的行为参数化。
需要一个接收BufferedReader并返回String的Lambda。下面是从BufferedReader中打印两行的写法:

String result = processFile(BufferedReader br) -> br.readLine() + br.readLine());

现在需要创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口:

@FunctionalInterface
public interface BufferedReaderProcessor{
    String process(BufferedReader b) throws IOException;
}

@FunctionalInterface 是什么?
这个标注用于表示该接口会设计成一个函数式接口。如果用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
使用它不是必须的,但是使用它是比较好的做法。

现在就可以把这个接口作为新的processFile方法的参数,在方法主体内,对得到BufferedReaderProcessor对象调用process方法执行处理:

public static String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br =
                 new BufferedReader(new FileReader("data.txt"))){
        return p.process(br);
    }
}

现在可以通过传递不同的Lambda重用processFile方法,并以不用的方式处理文件了。
处理一行:

String oneLine =
    processFile((BufferedReader br) -> br.readLine());

处理两行:

String twoLines = 
     processFile((BufferedReader br) -> br.readLine() + br.readLine());

完整代码如下:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class test{
    @FunctionalInterface
    public interface BufferedReaderProcessor{
        String process(BufferedReader b) throws IOException;
    }
    public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br =
                     new BufferedReader(new FileReader("c:/tmp/data.txt"))){
            return p.process(br);
        }
    }
    public static void main(String[] args) throws IOException {
        String oneLine = processFile((BufferedReader br) -> br.readLine());
        String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
        System.out.println(oneLine);
        System.out.println(twoLines);
    }
}

现在已经能成功的利用函数式接口来传递Lambda。

使用函数式接口

函数接口定义且只定义了一个抽象方法。函数式接口的抽象方法的签名称为函数描述符。因此,为了应用不同的Lambda表达式,需要一套能够描述常见函数描述符的函数式接口。
Java 8在java.util.function包中加入了许多新的函数式接口,你可以重用它来传递多个不同的Lambda。

Predicate

java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。在需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。

public interface Predicate<T> {
    boolean test(T t);
}

例如:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p){
    List<T> results = new ArrayList<>();
    for (T s: list){
        if(p.test(s)){
            results.add(s);
        }    
    }
    return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer

java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T对象,没有返回。
例如:

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

public static <T> void forEach(List<T> list, Predicate<T> c){
    for (T i: list){
        c.accept(i);
    }
}
forEach(
      Arrays.asList(1,2,3,4,5),
      (Interger i) -> System.ou.println(i)
    );
Function

java.util.function.Function<T, R>定义了一个名叫apply的抽象方法,它接受一个泛型T对象,并返回一个泛型R的对象。
下面将创建一个map方法,以将一个String列表映射到包含每个String长度的Interger列表:

@FunctionalInterface
public interface Function<T, R> {
    R accept(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f){
    List<R> result = new ArrayList<>();
    for (T s: list){
        result.add(f.apply(s));
    }
    return result;
}
List<Integer> l =map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());
原始类型特化

Java的类型有两种: 引用类型原始类型 。但是泛型只能绑定到引用类型。这是由于泛型内部的实现方式造成的。因此Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫装箱(boxing)。相反的操作便叫拆箱(unboxing)。装箱和拆箱操作是可以由自动装箱机制来自动完成的。但是这在性能方面要付出代价,装箱后的值需要更多的内存并且需要额外的内存。

Java 8为原始类型带来了一个专门的版本,用于在输入和输出都是原始类型时避免自动装箱的操作:

public interface IntPredicate{
    boolean test(int t);
}

无装箱:

IntPredicate evenNumbers = (int i) -> i%2 ==0;
evenNumbers.test(1000);

装箱:

Predicate<Integer> oddNumbers = (Integer i) -> i%2 == 1;
oddNumbers.test(1000);

类型检查、类型推断以及限制

Lambda本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式应该知道Lambda的实际类型是什么。

类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(例如接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

同样的Lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来。

类型推断

可以进一步简化你的代码。编译器会从上下文(目标类型)推断出痛什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,这样就可以再Lambda语法中省去标注参数类型。

没有自动类型推断:

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

有自动类型推断:

Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
使用局部变量

之前介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda

例如:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

Lambda捕获了portNumber变量。但关于对这些变量可以做什么有一些限制。Lambda可以没有限制的捕获实例变量和静态变量(也就是在其主体中引用)。但是局部变量必须显示声明为final(或事实上是final)。
下面的代码无法编译,因为portNumber变量被赋值了两次:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

实例变量和局部变量背后的实现有一个关键的不同。实例变量都存储在堆中,而局部变量则保存在栈上。Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后去访问该变量,这回引发造成线程不安全的新的可能性。

方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。

下面是用方法引用写的一个排序的例子:

先前:

invenstory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

之后(使用方法引用和 java.util.Comparator.comparing ):

inventory.sort(comparing(Apple::getWeight));

方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

方法引用就是让你根据已有的方法实现来创建Lambda表达式。

当你需要使用方法引用时,目标引用放在分隔符 :: 前,方法的名称放在后面。例如 Apple::getWeight 就是引用了Apple类中定义的方法getWeight。

方法引用也可以看做针对仅仅设计单一方法的Lambda的语法糖

构建方法引用

方法引用主要有三类。

  1. 指向静态方法的方法引用(Integer::parseInt)
  2. 指向任意类型实例方法的方法引用(String::length)
  3. 指向现有对象的实例方法的方法引用(Transaction::getValue)

第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。
例如 (String s) -> s.toUpperCase() 可以写作 String::toUpperCase

第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象的方法。例如,Lambda表达式 () -> expenssiveTransaction.getValue() 可以写作 expensiveTransaction::getValue

方法引用不需要括号,是因为没有实际调用这个方法。

构造函数引用

可以利用现有构造函数的名称和关键字来创建它的一个引用 ClassName:new

例如:

List<Integer> weight = Arrays.asList(7,3,4,10);
List<Apple> apples = map(weights, Apple::new);
public static List<Apple> map(List<Integer> List, Function<Integer, Apple> f){
    List<Apple> result = new ArrayList<>();
    for(Integer e: list){
        result.add(f.apply(e))
    }
    return result;
}

Lambda和方法引用实战(用不同的排序策略给一个Apple列表排序)

第1步:传递代码

Java API已经提供了一个List可用的sort方法,要如何把排序策略传递给sort呢?sort方法的签名样子如下:

void sort(Comparator<? super E> c)

它需要一个Comparator对象来比较两个Apple。

第一个解决方案看上去是这样的:

public class AppleComparator implements Comparator<Apple>{
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
}

inventory.sort(new AppleComparator());
第2步:使用匿名类改进
inventory.sort(new AppleComparator<Apple>(){
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

使用匿名类的意义仅仅在于不用为了只实例化一次而实现一个Comparator。

第3步:使用Lambda表达式
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

Comparator有一个叫做comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象:

import static java.util.Comparator.comparing;

inventory.sort(comparing((a) -> a.getWeight()));
第4步:使用方法引用
inventory(comparing(Apple::getWeight));

复合Lambda表达式的有用方法

比较器复合

对于:

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

1.逆序

想要按重量递减排序怎么办?不需要建立一个新的Comparator的实例。接口有个默认方法 reverse 可以使给定的比较器逆序。因此仍然可以用之前的那个比较器:

inventory.sort(comparing(Apple::getWeight).reversed());

2.比较器链

如果两个苹果一样重怎么办,哪个苹果应该排在前面?这时候可能需要再提供一个Comparator来进一步比较。
thenComparing 就是做这个用的。它接受一个函数作为参数(与comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator:

inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));
谓词复合

谓词接口包括三个方法:negate、and 和 or。

  1. negate
    可以使用negate方法来返回一个Predicate的非,比如苹果不是红色:
Predicate<Apple> notRedApple = redApple.negate()
  1. and
    可以用and方法将两个Lambda组合起来:
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
  1. or
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getcolor()));
函数复合

还可以把Function接口所代表的Lambda表达式复合起来。Function接口有两个默认方法:andThen和 compose。它们都会返回Function的一个实例。

  1. andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

比如函数f给数字加1,另一个函数给数字乘2:

Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.andThen(g);
int result = h.apply(1);

在数学上意味着g(f(x))

  1. compose 方法先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1);

在数学上意味着f(g(x))

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容