在介绍Lambda表达式之前,我们先来看只有单个方法的Interface(通常我们称之为回调接口):

public interface OnClickListener {
void onClick(View v);
}

我们是这样使用它的:

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.setText("lalala");
}
});

这种回调模式在各种框架中非常流行,但是像上面这样的匿名内部类并不是一个好的选择,因为:

  • 语法冗余;
  • 匿名内部类中的this指针和变量容易产生误解;
  • 无法捕获非final局部变量;
  • 非静态内部类默认持有外部类的引用,部分情况下会导致外部类无法被GC回收,导致内存泄露。

令人高兴的是Java8为我们带来了Lambda,下面我们看看利用Lambda如何实现上面的功能:

button.setOnClickListener(v -> v.setText("lalala"));

怎么样?!五行代码用一行就搞定了!!!

在这里补充个概念函数式接口;前面提到的OnClickListener接口只有一个方法,Java中大多数回调接口都有这个特征:比如Runnable和Comparator;我们把这些只拥有一个方法的接口称之为函数式接口

一、Lambda表达式

匿名内部类最大的问题在于其冗余的语法,比如前面的OnClickListener中五行代码仅有一行是在执行任务。Lambda表达式是匿名方法,前面我们也看到了它用极其轻量的语法解决了这一问题。

下面给大家看几个Lambda表达式的例子:

(int x, int y) -> x + y                      //接收x和y两个整形参数并返回他们的和
() -> 66 //不接收任何参数直接返回66
(String name) -> {System.out.println(name);} //接收一个字符串然后打印出来
(View view) -> {view.setText("lalala");} //接收一个View对象并调用setText方法

Lambda表达式语法由参数列表->函数体组成。函数体既可以是一个表达式也可以是一个代码块。

  • 表达式:表达式会被执行然后返回结果。它简化掉了return关键字。
  • 代码块:顾名思义就是一坨代码,和普通方法中的语句一样。

二、目标类型

通过前面的例子我们可以看到,lambda表达式没有名字,那我们怎么知道它的类型呢?答案是通过上下文推导而来的。例如,下面的表达式的类型是OnClickListener

OnClickListener listener = (View v) -> {v.setText("lalala");};

这就意味着同样的lambda表达式在不同的上下文里有不同的类型

Runnable runnable = () -> doSomething();  //这个表达式是Runnable类型的
Callback callback = () -> doSomething(); //这个表达式是Callback类型的

编译器利用lambda表达式所在的上下文所期待的类型来推导表达式的类型,这个被期待的类型被称为目标类型。lambda表达式只能出现在目标类型函数式接口的上下文中。

Lambda表达式的类型和目标类型的方法签名必须一致,编译器会对此做检查,一个lambda表达式要想赋值给目标类型T则必须满足下面所有的条件:

  • T是一个函数式接口
  • lambda表达式的参数必须和T的方法参数在数量、类型和顺序上一致(一一对应)
  • lambda表达式的返回值必须和T的方法的返回值一致或者是它的子类
  • lambda表达式抛出的异常和T的方法的异常一致或者是它的子类

由于目标类型是知道lambda表达式的参数类型,所以我们没必要把已知的类型重复一遍。也就是说lambda表达式的参数类型可以从目标类型获取:

//编译器可以推导出s1和s2是String类型
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
//当表达式的参数只有一个时括号也是可以省略的
button.setOnClickListener(v -> v.setText("lalala"));

ps: Java7中的泛型方法和<>构造器也是通过目标类型来进行类型推导的,如:

List<Integer> intList = Collections.emptyList>();
List<String> strList = new ArrayList<>();

三、作用域

在内部类中使用变量名和this非常容易出错。内部类通过继承得到的成员变量(包括来说object的)可能会把外部类的成员变量覆盖掉,未做限制的this引用会指向内部类自己而非外部类。

而lambda表达式的语义就十分简单:它不会从父类中继承任何变量,也不用引入新的作用域。lambda表达式的参数及函数体里面的变量和它外部环境的变量具有相同的语义(this关键字也是一样)。

下面我们举个栗子吧!

public class HelloLambda {

