Java重新出发--Java学习笔记(十一)--Lambda表达式

前言
这里对lambda表达式做简单学习,但是日后肯定还是要在实践中或他人大神经验里不断提高自己对这里的理解,因为这个Lambda表达式,确实非常好用,也可能确实不容易理解。

上一篇中,我们大概对行为参数化来传递代码有了一定了解。它可以应对不断变化的需求,它允许你定义一个代码块来表示一个行为,然后传递它。
一般来说,利用这个理念,你就已经可以编写更为灵活的代码了。随时遇到问题随时学习。但是虽然匿名类一定程度上提升了代码简介性,但是还是不够简洁。这样会影响程序员使用行为参数化的积极性。Lambda表达式很好的解决了这个问题,可以让你简洁的传递一个行为或代码。
现在你可以把Lambda表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。

lambda管中窥豹

可以把Lambda表达式理解为简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还会有一个抛出的异常列表。
Lambda表达式鼓励你采用上一篇文章中提到的行为参数化风格,最终结果就是你的额代码变得更加清晰、更加灵活。比如,利用Lambda表达式,你可以更为简洁地自定义一个Comparator对象:

先前:

        Comparator<Apple> byWeight = new Comparator<Apple>(){

            @Override
            public int compare(Apple o1, Apple o2) {
                // TODO Auto-generated method stub
                return o1.getWeight();
            }
            
        };

之后:

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

不得不承认,代码看起来更清晰了。要是现在觉得Lambda表达式看起来一头雾水的话也没关系,很快就会一点点的解释清楚的。现在,请注意你基本上只传递了比较两个苹果重量所需要的代码。看起来就像只传递了compare方法的主体。你很快就会学到,你甚至还可以进一步简化代码。为了进一步说明,下面给出了Java 8五个有效的Lambda表达式的例子:

//第一个表达式,具有一个String类型的参数并返回一个int.Lambda没有return语句,因为已经隐含了return
        (String) s -> s.length();
        
//传入一个类型为Apple的参数,返回苹果的重量是否超过150
        (Apple a) -> a.getWeight() >150;
        
//两个int型的参数,没有返回值
        (int x,int y) -> {
            System.out.println("resutlt:");
            System.out.println(x+y);
        }
        
//没有参数返回一个int
        () -> 42
        
//两个apple参数,返回比较的值
        (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

Java语言设计者选择这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。Lambda的基本语法是:

(parameters) -> expression

或(请注意语句的花括号)

(parameters) -> { statements; }

你可以看到,Lambda表达式的语法很简单,我们下来来测试一下你对这个模式的了解程度:

根据以上语法规则,以下哪个不是有效的lambda表达式?
(1)()->{} 是 //这个方法没有参数,返回为void,类似于public void run(){}
(2)()->"Panda" 是 //没有参数,以String作为返回表达式
(3)()->{return "Panda";} 是 //没有参数,返回String(利用显示返回语句)
(4)()->retutn "Alen"+i; 不是 //return是一个控制流语句,要使Lambda有效,需要使用花括号
(5)(String s) -> {"panda"} 不是 //panda是一个表达式,不是一个语句,要使其有效去掉分号和花括号,如果不想去除,可以使用显式返回语句,return "panda"

在哪里以及如何使用Lambda

现在你可能在想,在哪里可以使用Lambda表达式。直接公布答案:你可以在函数式接口上使用Lambda表达式。

函数式接口

还记得上一篇文章中,为了参数化filter方法的行为而创建的Predicate<T>接口吗?它就是一个函数式接口!为什么呢?因为Predicate仅仅定义了一个抽象方法:

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

一言以蔽之,函数式接口就是只定义一个抽象方法的接口。你已经知道了Java API中的一些其他函数式接口,如Comparator和Runnable

public interface Comparator<T>{
    int compare(T o1, T o2);
}

public interface Runnable{
    void run();
}

接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法,如List的sort方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

为了检测是否掌握了函数式接口的概念,我们来看一个小测试:

以下哪一个是函数式接口:

(1)public interface Adder{
        int add(int a,int b);            //是
    }
(2)public interface SmartAdder extends Adder{
        int add(Double a,Double b);           //不是,有两个方法,从上面接口继承了一个
    }
(3)public interface Nothing{          //不是,没有抽象方法
        
    }

用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
这听上去可能有些绕口,但是联想到上一篇文章中的Lambda表达式改造的语句,或许就会清晰许多,它不同于使用匿名内部类来完成时的笨拙,而是更加清晰直接:

Runnable r1 = () -> System.out.print("HelloWorld");//使用lambda
Runnable r2 = new Runnable(){

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.print("HelloWorld2");
    }
            
};//使用匿名表达式
public static void process(Runnable r){
        r.run();
}
        process(r1);
        process(r2);
        process(()->{System.out.print("HelloWorld0");});

