Java 8中Lambda学习笔记

以前看到别人的Java代码里有泛型了,接口了,就特别害怕,不知道是干啥的,虽然上网也查了一些资料,但觉得还是理解的不够深入,导致遇到同样的问题,还是得重新查一遍资料。

Lambda表达式

这个几把玩意出现了好多次了,自己感觉实现同样的功能,用原来的代码写法就可以了嘛,为啥非得用这个东西呢?就算查资料去尝试学习它,自己的内心对它还是有抵触情绪的。直到公司的项目里同事的代码出现了Lambda写法,不学不行了,要不会影响对项目代码的理解的,就生出了再学习它一下的意图。

为啥需要Lambda?

学习一项技术,一定要知道它出现是解决什么问题的,不能知其然而不知其所以然。

Java 是一流的面向对象语言,除了部分简单数据类型,Java 中的一切都是对象,即使数组也是一种对象,每个类创建的实例也是对象。在 Java 中定义的函数或方法不可能完全独立,也不能将方法作为参数或返回一个方法给实例。

从 Swing 开始,我们总是通过匿名类给方法传递函数功能,以下是旧版的事件监听代码:

someObject.addMouseListener(new MouseAdapter() {
        public void mouseClicked(MouseEvent e) {

            //Event listener implementation goes here...

        }
    });

在上面的例子里,为了给 Mouse 监听器添加自定义代码,我们定义了一个匿名内部类 MouseAdapter 并创建了它的对象,通过这种方式,我们将一些函数功能传给 addMouseListener 方法。

简而言之,在 Java 里将普通的方法或函数像参数一样传值并不简单,为此,Java 8 增加了一个语言级的新特性,名为 Lambda 表达式。Lambda 表达式是一种匿名函数(对 Java 而言这并不完全正确,但现在姑且这么认为),简单地说,它是没有声明的方法,也即没有访问修饰符、返回值声明和名字。

Java 中的 Lambda 表达式通常使用 (argument) -> (body) 语法书写,例如:

(arg1, arg2...) -> { body }

(type1 arg1, type2 arg2...) -> { body }

以下是一些 Lambda 表达式的例子:

(int a, int b) -> {  return a + b; }

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

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

() -> 42

() -> { return 3.1415 };

让我们了解一下 Lambda 表达式的结构:

  • 一个 Lambda 表达式可以有零个或多个参数
  • 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同
  • 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空圆括号代表参数集为空。例如:() -> 42
  • 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a
  • Lambda 表达式的主体可包含零条或多条语句
  • 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
  • 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空。

什么是函数式接口 ?

在 Java 中,Marker(标记)类型的接口是一种没有方法或属性声明的接口,简单地说,marker 接口是空接口。相似地,函数式接口是只包含一个抽象方法声明的接口。

扩展
1、什么是标记接口?
一个空的接口称为标记接口(Tag Interface),Java中很多标记接口,比如Serializable,EventListener, Remote(java.rmi.Remote)等。

package java.util;
public interface EventListener{
}

2、 标记接口有什么特点?
标记接口没有任何成员变量和方法,它就是空的。你肯定疑惑,既然是空的,其他类怎么去实现(implement)它?它用来做什么?
实际上,其他类implement它是为了声明该类在某个特定集合中的成员资格,比如当一个类实现(implement)了Serializable接口,它的目的是声明其是Serializable中的一个成员,当JVM虚拟机看到该类是Serializable的,那么它在处理序列化/反序列化时会做一些特殊的处理。
标记接口对JVM是很有意义,你可以创建自己的标记接口来分离或分类代码,从而提高代码的可阅读行。

java.lang.Runnable 就是一种函数式接口,在 Runnable 接口中只声明了一个方法 void run(),相似地,ActionListener 接口也是一种函数式接口,我们使用匿名内部类来实例化函数式接口的对象,有了 Lambda 表达式,这一方式可以得到简化。
每个 Lambda 表达式都能隐式地赋值给函数式接口,例如,我们可以通过 Lambda 表达式创建 Runnable 接口的引用。