    Runnable r1 = () -> System.out.println(this);
Runnable r2 = () -> System.out.println(toString()); @Override
public String toString() {
return "Hello, lambda!";
} public static void main(String[] args) {
new HelloLambda().r1.run();
new HelloLambda().r2.run();
}
}

上面的代码最终会打印两个Hello, lambda!,与之相类似的内部类则会打印出类似HelloLambda$1@32a890HelloLambda$1@6b32098这种出乎意料的字符串。

总结:基于词法作用域的理念,lambda表达式不可以掩盖任何其所在上下文的局部变量。

四、变量捕获

在Java7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为final就会产生一个编译错误。但是在Java8中放宽了这一限制–对于lambda表达式和内部类,允许在其中捕获那些符合有效只读的局部变量(如果一个局部变量在初始化后从未被修改过,那么它就是有效只读)。

Runnable getRunnable(String name){
String hello = "hello";
return () -> System.out.println(hello+","+name);
}

对于this的引用以及通过this对未限定字段的引用和未限定方法的调用本质上都属于使用final局部变量。包含此类引用的lambda表达式相当于捕获了this实例。在其他情况下,lambda对象不会保留任何对this的应用。

这个特性对内存管理是极好的:要知道在java中一个非静态内部类会默认持有外部类实例的强引用,这往往会造成内存泄露。而在lambda表达式中如果没有捕获外部类成员则不会保留对外部类实例的引用。

不过尽管Java8放宽了对捕获变量的语法限制,但试图修改捕获变量的行为是被禁止的,比如下面这个例子就是非法的:

int sum  = 0;
list.forEach(i -> {sum += i;});

为什么要禁止这种行为呢?因为这样的lambda表达式很容易引起race condition

lambda表达式不支持修改捕获变量的另外一个原因是我们可以使用更好的方式来实现同样的效果:使用规约(condition)。java.util.stream包提供了各种规约操作,关于Java8中的Stream API我们放到下一章介绍。

五、方法引用

lambda表达式允许我们定义一个匿名方法,并以函数式接口的方式使用它。Java8能够在已有的方法上实现同样的特性。

方法引用和lambda表达式拥有相同的特性(他们都需要一个目标类型,并且需要被转化为函数式接口的实例),不过我们不需要为方法引用提供方法体,我们可以直接通过方法名引用已有方法。

以下面的代码为例,假设我们要按照userName排序

class User{

    private String userName;

    public String getUserName() {
return userName;
}
...
} List<User> users = new ArrayList<>();
Comparator<User> comparator = Comparator.comparing(u -> u.getUserName());
Collections.sort(users, comparator);

我们可以用方法引用替换上面的lambda表达式

Comparator<User> comparator = Comparator.comparing(User::getUserName);

这里的User::getUserName被看做是lambda表达式的简写形式。尽管方法引用不一定会把代码变得更紧凑,但它拥有更明确的语义–如果我们想要调用的方法拥有一个名字,那么我们就可以通过方法名调用它。

方法引用有很多种,它们的语法如下:

  • 静态方法引用:ClassName::methodName
  • 实例上的实例方法引用:instanceReference::methodName
  • 超类上的实例方法引用:super::methodName
  • 类型上的实例方法引用:ClassName::methodName
  • 构造方法引用:Class::new
  • 数组构造方法引用:TypeName[]::new

如果你喜欢我的文章,就关注下我的知乎专栏或者在 GitHub 上添个 Star 吧!

