Java 8 Lambda 表达式详解

lambda 表达式,相信大家都不陌生,就算没有用过,那应该也听说过。我也是一样,在使用新特性 stream 流处理集合相关的代码时接触到这种语法,其他地方倒是不经常使用。所以也是仅仅知道一些皮毛,对于其中的原理什么的也不怎么清楚。

今天准备系统的学习一番,话不多说,接下来就开始我们的学习。

lambda 表达式介绍

lambda 表达式是 Java 8 的一个新特性,可以取代大部分的匿名内部类,简化了匿名委托的使用,让你让代码更加简洁,优雅。

比较官方的定义是这样的:

lambda 表达式是一个可传递的代码块(或者匿名函数),可以在以后执行一次或多次。

这个匿名函数没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。lambda 表达式也可称为闭包

在 Java 中传递一个代码段并不容易,你不能直接传递代码段。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。接下来就看看 Java 是怎么来处理代码块的。

lambda 表达式的语法

Java 中有一个 Comparator 接口用来排序。这是 Java 8 以前的代码形式:

public class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
}
String[] strArr = new String[]{"abcde", "qwer"};
Arrays.sort(strArr, new LengthComparator());

我们需要定义一个实现了 Comparator 接口的类,并实现里面的 compare() 方法,然后把这个类当做参数传给 sort 方法。

而我们使用 lambda 表达式就可以这样来写:

Arrays.sort(strArr, (String a, String b) -> a.length() - b.length());

其中的 (String a, String b) -> a.length() - b.length() 就是一个 lambda 表达式。

lambda 表达式就是一个代码块,以及必须传入代码的变量规范

lambda 表达式的一些例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

再看一个例子加深理解:

// 用匿名内部类的方式来创建线程 
new Thread(new Runnable() { 
    @Override 
    public void run() { 
        System.out.println("hello world"); 
    } 
});


// 使用Lambda来创建线程
new Thread(() -> System.out.println("hello world"));

注意:

如果一个 lambda 表达式只在某些分支返回一个值,而另外一些分支不返回值,这是不合法的。

例如,(int x) -> { if (x>= 0) return 1; } 就不合法。

函数式接口

Java 中有很多封装代码块的接口,比如上面的 ComparatorActionListener,lambda 表达式与这些接口是兼容的。

但并不是所有的接口都可以使用 lambda 表达式来实现。lambda 规定接口中只能有一个需要被实现的方法(只包含一个抽象方法),不是规定接口中只能有一个方法。 这种接口就称为函数式接口

Java 8 中有另一个新特性:default, 被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。

上面的 ComparatorActionListener,包括 Runnable 就是只有一个需要被实现的方法的接口。即函数式接口

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used...
     */
    public abstract void run();
}

我们来观察下 Runnable 接口,接口上面有一个注解 @FunctionalInterface

通过观察 @FunctionalInterface 这个注解的源码,可以知道这个注解有以下特点:

  1. 该注解只能标记在有且仅有一个抽象方法的接口上。

  2. JDK8 接口中的静态方法和默认方法,都不算是抽象方法。

  3. 接口默认继承 java.lang.Object,所以如果接口显示声明覆盖了 Object 中方法,那么也不算抽象方法。

  4. 该注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

我们再来看一下 Comparator 接口的源码:

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

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        Objects.requireNonNull(other);
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }

    public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(true, comparator);
    }
}

这里只贴出来了部分代码,可以看到排除掉接口的中的静态方法、默认方法和覆盖的 Object 中的方法之后,就剩下一个抽象方法 int compare(T o1, T o2);, 符合 lambda 函数式接口的规范。

方法引用

Java awt 包中有一个 Timer 类,作用是经过一段时间就执行一次。
用 lambda 表达式来处理:

Timer timer = new Timer(1000, event -> System.out.println("this time is " + new Date()));

这里面的 lambda 表达式可以这样表示:

Timer timer = new Timer(1000, System.out::println);

表达式 System.out::println 就是一个方法引用(method reference),它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。

方法引用需要用 ::运算符分隔方法名与对象或类名。主要有3种情况:

1. object::instanceMethod
2. Class::instanceMethod
3. Class::staticMethod

具体解释这里不再叙述,有兴趣的可以看看《Java 核心技术卷1》。

注意:

只有当 lambda 表达式的体只调用一个方法而不做其他操作时,才能把 lambda 表达式重写为方法引用

构造器引用

构造器引用与方法引用很类似,只不过方法名 new。例如,Person::new 是 Person 构造器的一个引用。

假如有一个字符串列表。可以把它转换为一个 Person 对象数组,为此要在各个字符串上调用构造器:

ArrayList<String> names = ... ;
Stream<Persion> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

其中,map 方法会为各个列表元素调用 Person(String) 构造器。

这里的 streammap 会在下一篇博客中学习,这篇暂不讨论。

变量作用域

看下面这个例子:

public static void repeatMessage(String text, int delay){
    ActionListener listener = event -> 
        {
            System.out.printLn(text);
        };
    new Timer(delay, listener).start();
}

// 调用
repeatMessage("Hello", 1000);

可以看到, lambda 表达式可以捕获外围作用域中变量的值。在 Java 中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中,只能引用值不会改变的变量。这是为了保证并发执行过程的安全。

lambda 表达式中捕获的变量必须实际上是事实最终变量。就是这个变量初始化之后就不会再为它赋新值。

lambda 表达式与匿名类的区别

使用匿名类与 Lambda 表达式的一大区别在于关键词的使用。对于匿名类,关键词 this 解读为匿名类,而对于 Lambda 表达式,关键词 this 解读为写就 Lambda 的外部类。也就是说,Lambda 表达式主体内使用的 this 关键字和其所在的类实例相同。

Lambda 表达式与匿名类的另一不同在于两者的编译方法。Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数。

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340