概述

使用Lambda表达式也有一段时间了,有时候用的云里雾里的,是该深入学习Java 8新特性的时候了。作为Java最大改变之一的Lambda表达式,其是Stream的使用基础,那就以它开始吧。

这里,我们先明确需要解决的问题:

  1. 什么是闭包?
  2. Lambda表达式如何写?
  3. 什么是函数接口?
  4. 类型推断在Lambda中的体现。

Lambda表达式

lambda表达式的语法由参数列表、->和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:

  • 表达式:表达式会被执行然后返回执行结果。
  • 语句块:语句块中的语句会被依次执行,就像方法中的语句一样—— 
    • return语句会把控制权交给匿名方法的调用者
    • break和continue只能在循环中使用
  • 如果函数体有返回值,那么函数体内部的每一条路径都必须返回值

表达式函数体适合小型lambda表达式,它消除了return关键字,使得语法更加简洁。

Lambda表达式的变体

不包含参数且主体为表达式

Lambda表达式不包含参数,使用空括号 ()表示没有参数。

OnClickListener mListener = () -> System.out.println("do on Click");

该Lambda表达式实现了OnClickListener接口,该接口也只有一个doOnClick方法,没有参数,且返回类型为void。

public interface OnClickListener {void doOnClick();
}

不包含参数且主体为代码段

该Lambda表达式实现了OnClickListener接口,其主体为一段代码段,在其内用返回或抛出异常来退出。 只有一行代码的Lambda表达式也可使用大括号, 用以明确Lambda表达式从何处开始、到哪里结束。

    OnClickListener mListener_ = () -> {System.out.println("插上电源");System.out.println("打开电视");};

包含一个参数且主体为表达式

Lambda表达式可以包含一个参数,将参数写在()内,如果只有一个参数可以将()省略。

OnItemClickListener mItemListener = position -> System.out.println("position = [" + position + "]");

该Lambda表达式实现了OnItemClickListener接口,该接口也只有一个doItemClickListener方法,其参数为int类型,且返回值为void。

public interface OnItemClickListener {void doItemClickListener(int position);
}

包含多个参数且主体为表达式

Lambda表达式可以包含多个参数,将参数写在()内,此时()不可以省略。

IMathListener mPlusListener = (x, y) -> x + y;
int sum = mPlusListener.doMathOperator(10, 5);

该Lambda表达式实现了IMathListener接口,该接口只有一个doMathOperator方法,其参数为(int, int)类型,且返回值为int类型。

public interface IMathListener {int doMathOperator(int start, int plusValue);
}

包含多个参数且主体为代码段

该Lambda表达式实现了IMathListener接口,该接口只有一个doMathOperator方法,在实现其方法时,创建了一个函数,用来处理结果。

    IMathListener mMaxListener = (x, y) -> {if (x > y) {return x;} else {return y;}};

包含多个参数,指定参数类型且主体为代码段

该Lambda表达式实现了IMathListener接口,在实现时指定了参数类型,此时,调用时方法时的参数类型是指定的,只能传入相应的类型的参数,若不传入相应参数,编译时会报错。

    IMathListener mSubListener = (int x, int y) -> x - y;

尽管与之前相比, Lambda表达式中的参数需要的样板代码很少,但是Java 8仍然是一种静态类型语言。

引用值, 而不是变量

在使用内部类时,我们总是碰到这种情况,需要引用内部类外面的变量,比如其所在方法内的变量,或者该类的全局变量。当使用方法内的变量时,需要将变量声明为final。此时,将变量声明为final, 意味着不能为其重复赋值,同时在匿名内部,实际上是用的使用赋给该变量的一个特定的值。

final String name = getUserName();
button.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent event) {System.out.println("hi " + name);}
});

在Java 8中对放松了这限制,在匿名内部,可以引用其所在方法内的非final变量,但是该变量在既成事实上必须是final,也就是说该变量只能赋值一次。如果再次对其赋值,编译器会报错。

现在,我们暂且将在匿名内部类内使用的其所在方法内的变量命名为A,不管是在匿名类内部还是在匿名类所在的方法内,再次对A进行赋值时,编译器都会报如下错误,其意思是变量A是在内部类中访问的,需要声明为final或有效的final类型。Variable ‘plusFinal’ is accessed from within inner class, needs to be final or effectively final

