文章目录
文章修改放置于:https://github.com/zgkaii/CS-Study-Notes,欢迎批评指正!
一、Lambda表达式
1.1 函数式编程思想
在数学中,函数是输入元素的集合到可能的输出元素的集合之间的映射关系,并且每个输入元素只能映射到一个输出元素。比如典型的函数 f(x)=x*x
把所有实数的集合映射到其平方值的集合,如 f(2)=4
和 f(-2)=4
。
而函数式编程则是一种编程范式。它把计算当成是数学函数的求值,从而避免改变状态和使用可变数据。它是一种声明式的编程范式,通过表达式和声明而不是语句来编程。
函数式编程有个重要的概念:纯函数。
纯函数需要具备两个特征:
- 对于相同的输入参数,总是返回相同的值(引用透明)。
- 求值过程中不产生副作用,也就是不会对运行环境产生影响。
对于第一个特征,如果是从数学概念上抽象出来的函数,则很容易理解。比如 f(x)=x+1
和 g(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 表达式可以对某些接口进行简单的实现,语法非常简洁,完全没有面向对象编程复杂的束缚。但是使用时有几个问题需要特别注意:
-
使用Lambda必须具有接口,且要求接口中有且仅有一个方法需要被实现。
无论是JDK内置的
Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 -
使用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 = () ->