java lambda表达式详解

java lambda表达式详解

一、为什么要引入lambda表达式?

java是一门纯面向对象的语言,如果我们需要传递某个可以复用的代码块,我们必须将代码块逻辑抽离到对象中。比如在调用Arrays.sort实现数组排序时,如果有compare代码块逻辑,我们必须将其抽离到Comparator接口中,并传入Arrays.sort:

class MyComparator implements Comparator<Integer>{
   public int compare(Integer i1, Integer i2){
       return i1 - i2 ; 
   }
}
....
Arrays.sort(array , new MyComparator) ; 

在数组完成排序之前,compare方法会不断被sort方法调用。这个可复用代码块的核心逻辑其实只有一行return i1-i2; ,但是我们却写了一个类、new了一个实例来将其传递到调用到位置。这是极不方便的。而在java8引入lambda后,java也有了直接处理代码块的能力,在一定程度上支持了函数式编程。
现在上述数组排序可以这样实现:Arrays.sort(array , (int i1 , int i2) -> i1-i2))。可以看到,通过lambda,我们能直接将代码块传入调用的位置。

二、lambda表达式的语法

lambda表达式结构 : (参数) -> {代码块}

  1. 即使没有参数也需要提供括号,像无参方法一样:
    () -> {System.out.println("test")}
  2. 代码块如果只有一条语句,可以忽略花括号{} ,1 中lambda表达式可以按照如下书写:
    () -> System.out.println("test")
  3. 如果通过上下文能过推导了lamda表达式的参数类型时,书写时可以忽略参数类型 :
    Comparator<Integer> comp = (i1 , i2) -> i1-i2 ;
    通过泛型Integer可以推导出参数类型为Integer,所以lambda表达式忽略了i1、i2的类型参数。(在这里能将代码块直接赋值给接口变量,是因为Comparator接口是一个函数接口,之后会提到)
  4. 无需指定lambda的返回值类型。如 3中所示,i1-i2的返回值类型要么参数类型可以推导出来
  5. 如果lambda表达式中含有分支,某些分支有返回值,某些没有是不合法的:
    (int x) - { if(x > 0) return 1 ;}

三、函数式接口

通俗地说:只有一个抽象方法的接口成为函数式接口。
当我们需要一个函数式接口的对象时(如第二节中的第3点)我们可以直接提供一个lambda表达式,而不用实现接口、new对象。
在第一节的Arrays.sort(array , (int i1 , int i2) -> i1-i2 )中,Aarrays.sort底层其实是接受了一个实现了Comparator<Integer>的接口,在sort调用接口的compare方法时,执行的是传入的lambda体。 从某种意义上说,是编译器将我们的lambda表达式自动封装进了函数式接口中,并并初始化了一个实例,显示出来就是“lambda表达式能转换成函数式接口的实例”:
Comparator<Integer> comp = (int i1 , int i2) -> i1-i2 ;

四、方法引用

如果现存的方法已经能够完成某个动作时,我们可以在需要函数式接口实例的位置,直接引用该方法,而不需要再写lambda表达式。
比如 Integer类中有静态方法:
Integer.compare(i1 , i2)
该方法是用来比较两个整数的方法,源码如下:

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

该方法可以实现比较两个整数,并返回int类型的值,符合比较器的逻辑。那么我们在实现Arrays.sort时可以这样写:
Arrays.sort(array , Integer::compare)即引用Integer的compare方法。
三种方法对比:

'''第一种,实现接口'''
class MyComparator implements Comparator<Integer>{
    public int compare(Integer i1, Integer i2){
        return i1 - i2 ; 
    }
}
....
Arrays.sort(array , new MyComparator()) ; 

'''第二种,使用lambda表达式'''
Arrays.sort(array 
            , (int i1 , int i2) -> (x < y) ? -1 : ((x == y) ? 0 : 1) ) ; 
            
'''第三种,使用方法调用'''
Arrays.sort(array,Integer::compare);

必须注意的是在第三种方式中,没有像lambda表达式一样显式的声明方法的参数和返回值 ,这是因为编译器能够从引用中获得该方法的参数和返回值,并与原本的函数式接口中的方法作对比。
因此:引用的方法的参数数量和类型、返回值都必须和原来函数式接口中的方法一样。比如Integer.compare和Comparator.compare方法在参数的数量类型、返回值类型上一致,如下:

// Integer.compare的定义
public static int compare(int x, int y) 
// Compatator.compare的定义
int compare(T o1, T o2);