在Lambda表达式中,也是同样的问题,对于其方法体内引用的外部变量,在Lambda表达式所在方法内对变量再次赋值时,编译器会报同样的错误。也就是意味着,换句话说,Lambda表达式引用的是值,而不是变量。

这种行为也解释了为什么Lambda表达式也被称为闭包。未赋值的变量与周边环境隔离起来,进而被绑定到一个特定的值。在Java 8中引入了闭包这一概念,并将其使用在了Lambda表达式中。众说纷纭的计算机编程语言圈子里,Java是否拥有真正的闭包一直备受争议,因为在 Java 中只能引用既成事实上的final变量。可以肯定的是,Lambda表达式都是静态类型。

闭包在现在的很多流行的语言中都存在,例如 C++、C# 。闭包允许我们创建函数指针,并把它们作为参数传递。

函数接口

函数式接口是什么呢?函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。这类接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),用作Lambda表达式的类型。

从函数接口的定义可以看出,首先要明确的,其是一个接口,而这个接口呢,有且只有一个抽象的方法,那怎么又和函数结合在一起了呢?

public interface IMathListener {int doMathOperator(int start, int plusValue);
}

我们先看一个例子,对于IMathListener接口,这个接口只有一个抽象方法doMathOperator,其接收两个int类型的参数,返回值为int,这个接口可以称为是一个函数接口。当我们声明其对象时,我们可以这样做:

IMathListener mSubListener = (x, y) -> x - y;
mMaxListener.doMathOperator(10, 5));// 其值:5

刚才的声明,就是用Lambda表达式声明了IMathListener的实现,其实现的意义是求两个传入值的差值。这个例子说明了,函数接口可以通过Lambda表达式来实现。下面来看它是如何和函数扯上关系的。

public class Math {public static int doIntPlus(int start, int plusValue) {return start + plusValue;}
}

现有一个Math类,其内声明了一个静态方法doIntPlus,该方法接收两个int类型的参数,返回值为int,也就是说doIntPlus与IMathListener接口中的doMathOperator方法的签名一样。既然签名一样,我们可以搞些什么事情呢。往下看:

IMathListener mPlusListener = Math::doIntPlus;

我们通过函数调用,直接生成了一个IMathListener对象,这里写法不了解的,后续会做介绍,看下Java 8中的引用。我们还是接着说,通过方法引用来支持Lambda表达式。这样现有函数、接口及Lambda表达式完美的结合在一起。

从前面已经知道,Lambda表达式都是静态类型的,也就是说其在编译时就已经被编译,所以刚才被引用的方法必须是静态的,否则编译器会报错。

Non-static method cannot be referenced from a static context

非静态方法不能从静态上下文引用

对于函数接口而言,接口中唯一方法的命名并不重要了,只要方法签名和Lambda表达式的类别相匹配即可。当然了,为了增加代码的易读性,只需在函数接口中为参数起一个代表意义的名字即可。

为了更形象的声明接口,我们可以使用图形来描述不同类型接口。指向函数接口的箭头表示参数, 如果箭头从函数接口射出, 则表示方法的返回类型。若接口没有返回值,没有箭头从函数接口射出。


这里,我们应该对函数接口有了清晰的认识。对于一个函数接口而言,其应该有以下特性:

  • 只具有一个方法的接口
  • 其可以被隐式转换为lambda表达式
  • 现有静态方法可以支持lambda表达式
  • 每个用作函数接口的接口都应添加 @FunctionalInterface注解
@FunctionalInterface
public interface IMathListener {int doMathOperator(int start, int plusValue);
}

该注解会强制 javac 检查一个接口是否符合函数接口的标准。 如果该注解添加给一个枚举 
类型、 类或另一个注解, 或者接口包含不止一个抽象方法, javac 就会报错。 重构代码时, 
使用它能很容易发现问题。

类型推断

关于类型推断,我们在Java 7中,已经不止一次用到了,可能你一直都没有注意到。比如创建一个ArrayList,我们可以这么做:

ArrayList<String> mArrayA = new ArrayList<String>();
ArrayList<String> mArrayB = new ArrayList<>();

在创建mArrayA时,明确指定了ArrayList为String类型,而在创建mArrayB时并未指定ArrayList的类型,编译器是如何知道mArrayB的数据类型呢?在Java 7中,有个神奇的<>操作符,它可使javac推断出泛型参数的类型,这样不用明确声明泛型类型,编译器就可以自己推断出来,这就是它的神奇之处!