Runnable r = () -> System.out.println("hello world");

当不指明函数式接口时,编译器会自动解释这种转化

new Thread(
   () -> System.out.println("hello world")
).start();

因此,在上面的代码中,编译器会自动推断:根据线程类的构造函数签名 public Thread(Runnable r) { },将该 Lambda 表达式赋给 Runnable 接口。

@FunctionalInterface 是 Java 8 新加入的一种接口,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。常见的函数式接口,比如Function,Consumer,Predicate,Supplier都是用@FunctionalInterface进行声明的。
以下是一种自定义的函数式接口:

@FunctionalInterface 
public interface WorkerInterface {

        public void doSomeWork();

}

根据定义,函数式接口只能有一个抽象方法,如果你尝试添加第二个抽象方法,将抛出编译时错误。例如:

@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

    public void doSomeMoreWork();
}

错误:

Unexpected @FunctionalInterface annotation 
    @FunctionalInterface ^ WorkerInterface is not a functional interface multiple 
    non-overriding abstract methods found in interface WorkerInterface 1 error

函数式接口定义好后,我们可以在 API 中使用它,同时利用 Lambda 表达式。例如:

 //定义一个函数式接口
@FunctionalInterface
public interface WorkerInterface {

   public void doSomeWork();

}

public class WorkerInterfaceTest {

public static void execute(WorkerInterface worker) {
    worker.doSomeWork();
}

public static void main(String [] args) {

    //不用Lambda表达式的写法
    execute(new WorkerInterface() {
        @Override
        public void doSomeWork() {
            System.out.println("哈喽,华妹妹!");
        }
    });

    //用Lambda表达式的写法
    execute( () -> System.out.println("华妹妹,我用Lambda表达式向你问好!😘") );
}

}

输出:

哈喽,华妹妹!
华妹妹,我用Lambda表达式向你问好!😘

这上面的例子里,我们创建了自定义的函数式接口并与 Lambda 表达式一起使用。execute() 方法现在可以将 Lambda 表达式作为参数。

Lambda 表达式举例

学习 Lambda 表达式的最好方式是学习例子。

线程可以通过以下方法初始化:

//旧方法:
new Thread(new Runnable() {
@Override
public void run() {
    System.out.println("Hello from thread");
}
}).start();

//新方法:
new Thread(
() -> System.out.println("Hello from thread")
).start();

事件处理可以使用 Java 8 的 Lambda 表达式解决。下面的代码中,我们将使用新旧两种方式向一个 UI 组件添加 ActionListener:

  //Old way:
button.addActionListener(new ActionListener() {
       @Override
       public void actionPerformed(ActionEvent e) {
            System.out.println("The button was clicked using old fashion code!");
       }
});

//New way:
button.addActionListener( (e) -> {
    System.out.println("The button was clicked. From Lambda expressions !");
});

还有一种情况下使用Lambda表达式是很爽的,在开始之前,我们先来了解一下Function,Consumer,Predicate,Supplier这几个Java中常用的函数式接口。

先看一下Function接口定义:

@FunctionalInterface    
public interface Function<T, R>

接口接受两个泛型类型<T, R>.

再看一下接口定义的方法(非静态,非default), 支持lambda表达式的接口只允许定义一个抽象方法(@FunctionalInterface注解的接口,只允许定义一个抽象方法),只要记住这一点,你就不会弄混了。

R apply(T t);    
/**
 * T 入参类型, t 输入参数
 * R 返回值类型
 */

OK,现在明确了, 该接口的lambda表达式应该是接受一个入参,最后要有一个返回值,写法应该是这样的: (x) -> {return y;}

如果你的lambda表达式非常简单,只有一行,那么你可以不写return,不加花括号{},返回值后面可以不加分号。

