Java 8中的Lambda表达式

💁 作者:小瓦匠
💖 欢迎关注我的个人公众号:小瓦匠学编程。微信号:xiaowajiangxbc
📢 本中涉及到的所有代码资源,可以在公众号中获取,关注并回复:源码下载
👉 本文涉及的源码资源在 coding-004-jdk8 项目中


Lambda表达式是一个匿名函数,可以将函数(行为)像数据一样进行传递。

Lambda表达式是什么

Lambda表达式是Java 8的一个新特性,是函数式编程思想的一个重要体现。同时Lambda表达式有强大的类型推断和方法引用特性。使用Lambda表达式可以让代码变的更加简洁紧凑。

在Java8之前,我们在通过接口传递行为代码时,需要传递一个实现了该接口的实例对象或者使用匿名内部类来处理。比如:

Thread t = new Thread(new Runnable() {
    public void run(){
        System.out.println("Hello world");
    }
});

上面这种方式虽然能实现所期望的功能,但从实现的方式来看,却显得十分啰嗦。而使用Lambda表达式的话,实现起来就很简洁:

Thread t = new Thread(() -> System.out.println("Hello world"));

相比前面的匿名内部类方式,Lambda表达式的实现方式更为直观,不再有实现接口的模板代码,不再声明方法,也没有名字,而是直接给出了方法的实现代码。其实这就是我们所说的函数式编程。

Lambda语法

Lambda表达式由三部分组成:参数列表、箭头、主体。
在这里插入图片描述

  • 参数列表:接口中抽象方法形参列表
  • ->:Lambda操作符,分隔参数和方法体
  • Lambda主体:抽象方法的方法体

下面提供了一些Lambda表达式的例子和使用示例。

// 1. 无参,无返回值
Runnable r = () -> System.out.println("Hello Lambda!");
r.run();

// 2. 有一个参数,无返回值(若只有一个参数时,小括号可以不写)
Consumer<String> con = x -> System.out.println(x);
con.accept("Lambda");

// 3. 有两个及以上的参数,有返回值,并且Lambda体有多条语句
Comparator<Integer> com = (x, y) -> {
    int compare = Integer.compare(x, y);
    return compare;
};
System.out.println(com.compare(1, 3));

// 4. 若Lambda体仅有一条语句,return和大括号可以省略
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
System.out.println(com.compare(1, 3));

Lambda表达式的重要特征

  1. 可选参数类型声明:不需要声明参数类型,编译器可以统一识别参数值。例如:

    (s) -> System.out.println(s)
    // 等同于
    (String s) -> System.out.println(s)
    

    上面两种语法是一样的,编译器会进行类型推断,所以不需要添加参数类型。

  2. 可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。例如:

    // 一个参数不需要添加圆括号
    s -> System.out.println(s)
     // 两个参数添加了圆括号,否则编译器报错
    (x, y) -> Integer.compare(y, x)
    
  3. 可选的Lambda体大括号:如果主体包含了一个语句,就不需要使用大括号。例如:

    // 不需要大括号
    s -> System.out.println(s)
    // 需要大括号
    (s) -> {
        if (s.equals("s")){
            System.out.println(s);
        }
    }
    
  4. 可选的return关键字:如果Lambda体不加{ }就不用写return,Lambda体加上{ }就需要添加return。例如:

    // Lambda体不加{ }就不用写return
    Comparator<Integer> com = (x, y)-> Integer.compare(y, x)
    // Lambda体加上{ }就需要添加return
    Comparator<Integer> com = (x, y)-> {
        return Integer.compare(y, x);
    }
    

在哪里使用Lambda表达式

到底在哪里可以使用Lambda表达式呢?在函数式接口上使用Lambda表达式。因为Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。

函数式接口是什么

所谓函数式接口,就是只定义了一个抽象方法的接口。比如:Java API中的Comparator、Runnable、Callable等。需要注意的是,哪怕你的接口中有很多默认方法,只要接口有且仅有一个抽象方法,那么它就仍然是一个函数式接口。

除此之外,Java 8中增加了一个新的包:java.util.function,它里面包含了很多常用的函数式接口,例如:
在这里插入图片描述

@FunctionalInterface注解

该注解主要用于表示该接口是一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将提示错误。虽然@FunctionalInterface不是必需的,但对于函数式接口设计而言,使用它是比较好的做法。

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

从前面的内容中,我们知道Lambda表达式是函数式接口的一个实例。然而,通过Lambda表达式本身,我们并不能知道它是哪个函数式接口的实现。那Lambda表达式的实际类型是什么呢?

类型检查

实际上,Lambda表达式的类型是从使用Lambda的上下文推断出来的。下面我们通过一个例子,来看一看Lambda表达式的类型检查过程。

public List<Person> test_4(List<Person> list) {
  List<Person> ageThan35 = filter(list, (Person p) -> p.getAge() > 35);
  return ageThan35;
}
/**
 * 数据过滤
 */
private <T> List<T> filter(List<T> list, Predicate<T> p) {
  List<T> result = new ArrayList<>();
  for (T e: list) {
    if (p.test(e)) {
      result.add(e);
    }
  }
  return result;
}

Lambda表达式的类型检查过程如下如所示:
在这里插入图片描述
我们把上下文中Lambda表达式需要的类型称为目标类型。反过来讲,我们也可以利用目标类型来检查一个Lambda表达式是否可以用于某个特定的上下文。