你可能会想:“为什么只有在需要函数式接口的时候才可以传递Lambda呢?
”语言的设计者也考虑过其他方法,例如给Java添加函数类型,但最终他们选择了现在这种方式,因为这种方式自然且能避免语言变得更加复杂。此外,大多数Java程序员都已经熟悉了具有一个抽象方法的接口的理念(例如事件处理)。

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

让我们通过一个例子,看看在实践中如何利用Lambda和行为参数化来让代码更为灵活,更为简洁。资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很相似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式:

public static String processFile() throws IOException{
        try(BufferedReader br = 
                new BufferedReader(new FileReader(data.txt))){
                    return br.readLine(); //有用工作代码
                }
    }
初始化/准备代码 -> 任务A -> 清理/结束代码
初始化/准备代码 -> 任务B -> 清理/结束代码
第一步:记得行为参数化

现在这段代码时有局限的。你只能读文件的第一行。如果你想要返回头两行,甚至返回使用最频繁的词,该怎么办呢?在理想的情况下,你要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作。
这听起来是不是很耳熟?是的,你需要把processFile的行为参数化。你需要一种方法把行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。传递行为正是Lambda的拿手好戏。那要是想一次读两行,这个新的processFile方法看起来又该是什么样的呢?基本上,你需要一个接受BufferedReader并返回String的Lambda。
例如,下面就是从BufferedReader中打印两行的写法:

String result = processFile((BufferedReader br) -> 
                                            br.readLine() + br.readLine());
第二步:使用函数式接口来传递行为

前面已经解释过了,Lambda仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口。让我们把这一接口叫做BufferedReaderProcessor吧。

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

@FunctionalInterface 标注表示该接口会设计成一个函数式接口。如果你用此标注定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。

现在你就可以把这个接口作为新的processFile方法的参数了:

public static String processFile(BufferedReaderProcessor p) throws IOException{
    ...
}
第三步:执行一个行为,

任何Buffered ->String形式的lambda都可以作为参数来传递,因为他们符合BufferedReaderProcessor接口中定义的process方法的签名。现在你只需要一种方法在ProcessFile主体内执行Lambda所代表的代码,请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,你可以在processFile主体内,对得到的BufferedReaderProcessor对象调用process方法执行处理:

public static String processFile(BufferedReaderProcesssor p) throws IOException{
    try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
        return p.process(br);
    }
}
第四步:传递Lambda

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

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

1.建函数接口,指定独有方法
2.将函数接口以参数形式传入一个方法中
3.实现这个接口中的方法
4.调用这个函数式参数的方式时,传入符合标准的lambda表达式。
5.不同的lambda表达式,可以起到不同的行为作用。

(1)public static String processFile()throws IOException{
        try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
            return br.readline();
        }
     }
(2)public interface BufferedReaderProcessor{
        String process(BufferedReader b)throws IOException;
    }
    public static String processFile(BufferedReaderProcessor p)throws IOException{
        try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
            return br.readline();
        }
     }
(3)//调用方法,传入lambda表达式
    String oneLine = processFile((BufferedReader br)->br.readLine());
    String twoLine = processFile((BufferedReader br)->br.readLine()+br.readLine());
使用函数式接口

如你所见,函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。

Predicate
java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。在你需要一个涉及类型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(listS,nonEmptyStringPredicate);

