Java8新特性第1章(Lambda表达式)
在介绍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@32a890和HelloLambda$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表达式)的更多相关文章
- Java8新特性(一)——Lambda表达式与函数式接口
一.Java8新特性概述 1.Lambda 表达式 2. 函数式接口 3. 方法引用与构造器引用 4. Stream API 5. 接口中的默认方法与静态方法 6. 新时间日期 API 7. 其他新特 ...
- Java8新特性学习笔记(一) Lambda表达式
没有用Lambda表达式的写法: Comparator<Transaction> byYear = new Comparator<Transaction>() { @Overr ...
- Java8新特性 利用流和Lambda表达式对List集合进行处理
Lambda表达式处理List 最近在做项目的过程中经常会接触到 lambda 表达式,随后发现它基本上可以替代所有 for 循环,包括增强for循环.也就是我认为,绝大部分的for循环都可以用 la ...
- Java1.8新特性——接口改动和Lambda表达式
Java1.8新特性——接口改动和Lambda表达式 摘要:本文主要学习了Java1.8的新特性中有关接口和Lambda表达式的部分. 部分内容来自以下博客: https://www.cnblogs. ...
- java8新特性(二)_lambda表达式
最近一直找java8相关新特性的文章,发现都太没有一个连贯性,毕竟大家写博客肯定都有自己的侧重点,这里找到一本书,专门介绍java8新特性的,感觉大家可以看看<写给大忙人看的JavaSE8> ...
- Java基础之java8新特性(1)Lambda
一.接口的默认方法.static方法.default方法. 1.接口的默认方法 在Java8之前,Java中接口里面的默认方法都是public abstract 修饰的抽象方法,抽象方法并没有方法实体 ...
- Java8新特性第3章(Stream API)
Stream作为Java8的新特性之一,他与Java IO包中的InputStream和OutputStream完全不是一个概念.Java8中的Stream是对集合功能的一种增强,主要用于对集合对象进 ...
- Java8新特性 (一)Lambda
目录 一.Lambda介绍 二.Lambda用法实例 三.Lambda变量作用域 前言: 这两天彻底的复习了一遍Java8的各种新特性,趁着热乎劲,把知识点整理成博客的形式保存一下. 一.Lambda ...
- jdk8新特性-亮瞎眼的lambda表达式
jdk8之前,尤其是在写GUI程序的事件监听的时候,各种的匿名内部类,大把大把拖沓的代码,程序毫无美感可言!既然Java中一切皆为对象,那么,就类似于某些动态语言一样,函数也可以当成是对象啊!代码块也 ...
随机推荐
- Django+xadmin打造在线教育平台(二)
三.xadmin后台管理 3.1.xadmin的安装 django2.0的安装(源码安装方式): https://github.com/sshwsfc/xadmin/tree/django2 把zip ...
- 15.C++-操作符重载
首先回忆下以前学的函数重载 函数重载 函数重载的本质为相互独立的不同函数 通过函数名和函数参数来确定函数调用 无法直接通过函数名得到重载函数的入口地址 函数重载必然发生在同一个作用域中 类中的函数重载 ...
- workflow--相关笔记
转自http://blog.csdn.net/u014682573/article/details/29922093 1. 工作流技术 工作流(Workflow) 定义:工作流就是将一组任务组织起来, ...
- 微信扫一扫JSSDK 扫一扫报错 invalid signature 问题
交代一下业务场景 在在四个页面都需要用到扫一扫去扫二维码.然而在图三-我的订单 下单中这个页面扫一扫不起效,当时就郁闷了为啥其他页面有用,这里却没用,开始调试吧. 报错信息是签名验证不成功. 自己去打 ...
- [Luogu 1122] 最大子树和
题目 Description 小明对数学饱有兴趣,并且是个勤奋好学的学生,总是在课后留在教室向老师请教一些问题.一天他早晨骑车去上课,路上见到一个老伯正在修剪花花草草,顿时想到了一个有关修剪花卉的问题 ...
- react开发
webpack.config.js var webpack=require("webpack"); var htmlWebpackPlugin=require('html-webp ...
- 源码实现 --> strrev
字符串的顺序反序 函数 char *strrev(char *string); 将字符串string中的字符顺序颠倒过来. NULL结束符位置不变. 返回调整后的字符串的指针. 源码 //其基于的思想 ...
- mysql新手入门随笔2
17.创建表 CREATE TABLE tbname(columnname1 类型 约束条件, columnname2 类型 约束条件,-); 三大类型:数值型,时间日期型,字符串类型 六大约束条件: ...
- Android类参考---SQLiteOpenHelper
public 抽象类 SQLiteOpenHelper 继承关系 java.lang.Object |____android.database.sqlite.SQLiteOpenHelper 类概要 ...
- IDEA 中配置JDK
提前安装jdk,配置环境变量 一.配置jdk 1.依次点开File -->Project Structure,点击左侧标签页,点击SDKs 2.点击+号,选SDK 3.在弹出框选择jdk路径(我 ...