Java8新特性第1章(Lambda表达式)的更多相关文章

  1. Java8新特性(一)——Lambda表达式与函数式接口

    一.Java8新特性概述 1.Lambda 表达式 2. 函数式接口 3. 方法引用与构造器引用 4. Stream API 5. 接口中的默认方法与静态方法 6. 新时间日期 API 7. 其他新特 ...

  2. Java8新特性学习笔记(一) Lambda表达式

    没有用Lambda表达式的写法: Comparator<Transaction> byYear = new Comparator<Transaction>() { @Overr ...

  3. Java8新特性 利用流和Lambda表达式对List集合进行处理

    Lambda表达式处理List 最近在做项目的过程中经常会接触到 lambda 表达式,随后发现它基本上可以替代所有 for 循环,包括增强for循环.也就是我认为,绝大部分的for循环都可以用 la ...

  4. Java1.8新特性——接口改动和Lambda表达式

    Java1.8新特性——接口改动和Lambda表达式 摘要:本文主要学习了Java1.8的新特性中有关接口和Lambda表达式的部分. 部分内容来自以下博客: https://www.cnblogs. ...

  5. java8新特性(二)_lambda表达式

    最近一直找java8相关新特性的文章,发现都太没有一个连贯性,毕竟大家写博客肯定都有自己的侧重点,这里找到一本书,专门介绍java8新特性的,感觉大家可以看看<写给大忙人看的JavaSE8> ...

  6. Java基础之java8新特性(1)Lambda

    一.接口的默认方法.static方法.default方法. 1.接口的默认方法 在Java8之前,Java中接口里面的默认方法都是public abstract 修饰的抽象方法,抽象方法并没有方法实体 ...

  7. Java8新特性第3章(Stream API)

    Stream作为Java8的新特性之一,他与Java IO包中的InputStream和OutputStream完全不是一个概念.Java8中的Stream是对集合功能的一种增强,主要用于对集合对象进 ...

  8. Java8新特性 (一)Lambda

    目录 一.Lambda介绍 二.Lambda用法实例 三.Lambda变量作用域 前言: 这两天彻底的复习了一遍Java8的各种新特性,趁着热乎劲,把知识点整理成博客的形式保存一下. 一.Lambda ...

  9. jdk8新特性-亮瞎眼的lambda表达式

    jdk8之前,尤其是在写GUI程序的事件监听的时候,各种的匿名内部类,大把大把拖沓的代码,程序毫无美感可言!既然Java中一切皆为对象,那么,就类似于某些动态语言一样,函数也可以当成是对象啊!代码块也 ...

随机推荐

  1. java--计时器

    计时器 一.窗口化 public class Pro extends JFrame{ private JTextField textField = new JTextField(45);//系统时间文 ...

  2. Java中调用文件中所有bat脚本

    //调用外部脚本String fileips=null;//所有的路径String[] files=null;String fileip=null;//单个路径try { InputStream is ...

  3. 剑指Offer-对称的二叉树

    package Tree; /** * 对称的二叉树 * 请实现一个函数,用来判断一颗二叉树是不是对称的.注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的. */ public class ...

  4. equals和hashCode详解

    equals和hashCode详解 http://www.cnblogs.com/Qian123/p/5703507.html

  5. Mysql设置字段自动获取时间

    问题:现在在用户表中有一个字段表示用户创建的时间 第一个想法是写一段程序获得系统当前时间,想想就太麻烦了,果断放弃,之后想到了存储过程和函数,再想想还要编写存储过程或者函数的代码,有点放弃的想法,但是 ...

  6. 笔记:Spring Cloud Ribbon 客户端配置详解

    自动化配置 由于 Ribbon 中定义的每一个接口都有多种不同的策略实现,同时这些接口之间又有一定的依赖关系,Spring Cloud Ribbon 中的自动化配置能够很方便的自动化构建接口的具体实现 ...

  7. 笔记:JDBC 数据库

    数据库 URL 在连接数据库时,我们必须使用各种与数据库类型相关的参数,例如主机名.端口号和数据库名称等,JDBC使用了一种与普通URL相类似的语法来描述数据库,JDBC URL 一般语法为: jdb ...

  8. Cxf 自动生成客户端服务端代码

    第一步: 下载apache-cxf安装包.并安装. 第二步: 配置cxf的环境变量. CXF_HOME = "CXF安装路径". 例如:F:\apache-cxf-2.1.2 在P ...

  9. c++ --> 父类与子类间的继承关系

    父类与子类间的继承关系 一.父类与子类 父类与子类的相互转换 1.派生类的对象可以赋给基类,反之不行 2.基类的指针可以指向派生类,反之不行 3.基类的引用可以初始化为派生类的对象,反之不行 4.派生 ...

  10. 用SpringBoot搭建简单电商项目 01

    前几节呢,我们已经简单介绍了SpringBoot框架的使用,从这一节开始,我们尝试着使用SpringBoot框架来一步一步搭建一个简单电商项目.当然了,这不是真正的电商项目,你可以看成是一个CRUD案 ...