lambda表达式的使用

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这篇博客,作者曾经的一些困惑也恰是我想知道的,但是学习起来还是比较困难的。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值