Consumer
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口:

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        forEach(Arrays.asList(1,2,3,4,6), (Integer i)->System.out.println(i));
    }

    public static <T> void forEach(List<T> list,Consumer<T> c){
        for(T i:list){
            c.accept(i);
        }
    }

Function
java.util.function.Function<T,R>接口定义了一个叫做apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度):

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
public static void main(String[] args) {
    // TODO Auto-generated method stub
    List<Integer> I = mapDemo(Arrays.asList(0,5),(Integer i)->i+1);
    System.out.println(I.toString());
}

public static <R, T> List<R> mapDemo(List<T> list,Function<T,R> f){
    List<R> result = new ArrayList<>();
    for(T s :list){
        result.add(f.apply(s));
    }
    return result;      
}

还有更为丰富的一些函数式接口,这里列举了三个比较有代表性的。

方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。
下面就是借助Java 8API,用方法引用写的一个排序的例子:

先前:

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

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

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

是不是更酷了?念起来就是“给库存排序,比较苹果的重量”,这样的代码读起来简直就像是在描述问题本身,太酷了。
为什么要关心方法引用呢?方法引用可以被看作调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的知识“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式,但是,显式地指明方法的名称,你的代码可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符** :: **前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的
方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法,方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法。
下面给出一些在Java 8中方法引用的例子来让你更加了解:

(Apple apple)-> apple.getWeight();   Apple::getWeight;
()->Thread.currentThread().dumpStack();     Thread.currentThread()::dumpStack();
(str, i) -> str.subString(i);     String::subString;
String s = System.out.println(s);           System.out::println;

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时写的代码更少了。

Lambda 和方法引用实战

我们继续来研究开始的那个问题——用不同的排序策略给一个Apple列表排序,并展示如何把一个原始粗暴
的解决方案转变得更为简明:

inventory.sort(comparing(Apple::getWeight));
第一步:传递代码

很幸运,Java 8的API已经为你提供了一个List可用的sort方法,你不用自己去实现它。那么最困难的部分已经搞定了!但是,如何把排序的策略传递给sort方法呢?你看,sort方法的签名是这样的:

void sort(Comparator<? super E> c)

它需要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象里。我们说sort的行为被参数化了:传递给它的排序策略不同,其行为也会不同。

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

public class AppleComparator implements Comparator<Apple>{
    public int compare(Apple a1, Apple a2){
        return a1.getWeigh().compareTo(a2.getWeight());
    }
}
inventory.sort(new AppleComparator());
第二步:使用匿名类

你可以使用匿名类来改进解决方案,而不是实现一个Comparator却只实例化一次:

inventory.sort(new Comparator<Apple>(){
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});
第三步:使用Lambda表达式

但你的解决方案仍然挺啰嗦的。使用Java 8引入的Lambda改进后的代码如下:

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

你的代码还能变得更易读一点吗?Comparator具有一个叫做comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。它可以像下面这样用:

Comparator<Apple> c = Comparator.comparing((Apple a1) -> a.getWeight());

现在你可以把代码再改得紧凑一点了:

import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
第四步:使用方法引用

前面解释过,方法引用就是替代那些转发参数的Lambda表达式的语法糖。你可以用方法引用让你的代码更加简洁(假设你已经静态导入了java.util.Comparator.comparing):

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

恭喜你,这就是你的最终解决方案!这笔Java 8之前的代码好在哪儿呢?它比较短;它的意思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”

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

推荐阅读更多精彩内容

  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,442评论 0 4
  • 前段时间一直在看lambda表达式,但是总感觉吃不透,在深入了解lambda表达式的时候,需要很多基础的知识栈。这...
    西瓜真好吃丶阅读 2,670评论 0 7
  • Lambda表达式 利用行为参数化这个概念,就可以编写更为灵活且可重复使用的代码。但同时,使用匿名类来表示不同的行...
    谢随安阅读 843评论 2 0
  • 简介 概念 Lambda 表达式可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主...
    刘涤生阅读 3,166评论 5 18
  • 今早出发在顺风车上睡了一个美美的早觉,把3点30就自动起床的疲累安抚抹去。果然手机不在身旁,又碰上必须早...
    呀小学酥阅读 505评论 0 0