Lambda表达式与函数式接口

本文详细介绍了Lambda表达式及其在Java 8中的作用,阐述了函数式编程的思想和纯函数的特征。通过实例展示了Lambda如何简化匿名内部类的使用,特别是在启动线程和处理集合时的优势。同时,文章列举了常用的函数式接口,如Supplier、Consumer、Predicate和Function,解释了它们的作用和使用场景,并展示了方法引用的应用。
摘要由CSDN通过智能技术生成

文章修改放置于:https://github.com/zgkaii/CS-Study-Notes,欢迎批评指正!

一、Lambda表达式

1.1 函数式编程思想

在数学中,函数是输入元素的集合到可能的输出元素的集合之间的映射关系,并且每个输入元素只能映射到一个输出元素。比如典型的函数 f(x)=x*x 把所有实数的集合映射到其平方值的集合,如 f(2)=4f(-2)=4

函数式编程则是一种编程范式。它把计算当成是数学函数的求值,从而避免改变状态和使用可变数据。它是一种声明式的编程范式,通过表达式和声明而不是语句来编程。

函数式编程有个重要的概念:纯函数

纯函数需要具备两个特征:

  • 对于相同的输入参数,总是返回相同的值(引用透明)。
  • 求值过程中不产生副作用,也就是不会对运行环境产生影响。

对于第一个特征,如果是从数学概念上抽象出来的函数,则很容易理解。比如 f(x)=x+1g(x)=x*x 这样的函数,都是典型的纯函数。可见,纯函数中不能使用静态局部变量、非局部变量,可变对象的引用或 I/O 流。这是因为这些变量的值可能在不同的函数执行中发生变化,导致产生不一样的输出。第二个特征,要求函数体中不能对静态局部变量、非局部变量,可变对象的引用或 I/O 流进行修改。这就保证了函数的执行过程中不会对外部环境造成影响。纯函数的这两个特征缺一不可。

// 纯函数
int f1(int x) {
   
  return x + 1;
}

// 不是纯函数,因为引用了外部变量 y
int f2(int x) {
   
  return x + y;
}

// 不是纯函数,因为使用了调用了产生副作用的 Counter 对象的 inc 方法
int f3(Counter c) {
   
  c.inc();
  return 0;
}

// 不是纯函数,因为调用 writeFile 方法会写入文件,从而对外部环境造成影响
int f4(int x) {
   
  writeFile();
  return 1;
}

除此之外,函数式编程还强调函数为“第一公民”。所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,它不仅拥有一切传统函数的使用方式(声明和调用),可以赋值给其他变量(赋值),也可以作为参数,传入另一个函数(传参),或者作为别的函数的返回值(返回)。函数可以作为参数进行传递,意味我们可以把行为"参数化",处理逻辑可以从外部传入,这样程序就可以设计得更灵活。

与传统的命令式编程范式相比,函数式编程范式由于其函数天然的无状态特性,在并发编程中有着独特的优势。像多线程编程的问题根源在于对共享变量的并发访问。如果这样的访问并不需要存在,那么自然就不存在多线程相关的问题。在函数式编程范式中,函数中并不存在可变的状态,也就不需要对它们的访问进行控制。这就从根本上避免了多线程的问题。

当提到 Java 8 的时候,Lambda 表达式总是第一个提到的新特性。Lambda 表达式把函数式编程风格引入到了 Java 平台上,可以极大的提高 Java 开发人员的效率。

Lambda 表达式(lambda expression)是一个匿名函数,Lambda 表达式基于数学中的 λ 演算 得名,直接对应于其中的 lambda 抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。由于Java中函数不是"第一公民",需要封装进接口中;而Lambda表达式 就是主要简写内部匿名类。

在兼顾面向对象特性的基础上,Java语言通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。

1.2 体验Lambda表达式

1.2.1 匿名内部类方式启动线程

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。代码如下:

public class RunnableTest {
   
    public static void main(String[] args) {
   
        new Thread(new Runnable() {
   
            // 重写抽象方法
            @Override
            public void run() {
   
                System.out.println("Hello World!");
            }
        }).start();// 启动线程
    }
}

代码分析

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,只有方法体才是关键所在
1.2.2 Lambda表达式启动线程

借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以这样书写:

public class LambdaTest {
   
    public static void main(String[] args) {
   
        new Thread(() -> System.out.println("Hello World!")).start();
    }
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

1.3 格式及使用

1.3.1 Lambda表达式格式

Lambda表达式省去面向对象中的条条框框,由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称) -> {
    代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

以下是 Lambda 表达式的一些示例:

(int a, int b) -> {
     return a + b; }

() -> System.out.println("Hello World");

(String s) -> {
    System.out.println(s); }

() -> 42

() -> {
    return 3.1415 };
1.3.2 Lambda的使用前提

虽然使用 Lambda 表达式可以对某些接口进行简单的实现,语法非常简洁,完全没有面向对象编程复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个方法需要被实现

    无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。

  2. 使用Lambda必须具有上下文推断
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

Tips:有且仅有一个抽象方法的接口,称为“函数式接口”。

可以使用@FunctionalInterface来修饰函数式接口,要求接口中的抽象方法只有一个。

1.3.3 函数式接口

函数式接口就是:有且仅有一个抽象方法的接口(但是可以有多个非抽象方法的接口)

  • FunctionalInterface注解标注一个函数式接口,不能标注方法枚举属性
  • 如果接口被标注了@FunctionalInterface,这个类就必须符合函数式接口的规范。
  • 即使一个接口没有标注@FunctionalInterface,如果这个接口满足函数式接口规则,依旧被当作函数式接口。

Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

Tips:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实
底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部
类的“语法糖”,但是二者在原理上是不同的。

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
   
	public abstract 返回值类型 方法名称(可选参数信息);
	// 其他非抽象方法内容
}

由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:

public interface MyFunctionalInterface {
   
	void myMethod();
}

与@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注
解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
   
	void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注 意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

1.3.4 Lambda基本使用

我们这里给出六个接口。

/**多参数无返回*/
@FunctionalInterface
public interface NoReturnMultiParam {
   
    void method(int a, int b);
}

/**无参无返回值*/
@FunctionalInterface
public interface NoReturnNoParam {
   
    void method();
}

/**一个参数无返回*/
@FunctionalInterface
public interface NoReturnOneParam {
   
    void method(int a);
}

/**多个参数有返回值*/
@FunctionalInterface
public interface ReturnMultiParam {
   
    int method(int a, int b);
}

/**无参有返回*/
@FunctionalInterface
public interface ReturnNoParam {
   
    int method();
}

/**一个参数有返回值*/
@FunctionalInterface
public interface ReturnOneParam {
   
    int method(int a);
}

使用Lambda表达式:

public class LambdaTest1 {
   
    public static void main(String[] args) {
   
        //无参无返回
        NoReturnNoParam noReturnNoParam = () -> {
   
            System.out.println("无参无返回");
        };
        noReturnNoParam.method();

        //一个参数无返回
        NoReturnOneParam noReturnOneParam = (int a) -> {
   
            System.out.println("一个参数无返回:" + a);
        };
        noReturnOneParam.method(6);

        //多个参数无返回
        NoReturnMultiParam noReturnMultiParam = (int a, int b) -> {
   
            System.out.println("多个参数无返回:" + "{" + a + "," + +b + "}");
        };
        noReturnMultiParam.method(6, 8);

        //无参有返回值
        ReturnNoParam returnNoParam = () ->
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值