对于一个传递的参数,编辑器也可以根据参数的类型来推断具体传入的参数的数据类型。比如有一个方法updateList,其参数为一个String的ArrayList,在调用该方法时,我们传入了一个新建的ArrayList但未指定ArrayList的数据类型,此时编辑器会自行推断传入的ArrayList的数据类型为String,

public void updateList(ArrayList<String> values);updateList(new ArrayList<>());

Lambda表达式中的类型推断,实际上是Java 7就引入的目标类型推断的扩展。javac根据Lambda 表达式上下文信息就能推断出参数的正确类型。 程序依然要经过类型检查来保证运行的安全性, 但不用再显式声明类型罢了,这就是所谓的类型推断。

目标类型是指Lambda表达式所在上下文环境的类型。比如,将 Lambda 表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是 Lambda 表达式的目标类型

以之前提到的IMathListener为例,在下面表达式中,javac会自行将x和y推断为int类型.

IMathListener mSubListener = (x, y) -> x - y;

而在实际开发过程中,为了接口方法的通用性,一般都是使用泛型来指定参数的类型,比如Funtion接口,该接口接收一个F类型的参数并返回一个T类型的值。

Function<String, Integer> string2Integer = Integer::valueOf; 

在这个实例中,javac可以推断出接收的数据类型为String,返回类型为Integer。尽管类型推断已经相当智能,但是其也不是无所不能的。在其自行推断前,你需给出其推断的标注。比如下面的例子,javac并不能够推断出Function的具体数据类型:

Function string2Integer = Integer::valueOf; 

上述代码,编译都不会通过,编译器给出的报错信息如下: 
Operator ‘& #x002B;’ cannot be applied to java.lang.Object, java.lang.Object.

大家都知道泛型的擦除原则,在编译时,编译器会擦除泛型的具体类型。从而,此时编译器认为参数和返回值都是java.lang.Object实例。这已经偏离了我们的思想,就算编译可以通过,也会造成后续逻辑的混乱,从而不知道该行代码,到底在做什么。在使用泛型时,我们一定会指定泛型的具体的数据类型,以作为编译器的类型推断的标准。

方法重载带来的烦恼

在Java中可以重载方法,造成多个方法有相同的方法名,但签名却不一样,尽管这样让多态性展现的淋漓尽致,但是对于类型推断,带来了不少的烦恼,因为javac可能会推断出多种类型。 这时, javac会挑出最具体的类型。比如方法overloadedMethod中,参数类型不同,返回值相同,这是一个典型的方法重载,在使用具体类型调用时,java可以根据具体类型来判断,此时控制台应打印“String”。

overloadedMethod("abc");private void overloadedMethod(Object o) {System.out.print("Object");
}
private void overloadedMethod(String s) {System.out.print("String");
}

如果我们参数传递的是Lambda表达式呢?下面的表达式中,编译器并不知道x和y的数据类型,也并未指定具体的类型,必然造成编译异常。

overloadedMethod((x)->y);

如果在Lambda表达式中指定返回值的数据类型,编译器可以清晰的知道overloadedMethod的参数类型为String类型,根据具体的数据类型,从而调用overloadedMethod(String s) 方法,避免了类型推断不明确的问题。

overloadedMethod((x)->(String)y);

总而言之,Lambda表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循如下规则:

  • 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
  • 如果有多个可能的目标类型,由最具体的类型推导得出;
  • 如果有多个可能的目标类型且最具体的类型不明确, 则需人为指定类型。

总结

Lambda是函数式编程的基础,而函数式编程是技术的发展方向。作为一个成熟的Java开发人员,学习新的编程技术那是必须的,也是值得花时间学习的。

大量的使用Lambda表达式,尽管避免了大量的使用匿名内部类,提高了代码的可读性,可是对猿人们要求更高了,应当对相应的接口或者框架有一定的熟悉程度,否则,看代码就活在云里雾里了。这也是自我相逼提升的一种方式吧。

参考文档

  1. Java中的闭包与回调
  2. 深入理解Java闭包概念
  3. 一见钟情!Java闭包
  4. Java 8 函数式接口
  5. 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

