lambda表达式的使用

来源:互联网 发布:涂色 app 源码 编辑:程序博客网 时间:2024/03/28 23:02

1、为什么要用lambda表达式?

简单说lambda表达式为了替换一些匿名内部类(函数式接口)的使用,这样的匿名内部类有以下问题:
1. 语法过于冗余
2. 匿名类中的 this 和变量名容易使人产生误解
3. 类型载入和实例创建语义不够灵活
4. 无法捕获非 final 的局部变量
而lambda表达式彻底解决了问题1和2,绕开了问题3,减轻了问题4的困扰。

2、函数式接口

指有且仅有一个抽象方法的接口,这里的抽象方法指的是该接口自己特有的抽象方法,而不包含它从其上级继承过来的抽象方法,如java.lang.Comparable,之前被称为SAM类型,即单抽象方法(Single Abstract Method)。我们并不需要声明一个接口是函数式接口,编译器会根据接口的结构自行判断,但是也可以用 @FunctionalInterface来显式指定一个接口是函数式接口,加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。关于java8中出现的函数式接口请参考java8函数式接口(建议学完这篇博客再看)。

3、lambda表达式语法

格式如:(parameters) -> expression或(parameters) ->{ statements; }
以下是lambda表达式的重要特征:
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

4、综合逐步简化实例

public class Lambda {    public static void main(String[] args) {         //产生对象         //User只有id(String)和age(Integer)两个属性,        List<User> ts = new ArrayList(100);        for (int i = 1; i < 100; i++) {            ts.add(new User(new DecimalFormat("00000").format(i), new Random().nextInt(100) + 1));        }        //实现对年龄排序        ts.sort(new Comparator<User>() {            @Override            public int compare(User o1, User o2) {                return o1.getAge().compareTo(o2.getAge());            }        });        //去掉冗余的匿名类        ts.sort((User u1, User u2) -> u1.getAge().compareTo(u2.getAge()));        //可类型推导和静态导入,注:u1u2前没有写声明类型        ts.sort((u1, u2) -> u1.getAge().compareTo(u2.getAge()));        //注意lambda表达式结构        ts.sort((u1, u2) -> { return u1.getAge().compareTo(u2.getAge());});        //抽象程度依然很差(如果比较的值是基本数据类型那么情况会更糟),借助 Comparator里的comparing方法实现比较操作:        ts.sort(Comparator.comparing(user -> user.getAge()));        //方法引用        ts.sort(Comparator.comparing(User::getAge));        ts.forEach(System.out::println);    }}

接下来详细介绍本例的一些技术。

5、目标类型

函数式接口的名称并不是 lambda 表达式的一部分。那么问题来了,对于给定的 lambda 表达式,它的类型是什么?答案是:它的类型是由其上下文推导而来。如:

Comparator<User> comparator = (u1, u2) -> u1.getAge().compareTo(u2.getAge());

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

Callable<String> c = () -> "done";PrivilegedAction<String> a = () -> "done";

第一个lambda表达式是Callable<String>的实例,第二个lambda表达式是PrivilegedAction<String>的实例。
编译器负责推导 lambda 表达式类型。它利用 lambda 表达式所在上下文 所期待的类型 进行推导,这个被期待的类型被称为目标类型。lambda 表达式只能出现在目标类型为函数式接口的上下文中。

当然,lambda 表达式对目标类型也是有要求的。编译器会检查 lambda 表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda 表达式才可以被赋给目标类型 T:
- T 是一个函数式接口
- lambda 表达式的参数和 T 的方法参数在数量和类型上一一对应
- lambda 表达式的返回值和T的方法返回值相兼容(Compatible)
- lambda 表达式内所抛出的异常和T的方法 throws 类型相兼容

6、目标类型的上下文(代码)

带有目标类型的上下文:
- 变量声明
- 赋值
- 返回语句
- 数组初始化器
- 方法和构造方法的参数
- lambda 表达式函数体
- 条件表达式
- 强制类型转换
(1)目标类型是被赋值或被返回的类型(前四种)

 //变量声明 Comparator<Integer> integerComparator; //赋值 integerComparator = (integer1, interger2) -> integer1.compareTo(interger2); //数组初始化 FileFilter[] filters = {f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")}; //返回语句public Comparator<Integer> getIntegerComparator() {    return (integer1, interger2) -> integer1.compareTo(interger2);}

(2)方法和构造方法的参数

方法参数的类型推导要相对复杂些:目标类型的确认会涉及到其它两个语言特性:重载解析(Overload resolution)和参数类型推导(Type argument inference)。

重载解析会为一个给定的方法调用(method invocation)寻找最合适的方法声明(method declaration)。由于不同的声明具有不同的签名,当 lambda 表达式作为方法参数时,重载解析就会影响到 lambda 表达式的目标类型。编译器会通过它所得的信息来做出决定。如果 lambda 表达式具有显式类型(参数类型被显式指定),编译器就可以直接 使用lambda 表达式的返回类型;如果lambda表达式具有 隐式类型(参数类型被推导而知),重载解析则会忽略lambda 表达式函数体而只依赖 lambda 表达式参数的数量。

如果在解析方法声明时存在二义性,我们就需要利用转型(cast)或显式 lambda 表达式来提供更多的类型信息。如果 lambda 表达式的返回类型依赖于其参数的类型,那么 lambda 表达式函数体有可能可以给编译器提供额外的信息,以便其推导参数类型。

List<User> ts = new ArrayList(100);Stream<String> stringStream = ts.stream().map(user -> user.getId());

在上面的代码中,ts的类型是List<User>ts.stream()得到的类型是Stream<User>map方法接收一个类型为Function<T, R>的函数式接口,T类型是User,但是R类型是未知的。由于在重载解析之后 lambda 表达式的目标类型仍然未知,我们就需要推导 R 的类型:通过对 lambda 表达式函数体进行类型检查,我们发现函数体返回 String,因此 R 的类型是 String,因而 map() 返回Stream<String>。绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要:

  • 使用显式 lambda 表达式(为参数 p 提供显式类型)以提供额外的类型信息
  • 把 lambda 表达式转型为Function<User, String>
  • 为泛型参数 R 提供一个实际类型。如(.<String>map(user -> user.getId()))

    lambda 表达式本身也可以为它自己的函数体提供目标类型,也就是说 lambda 表达式可以通过外部目标类型推导出其内部的返回类型,这意味着我们可以方便的编写一个返回函数的函数:

Supplier<Runnable> sr = () -> () -> { System.out.println("hello world"); };

(3)条件表达式和强制类型转换

//条件表达式Runnable r = 10 % 2 == 0 ? () -> System.out.println("偶数") : () -> System.out.println("奇数");//强制类型转换Object obj = (Runnable)() -> System.out.println("偶数");

7、与内部类的比较

(1)词法作用域

在内部类中使用变量名(以及 this)非常容易出错。内部类中通过继承得到的成员(包括来自 Object 的方法)可能会把外部类的成员掩盖,此外未限定的 this 引用会指向内部类自己而非外部类。相对于内部类,lambda 表达式的语义就十分简单:它不会从超类中继承任何变量名,也不会引入一个新的作用域。lambda 表达式基于词法作用域,也就是说lambda 表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括 lambda 表达式的形式参数)。此外this关键字及其引用在 lambda 表达式内部和外部也拥有相同的语义。

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

执行结果如下:
执行结果

(2)变量捕获
在java8以前内部类使用外部变量必须声明为final,而现在放宽了这个限制——对于 lambda 表达式和内部类,我们允许在其中捕获那些符合 有效只读(Effectively final)的局部变量。如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,换句话说,加上 final 后也不会导致编译错误的局部变量就是有效只读变量。

String hello = "Hello";Runnable r2 = () -> System.out.println(hello);

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

这个特性对内存管理是一件好事:内部类实例会一直保留一个对其外部类实例的强引用,而那些没有捕获外部类成员的 lambda 表达式则不会保留对外部类实例的引用。要知道内部类的这个特性往往会造成内存泄露。
尽管放宽了对捕获变量的语法限制,但试图修改捕获变量的行为仍然会被禁止,比如下面这个例子就是非法的:

String hello = "Hello";Runnable r2 = () -> {    hello = "";//不可对hello赋值    System.out.println(hello);};

为什么要禁止这种行为呢?因为这样的 lambda 表达式很容易引起 race condition。除非我们能够强制(最好是在编译时)这样的函数不能离开其当前线程,但如果这么做了可能会导致更多的问题。简而言之,lambda 表达式对 值 封闭,对 变量 开放。

8、方法引用

方法引用与lambda表达式具有相同的特性,你可以把方法引用看作是lambda表达式的简写形式。方法引用分类如下:
- 静态方法引用:ClassName::methodName
- 实例上的实例方法引用instanceReference::methodName
- 超类上的实例方法引用:super::methodName
- 类型上的实例方法引用:ClassName::methodName
- 构造方法引用:Class::new
- 数组构造方法引用:TypeName[]::new
以下代码演示了各种方式的使用

//静态方法引用Comparator<Integer> Lcomparator = (a, b) -> a.compareTo(b);Comparator<Integer> comparator = Integer::compare;//实例上的实例方法引用User tempUser = new User("0001", 21);Comparator<User> LcomparatorObj = (u1, u2) -> tempUser.compareByAge(u1, u2);Comparator<User> comparatorObj = tempUser::compareByAge;//类型上的实例方法引用Comparator<User> comparatorByAge = User::compareByAge;//构造方法引用BiFunction<String, Integer, User> LcreateUser = (id,age)->new User(id,age);BiFunction<String, Integer, User> createUser = User::new;System.out.println(createUser.apply("00001", 20).toString());//数组构造方法引用IntFunction<User[]> LarrayUser = (len)->new User[len];IntFunction<User[]> arrayUser = User[]::new;User[] users = arrayUser.apply(10);System.out.println(users.length);//超类上的实例方法引用public String superToString() {    Supplier<String> LsuperString = () -> super.toString();    Supplier<String> superString = super::toString;    return superString.get();}//this上的实例方法引用public String thisToString() {    Supplier<String> LsuperString = () -> this.toString();    Supplier<String> superString = this::toString;    return superString.get();}

9、结语

本文重在帮助大家理解和掌握java8中的lambda表达式,涉及到的功能非常有限。大部分内容源自深入理解Java 8 Lambda这篇博客,作者曾经的一些困惑也恰是我想知道的,但是学习起来还是比较困难的。

2 0
原创粉丝点击