类型推断

类型推断就是说我们不需要指定Lambda表达式的参数类型,Java编译器会从目标类型中推断出适合Lambda表达式的签名。所以我们可以在Lambda语法中省去标注参数类型。

需要注意的是,有时候显式写出类型更易读,有时候去掉它们更易读。对于如何让代码更易读,程序开发人员必须做出自己的选择。

局部变量使用限制

Lambda表达式可以没有限制地引用实例变量和静态变量。但是Lambda表达式引用的局部变量在被创建后不得被重新赋值,也就是说被引用的局部变量必须是最终态(即隐性的具有final语义),否则会编译错误。

public void test_5() {
    int x = 1;
    Runnable runnable = () -> {
        // Variable used in lambda expression should be final or effectively final
        // System.out.println(x++);
        System.out.println(x);
    };
    runnable.run();
}
public void test_6() throws Exception {
    IntHolder holder = new IntHolder();
    Callable callable = () -> {
        // 使用对象引用 - 编译通过
        holder.x++;
        return holder;
    };
    IntHolder call = (IntHolder) callable.call();
    System.out.println("变更后:" + call.x);
}

private static class IntHolder {
    int x = 1;
}

由此可见,Lambda表达式是对值类型封闭,对引用类型开放。

这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。

Lambda方法引用

我们可以把方法引用看作是某些Lambda表达式的快捷写法。

如何使用方法引用

在使用了 Lambda 表达式的地方,就可以使用方法引用。示例如下:
在这里插入图片描述

方法引用语法

静态方法引用
如果我们在Lambda表达式中要调用某个类的静态方法,并且这个静态方法的参数列表和函数式接口中抽象方法的参数列表相对应时,我们可以采用静态方法引用来处理。

语法:ClassName::methodName,在静态方法名后面不需要加括号,也不用加参数,因为编译器都可以推断出来。

在工具类中提供一个静态方法

public class Utils {
    public static int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

使用Lambda表达式实现

Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));

使用静态方法引用实现

Collections.sort(list, Utils::compare);

对象方法引用
与引用静态方法类似,只不过在传递方法时,需要一个对象的存在。

语法:instanceReference::methodName

提供一个方法类

public class MyCompare {
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

使用Lambda表达式实现

Collections.sort(list, (o1, o2) -> myCompare.compare(o1, o2));

使用对象方法引用实现

Collections.sort(list, myCompare::compare);

如果我们要执行的方法引用是当前所在的类的方法时,我们可以使用如下格式:this::methodName

类方法引用

如果在引用某一个对象的方法时,而这个对象本身又是Lambda表达式的一个参数,并且其他参数(或无参)可以对应到该对象方法的参数列表,此时可以使用类方法引用进行简化。

语法:ClassName::methodName

// 使用Lambda表达式实现
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
// 类方法引用实现
Collections.sort(list, Integer::compareTo);

构造方法引用
如果我们要通过Lambda表达式来新建一个对象,并且这个对象的构造方法的参数列表和函数式接口中抽象方法的参数列表相对应时,我们可以采用构造方法引用来处理。

语法:

  • 构造方法引用:ClassName::new
  • 数组构造方法引用:TypeName[]::new

无参构造方法

Supplier<Person> p = () -> new Person();
Person person = p.get();
// 等价于
Supplier<Person> p1 = Person::new;
Person person1 = p1.get();

一个参数的构造方法

Function<String, Person> p = (code) -> new Person(code);
Person person = p.apply("C01001");
// 等价于
Function<String, Person> p1 = Person::new;
Person person1 = p1.apply("C01001");

两个参数的构造方法

BiFunction<String, String, Person> p = (code, name) -> new Person(code, name);
Person person = p.apply("C01001", "小瓦匠");
// 等价于
BiFunction<String, String, Person> p1 = Person::new;
Person person1 = p1.apply("C01001", "小瓦匠");

三个参数的构造方法,首先需要自己定义一个与构造函数引用的签名匹配的函数式接口,然后通过该函数式接口进行实现。

@FunctionalInterface
public interface TriFunction<T, U, V, R>{
    R apply(T t, U u, V v);
}
TriFunction<String, String, String, Person> p = (code, name, address) -> new Person(code, name, address);
Person person = p.apply("C01001", "小瓦匠", "北京市");
// 等价于
TriFunction<String, String, String, Person> p1 = Person::new;
Person person1 = p1.apply("C01001", "小瓦匠", "北京市");

总结

  1. Lambda表达式可以理解为是一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
  2. 并且我们可以将Lambda表达式作为函数式接口的一个实例。
  3. 函数式接口就是仅仅声明了一个抽象方法的接口。只有在接受函数式接口的地方才可以使用Lambda表达式。
  4. 方法引用其实就是某些Lambda表达式的快捷写法。在使用了Lambda表达式的地方,就可以使用方法引用。

以上就是我对Java 8中Lambda表达式的一个理解,如果我的分享对你有帮助,请你动动手指,分享给更多的人。

关注我,编程不再难!


在这里插入图片描述
📌 学习 积累 沉淀 分享
💖 欢迎关注我的个人公众号:小瓦匠学编程! 微信号:xiaowajiangxbc
🔎 扫描二维码或微信搜索 “小瓦匠学编程” 即可关注。

(本文完)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小瓦匠学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值