下面就可以写example了,写一个简单的,再写一个标准的:

    public void testFunction(){
                //简单的,只有一行
        Function<Integer, String> function1 = (x) -> "test result: " + x;
        
        //标准的,有花括号, return, 分号.
        Function<String, String> function2 = (x) -> {
            return "after function1";
        };
        System.out.println(function1.apply(6));
        System.out.println(function1.andThen(function2).apply(6));
    }

OK,Function的例子写完了,接下来写其他的,其实原理懂了,其他的就都简单了,然后就是熟能生巧了。

再看看Supplier的接口定义,这个接口定义比较简单,我就都贴上来了

    @FunctionalInterface
    public interface Supplier<T> {

        /**
         * Gets a result.
         *
         * @return a result
         */
        T get();
    }

接口接受一个泛型<T>,接口方法是一个无参数的方法,有一个类型为T的返回值。 OK, 那么接口的lambda表达式应该是这样的: () -> { return something; },好,下面来写一个example:

public void testSupplier(){
            //简写
    Supplier<String> supplier1 = () -> "Test supplier";
    System.out.println(supplier1.get());
    
    //标准格式
    Supplier<Integer> supplier2 = () -> {
        return 20;
    };
    System.out.println(supplier2.get() instanceof Integer);
}

到这里你或许有一点疑惑,这Supplier到底能用在哪啊?Java 8里新增了一个异步线程的类,很牛逼,很强大的类:CompletableFuture, 里面的很多方法的入参都用到的Supplier,例如: supplyAsync方法。 本文暂时不介绍CompletableFuture。

接下来是Consumer,我们来看一下接口的定义:

@FunctionalInterface
public interface Consumer<T>

然后再看一下里面的抽象方法:

void accept(T t);

现在了解了: 接口接受一个泛型<T>,接口方法是入参类型为T, 无返回值的方法, OK,下面开始写example:

public void testConsumer(){
    Consumer<String> consumer1 = (x) -> System.out.print(x);
    Consumer<String> consumer2 = (x) -> {
        System.out.println(" after consumer 1");
    };
    consumer1.andThen(consumer2).accept("test consumer1");
}

接下来看一下Predicate接口
接口定义:

    @FunctionalInterface    
    public interface Predicate<T>

抽象方法:

    boolean test(T t);

接口接受一个泛型<T>, 接口方法的入参类型是T, 返回值是一个布尔值, OK, 下面写example:

    public void testPredicate(){
        Predicate<String> predicate = (x) -> x.length() > 0;
        System.out.println(predicate.test("String"));
    }

Predicate接口在stream里面用的比较多,感兴趣的可以去看看stream,java 8 里另一个新的东西,很好玩。

看完上面这几个接口的定义和用法,你会发现它们和其他普通的接口没啥不一样的,都只是声明,而不实现细节,唯一特殊之处,就是它被@FunctionalInterface修饰,支持Lambda表达式罢了!

回到刚才的那个话题,有一种情况下用Lambda是很爽的,看下面的代码:

先声明一个数组

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

如果我们要计算所有元素的和,怎么写代码?

public int sumAll(List<Integer> numbers) {
    int total = 0;
    for (int number : numbers) {
        total += number;
    }
    return total;
}

又来个需求,如果只想计算所有偶数的和怎么写代码?我们会基于上面的代码,做一个判断:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;
    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    }
    return total;
}

哈哈,又来几个需求,比如说计算只大于3的所有数之和,你是不是又得把刚才的代码粘贴过来,改叭改叭呢?

看看用Lambda怎么写的吧,结合Predicate使用

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;
    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

对应上面三个需求,分别调用:

sumAll(numbers, n -> true);
sumAll(numbers, n -> n % 2 == 0);
sumAll(numbers, n -> n > 3);

非常的灵活啊!先写到这里吧!

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

推荐阅读更多精彩内容