美文网首页
Java 8中Lambda学习笔记

Java 8中Lambda学习笔记

作者: 雨林木风博客 | 来源:发表于2018-02-04 17:18 被阅读0次

以前看到别人的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);

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

相关文章

  • Java 学习笔记(13)——lambda 表达式

    title: Java 学习笔记(11)——lambda 表达式tags: [Java 基础, 学习笔记, 函数式...

  • Java8学习笔记目录

    Java8学习笔记 -- 接口的默认方法与静态方法 Java8学习笔记 -- Lambda表达式,Function...

  • 2018-10-23

    java学习笔记(四) 简单地讲讲Lambda表达式 Lambda管中窥豹 在前一篇文章中,已经看见了Lambda...

  • Lambda 表达式

    Lambda 表达式 声明:java8新特性系列为个人学习笔记,参考地址点击这里,侵删!! Lambda 表达式,...

  • Java Lambda 学习笔记

    开发环境 eclipse 4.7.3a jdk 9 案例 通常我们在开发过程中会遇到如下需求: 需求1:找到大于指...

  • Java8 学习笔记

    @(in action系列)[java8, lambda, stream] Java8 学习 java8 能高效的...

  • Kotlin 使用高阶函数实现回调

    前言java 思想实现这样实现的问题kotlin 思想实现 lambda 和 高阶函数 之前学习了 lambda ...

  • Java 8中Lambda学习笔记

    以前看到别人的Java代码里有泛型了,接口了,就特别害怕,不知道是干啥的,虽然上网也查了一些资料,但觉得还是理解的...

  • JAVA8 lambda学习笔记

    在java9都刚出来的日子里,我终于沉下心来好好的学习了lambda,特此记录下来。 简介 官方教程-Englis...

  • Java Lambda学习

    Java Lambda学习 1. 前言 最近开放项目的时候总感觉许多时候代码冗余,所以打算好好研究研究lamdba...

网友评论

      本文标题:Java 8中Lambda学习笔记

      本文链接:https://www.haomeiwen.com/subject/fibmzxtx.html