在介绍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. 初识Selenium以及Selenium常用工具的简单介绍

    一.为什么要学习自动化测试? 在互联网行业中敏捷开发小步快跑,快速迭代,测试环节中回归测试任务大繁琐,手工测试容易漏测,自动化测试可以提高测试效率保证产品质量. 二.学习的层次模型 1.单元自动化测试 ...

  2. 剑指Offer-求1+2+3+...+n

    package Other; /** * 求1+2+3+...+n * 求1+2+3+...+n,要求不能使用乘除法.for.while.if.else.switch.case等关键字及条件判断语句( ...

  3. Python 中 mySQL 中的语句

    class DeleteInventorybusiness(BaseBusiness): def DeleteInventory(self,Delete_goodsID): DeleteInvento ...

  4. [Luogu 1402] 酒店之王

    题目 Description XX酒店的老板想成为酒店之王,本着这种希望,第一步要将酒店变得人性化.由于很多来住店的旅客有自己喜好的房间色调.阳光等,也有自己所爱的菜,但是该酒店只有p间房间,一天只有 ...

  5. Oracle 12c(12.1.0.5)OEM server agent 安装配置

    注意: 此文档为生产上操作文档,省略了IP,oracle用户server,agent 端至少需要sudo,ping,ssh,创建目录权限. 一.安装要求 1.1. 系统情况一览 IP 数据库 OEM ...

  6. 后台运行之[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil]

    // 正常程序退出后,会在几秒内停止工作: // 要想申请更长的时间,需要用到 // beginBackgroundTaskWithExpirationHandler // endBackground ...

  7. mysql在线修复主从同步

    要实现MySQL的主从复制,首先必须打开Master端的binlog记录功能,否则就无法实现.因为整个复制过程实际上就是Slave从aster端获取binlog日志,然后再在Slave上以相同顺序执行 ...

  8. Vim编辑器的注释,解注,删除与恢复

    1. 注释: 将光标移动到注释首部 命令模式下 Ctrl+V,进入列模式 上下移动,选中待注释内容 按大写I,进入插入模式 输入 // or # 按两次退出 2 解注 将光标移动到待解注首部 命令模式 ...

  9. python全栈学习--day2

    一.in的使用 说明:in有相当多的用处,比如判断,循环for 等. 实例一:in 操作符用于判断关键字是否存在于变量中 s = '男人john' print('男孩' in s) print('男孩 ...

  10. MySQL之数据库和表的基本操作(建立表、删除表、向表中添加字段)

    介绍关于数据库和表的一些基本操作 添加字段.给字段添加注释 ); ) COMMENT '统一社会信用代码录入单位'; ,) 更改字段类型 ,) COMMENT '一头签收,@0或空不用,1必须'; 有 ...