--------------------- 
作者:行云间 
来源:CSDN 
原文:https://blog.csdn.net/io_field/article/details/54380200 
版权声明:本文为博主原创文章,转载请附上博文链接!

Java 8系列之Lambda表达式相关推荐

  1. Java 函数式编程和 lambda 表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...

  2. Java 8 新特性 lambda表达式

    / Created by Manager on 2021/4/1. Java 8 新特性 lambda表达式 StreamAPI 新日期 新注解 */ 视频连接 1https://www.bilibi ...

  3. JDK8系列之Lambda表达式教程和示例

    JDK8系列之Lambda表达式教程和示例 1.Lambada 表达式简介 Lambda 表达式是一种匿名函数,但对Java中的Lambda表达式而已并不完全正确,简单来说,Lambda表达式是一种没 ...

  4. Java 8中使用Lambda表达式的策略模式

    策略模式是" 设计模式:可重用对象的元素"书中的模式之一 . 本书所述的策略模式的意图是: 定义一系列算法,封装每个算法,并使它们可互换. 策略使算法独立于使用该算法的客户端而变化 ...

  5. 精通lambda表达式:java多核编程_Java8 Lambda表达式和流操作如何让你的代码变慢5倍...

    有许许多多关于 Java 8 中流效率的讨论,但根据 Alex Zhitnitsky 的测试结果显示:坚持使用传统的 Java 编程风格--iterator 和 for-each 循环--比 Java ...

  6. Java基础教程:Lambda表达式

    Java基础教程:Lambda表达式 引入Lambda Java 是一流的面向对象语言,除了部分简单数据类型,Java 中的一切都是对象,即使数组也是一种对象,每个类创建的实例也是对象.在 Java ...

  7. Java 8 新特性Lambda 表达式

    Java 8 新特性Lambda 表达式 一.常用循环 二.匿名内部类 三.排序集合 四.循环打印对象 五.根据条件修改 六.排序 七.求和 八.统计方法 九.材料 一.常用循环 public cla ...

  8. java 函数式接口与lambda表达式的关系

    函数式接口与lambda表达式的关系 在java中,lambda表达式与函数式接口是不可分割的,都是结合起来使用的. 对于函数式接口,我们可以理解为只有一个抽象方法的接口,除此之外它和别的接口相比并没 ...

  9. Java函数式编程和Lambda表达式

    文章目录 什么是函数式编程 Lambda表达式 @FunctionalInterface函数式接口 Lambda表达式的格式 方法引用 什么是函数式编程 相信大家都使用过面向对象的编程语言,面向对象编 ...

最新文章

  1. 中国数学竞赛史上最玩命的“赌徒”,为了国家荣誉,他不惜用生命换来了五次世界第一...
  2. jQuery 基础教程 (二)之jQuery对象与DOM对象
  3. 如何在postgresql中模拟oracle的dual表,来测试数据库最基本的连接功能?
  4. 2020年中国无人经济市场研究报告
  5. Spring中事务的使用、抽象机制及模拟Spring事务实现
  6. Activity实现 高亮显示活动节点,和所有已完成过的节点
  7. Python求1~300之间所有的完数
  8. 我的本科毕业论文——Messar即时通讯系统
  9. 【目标检测】目标检测算法-从OverFeat到YOLO
  10. python shp文件_对python 读取线的shp文件实例详解
  11. 万能素材库_2016万能高考作文素材大全
  12. Hive基础知识及底层架构
  13. 大三上学期实训——基于SpringBoot的电影后台管理系统
  14. supplier java8_Java 8之 Supplier示例
  15. Python中计时,看这一篇就够了
  16. [JVM]成为JavaGC专家(1)—深入浅出Java垃圾回收机制
  17. pyinstaller系列之七:打包各种问题汇总
  18. Handler机制——同步屏障
  19. 富人的底层逻辑,诠释什么是自控力,其实就是对自己狠
  20. 应用在洗衣机触摸屏中的触摸芯片

热门文章

  1. JavaScript对象继续总结
  2. maven 报错解决
  3. JSP标签和JSTL标签注意点
  4. mysql之 double write 浅析
  5. CentOS安装setup
  6. Hibernate面试题
  7. 如何访问Wizard控件里的按钮
  8. VLC通信仿真中数字脉冲间隔调制(DPIM)实例
  9. Google code 100个开源项目
  10. Xshell更改命令提示符以及背景配色