所以方法引用Integer::compare 能够转换成Comparetor接口的实例。
方法引用有三种形式:

  1. Class::staticMethod 引用某类型的静态函数 ,如:Math::pow , 等同于:(x, y) -> Math.pow(x,y)
  2. Object::instanceMethod 引用某实例的实例方法 ,效果与第一种相同,且this::instanceMethod和super::instanceMethod是合法的。
  3. Class::instanceMethod 引用某类型的实例方法,实例是传入的第一个参数,
    如String::equels 等同于 :(String x , String y)-> x.equles(y)
    五、构造器引用
    构造器是一种特殊的方法,可以直观地理解为方法引用。引用形式为Class::new。由于使用的较少,在这里不赘述,有兴趣的同学可以自行了解。

六、lambda表达式访问外部变量

lambda可以在代码体中访问lambda表达式外的变量,如下所示:首先定义了一个函数式接口,然后在第8行代码,使用将printMsg方法的参数写入到lambda代码体中。

@FunctionInterface 
interface MyfunctionIT{ 
    void print() ; 
}

public static void printMsg(String text)
{
    repeatPrint(() -> System.out.println(text)); 
}

public void repeatPrint(MyfunctionIT printIt){
    for(int i= 0 ; i<100 ; i++){
        printIt.print();
    }    
}

值得注意的是lambda表达是引用的外部变量不可以改变,这和局部内部类的特性有关。

七、处理lambda表达式

lambda表达式的重点是延迟执行。在我们需要多次运行代码、回调时再运行代码等等非立即执行的时候,就可以将代码块包装成lambda表达,转化为一个函数式接口实例。比如某个动作想要重复10次:
repeat(10 , ()-> System.out.println("Hello , World !"))要接收这个lambda表达式,我们就需要一个接口:

reapet(int n , Runable action){
    for(int i = 0 ; i < n ; i++){
        action.run();
    }
}

在这里,我们使用了Runable接口。当acrion.run执行时,会执行我们传入的lambda表达式。
在进行方法设计时,我们可以利用系统提供的函数式接口:
常用函数式接口

值得注意的是,在高性能需求的场景下。对于基本类型的参数传递,我们需要注意减少自动装箱的开销。比如将接口Consumer 替换为不需要包装类型的IntConsumer接口:

interface Consumer<Integer>{
    void accept(Integer i);
}

@FounctionalInterface
interface IntConsumer{
    void accept(int i);
}

如果自定义函数式接口最好使用@FounctionalInterface定义,一是做标识,二是确保接口只有一个抽象方法。

八、再谈Comparator

Comparator中提供了很多静态方法来创建比较器。比如:comparing方法能够提取类型T中一个键s,将该类型T转化为按照键s可比较的(Comparable)。假设有一个Person对象数组,可如下按名字对数组进行排序:

Arrays.sort(people , 
            Comparator.comparing(Person::getFirstName)
                      .thanComparing(Persion::getSecondName));

comparing函数接收的是一个方法引用,返回的也是一个比较器实例。Person::getFirstName称为键函数。
上述例子的结果是,先用FirstName排序,如果相同,再依照SecondName排序。默认按照字典序排序。
如果我们需要对名字按照长度排序如何自定义呢?如下所示,可以在comparing方法的第二个参数位置传入一个自定义比较器:

  Arrays.sort(people , 
          Comparator.comparing(Person::getFirstName , (s,t) -> s.length - t.length);

这里传入的比较器使用的是lambda表达式。
对于上述动作,也可以直接使用变体Arrays.sort(people , Comparator.comparInt((p) -> p.getName().getlength()))
如果键函数可以返回null,还需要用到nullsFirst和nullsLast适配器。这里不赘述,需要了解的读者可以自行了解。

九、lambda表达式的原理

参考文章Lambda表达式实现原理分析

public class LambdaTest {
    public static void printString(String s, Print<String> print) {
        print.print(s);
    }
    public static void main(String[] args) {
        printString("test", (x) -> System.out.println(x));
    }
}

@FunctionalInterface
interface Print<T> {
    public void print(T x);
}

经过反编译分析还原:

public class LambdaTest {
    public static void PrintString(String s, Print<String> print) {
        print.print(s);
    }

    public static void main(String[] args) {
        PrintString("test", new LambdaTest$$Lambda$1()); // 传入内部类实例
    }

    //lambda表达式代码逻辑的私有静态方法
    private static void lambda$main$0(String x) {
        System.out.println(x);
    }

    // 生成内部类,内部类实现了接口
    static final class LambdaTest$$Lambda$1 implements Print {
        public void print(Object obj) {
            LambdaTest.lambda$main$0((String) obj); //调用lambda私有静态方法
        }
        private LambdaTest$$Lambda$1() {
        }
    }

}

@FunctionalInterface
interface Print<T> {
    public void print(T x);
}

过程:

  1. 在类编译时,会生成一个私有静态方法+一个内部类;
  2. 在内部类中实现了函数式接口,在实现接口的方法中,会调用编译器生成的静态方法;
  3. 在使用lambda表达式的地方,通过传递内部类实例,来调用函数式接口方法。

参考资料

《java核心技术-卷1》

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值