文章目录
- 函数式编程介绍
- 纯函数
- Lambda表达式基础
- [函数式接口(Functional Interface)](#函数式接口(Functional Interface))
- [**方法引用(Method Reference)详解**](#方法引用(Method Reference)详解)
-
- [**1. 方法引用的基本概念**](#1. 方法引用的基本概念)
- [**2. 方法引用的四种形式**](#2. 方法引用的四种形式)
-
- [**2.1 引用静态方法**](#2.1 引用静态方法)
- [**2.2 引用特定对象的实例方法**](#2.2 引用特定对象的实例方法)
- [**2.3 引用构造方法**](#2.3 引用构造方法)
- [**3. 方法引用与函数式接口的关系**](#3. 方法引用与函数式接口的关系)
- [**4. 方法引用的实际应用**](#4. 方法引用的实际应用)
- 常用的函数式接口
-
- [**1. Consumer接口(级联多个Consumer)**](#1. Consumer接口(级联多个Consumer))
-
-
- **Consumer的基本用法**
- [**级联Consumer - andThen方法**](#级联Consumer - andThen方法)
- **更复杂的级联示例**
-
- [**2. Predicate接口与对象查找**](#2. Predicate接口与对象查找)
- [**3. Function接口及相关接口**](#3. Function接口及相关接口)
函数式编程介绍
Java8新引入函数式编程方式,大大的提高了编码效率。
从面向对象编程视角来看,程序中使用的变量,其实只是一个值的容器,这个容器中,可以放置不同的值。
从函数式编程视角,变量其实只是值的一个"别名",值本身是不能改的 。
采用函数式编程思想设计类,强调"类"所封装的数据,应该只被初始化一次,之后就不再更改。
JDK中的String
类型,就是用"函数式编程"风格设计出来的一个例子。
Java 8引入了Lambda
表达式特性,提供了Stream API
,其内置的函数支持动态组合和级联调用,能够方便地实现"声明式"的编程方式。
函数式与面向对象编程的基本构造元素:
面向对象编程,编程的基本单元是"类",函数必须放在类中,从属于"类"。
函数式编程,以"函数"作为编程的基本构造块,函数之间可以相互协作和动态组合。
函数式编程能很容易地实现"行为的参数化"
函数封装了"行为",在函数式编程中,是"把函数作为值"来看待的。
既然"函数是值",那么它就可以作为另一个函数的参数或返回值,这个就是"行为的参数化"。
Java使用Lambda
表达式来代表那些需要反复传递的行为,将其作为函数参数或返回值,从而实现了"行为参数化"。
这样讲有点抽象ヽ(´¬`)ノ,下面进行详细介绍。
纯函数
函数式编程中的"函数",强调要消除"副作用"。
所谓"副作用",就是指:
(1)函数的执行,受到其"运行上下文"的影响,在不同的运行环境中执行,会得到不同的结果。
(2)函数执行之后,会修改外界的数据,从而对外界的状态有所影响。
没有"副作用"的函数,可以放心地让多个线程调用。
抛出异常的函数,不满足"函数式编程"的要求
"纯函数"------没有副作用的函数:
一个方法是不是"Pure Function(纯函数)",关键就是它的运行,是不是有"副作用(Side Effect)"。纯函数是没有副作用的,只要输入参数值一定,它的结果总是一致的,从而可以安全地被跨线程调用而无需考虑线程同步问题。
java
public class SideEffectIllustration {
// 没有副作用的方法
public int f1(int x) {
return x * 2;
}
private int state = 0;
// 有副作用的方法
public int f2(int x) {
state++;
return x * 2 + state;
}
public static void main(String[] args) {
SideEffectIllustration obj = new SideEffectIllustration();
//创建10个线程,每个线程都调用f1或f2方法,观察多线程环境下
//Pure Function的输出与有副作用的方法的输出有何区别
Thread[] theads = new Thread[10];
for (int i = 0; i < theads.length; i++) {
final int index = i;
theads[i] = new Thread(() -> {
// Note:切换以下两句的注释,观察输出的结果
System.out.println(String.format("第%d次,结果为:%d", index + 1, obj.f1(5)));
//System.out.println(String.format("第%d次,结果为:%d", index + 1, obj.f2(5)));
});
theads[i].start();
}
}
}
在实际开发中,推荐尽量编写"纯函数"。
Lambda表达式基础
Lambda的引入
我们把只定义有一个抽象方法的接口,称为"单一抽象方法(SAM:Single Abstract Method)"的接口,在开发中可以有三种方式实现它:
传统方法
1. 顶层类
java
interface MyInterface {
void func();
}
class MyClass implements MyInterface {
@Override
public void func() {
System.out.println("MyClass's func()");
}
}
上述代码定义了一个MyInterface接口(它只定义了一个抽象方法,所以是SAM接口),接着,写了一个MyClass类实现这个接口,在这里,MyClass是一个顶层类。
然后,写了一个静态方法调用接口定义的方法:
java
public static void doWithMyInterface(MyInterface obj) {
obj.func();
}
使用传统编程方式,上面的代码是这样被调用的:
java
//传统方法,定义一个类实现接口,创建这个类的对象,
//再把它传给doWithMyInterface()方法
MyClass obj = new MyClass();
doWithMyInterface(obj);
2. 内部类
除了使用顶层类,也可以使用内部类实现接口:
内部类的适用场景,主要是"仅在本类内部使用",不需要被外界调用,并且代码比较简短。
java
class MyInnerClass implements MyInterface{
@Override
public void func() {
System.out.println("本地内部类,实现接口");
}
}
//实例化本地内部类对象,传给示例方法
doWithMyInterface(new MyInnerClass());
3. 匿名类
如果代码仅在特定方法内部调用,并且代码量也不大,可以直接使用匿名内部类实现接口:
java
doWithMyInterface(new MyInterface() {
@Override
public void func() {
System.out.println("使用匿名内部类,实现接口");
}
});
Lambda
前面的代码是传统的经典的Java面向对象编程代码, 在开发中使用接口,所有代码可以很明确地分为"第一步、第二步、第三步......",很易于理解,也很规范,但每次都需要手工做这么多的事,似乎有点过于麻烦了。
为了便捷性考虑,Java设计者借鉴其他编程语言,把Lambda特性引入到了Java中。
就可以把上述的代码写成这样的形式。
java
//定义一个Lambda表达式,将其引用保存到变量中,
//再把它传给doWithMyInterface()方法
//从而可以节省下新定义一个类的工作任务
MyInterface lambdaObj = () -> {
System.out.println("Explicit Define Lambda object's func()");
};
doWithMyInterface(lambdaObj);
可以更进一步地简化为:
java
//直接把一个Lambda表达式作为doWithMyInterface()方法的参数
//不仅不需要定义一个单独的类,甚至不再需要定义一个变量
doWithMyInterface(() -> {
System.out.println("inline lambda object's func()");
});
** 直接执行Lambda表达式:**
要"执行"一个Lambda表达式所封装的代码,需要使用关联接口所定义的方法:
java
MyInterface lambdaObj2 = () -> {
System.out.println("另一个Lambda表达式");
};
//Lambda表达式,也可以直接执行
lambdaObj2.func();
在函数式编程代码中,函数与其它数据类型一样,也可以进行"赋值"和"传送",具体来说,就是可以定义"函数类型"的变量,函数可以成为另一个函数的"参数",函数也可以返回"另一个函数"。
可以把Lambda表达式理解为一种简洁的可传递匿名函数:它没有名称,但有参数列表,函数主体,返回类型,可能还有一个可以抛出的异常列表。
Lambda表达式格式:(如果方法体只有单行的话,可以把大括号去掉)
(参数列表) -> { 方法体 }
这是一些常见的Lambda使用例子:
再举一个完整的例子:
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class UseComparator {
public static void main(String args[]) {
List<String> strings = new ArrayList<String>();
strings.add("CCC");
strings.add("ddd");
strings.add("EEE");
strings.add("AAA");
strings.add("bbb");
//使用Lambda表达式重写上述代码段
Comparator<String> comparator = (str1, str2) -> {
return str1.compareToIgnoreCase(str2);
};
strings.sort(comparator);
System.out.println("Sort with comparator");
//输出排序结果
for (String str : strings) {
System.out.println(str);
}
}
}
我们可以用Lambda表达式来写comparator方法,并在sort中利用这个方法进行排序。
现在知道了如何编写Lambda表达式,但在哪里使用呢?可以通过函数式接口来使用Lambda表达式。
Java通过"函数式接口"+ Lambda表达式,实现函数式编程。
函数式接口(Functional Interface)
能接收一个Lambda表达式的变量,必须是接口类型,并且这种接口,还必须是一种"函数式接口(functional interface)"。
所谓"函数式接口",就是"只定义有一个抽象方法的接口",在Java 8之前,这种接口被称为"SAM:Single Abstract Method"
接口。
Java 8中,使用"@FunctionalInterface"
标识一个"函数式接口"。
函数式接口 是 Java 8 引入的一个概念,指 只有一个抽象方法 的接口。
它是 Lambda 表达式的基础,Lambda 表达式可以直接替代函数式接口的实现。
1. 函数式接口的定义
函数式接口在语法上与普通接口无异,但必须确保只有一个抽象方法。
示例:
java
@FunctionalInterface
public interface MyFunction {
int apply(int x, int y); // 唯一的抽象方法
}
注意:
- Java 8 提供了
@FunctionalInterface
注解,用于显式声明一个接口为函数式接口。 - 如果有多个抽象方法,编译器会报错。
即使不加 @FunctionalInterface
,接口只有一个抽象方法时,依然可以作为函数式接口使用。
2. 函数式接口与Lambda表达式的关系
Lambda表达式的本质 是一种简化语法,用来表示函数式接口的实例。
当一个 Lambda 表达式被传递时,JVM 自动将其映射为对应函数式接口的实现。
关联逻辑:
- 函数式接口提供了唯一的抽象方法。
- Lambda 表达式的代码实现对应函数式接口的唯一抽象方法。
示例:
java
@FunctionalInterface
interface MyFunction {
int apply(int x, int y);
}
// 使用Lambda表达式实现MyFunction接口
MyFunction add = (x, y) -> x + y;
System.out.println(add.apply(3, 5)); // 输出: 8
解释:
MyFunction
是一个函数式接口。Lambda表达式 (x, y) -> x + y
实现了apply
方法。
3. JDK内置的函数式接口
Java 提供了许多内置的函数式接口,位于 java.util.function
包中。这些接口可以直接配合 Lambda 表达式使用。
常见接口:
-
Predicate<T>
:接收一个参数,返回布尔值。javaPredicate<Integer> isEven = n -> n % 2 == 0; System.out.println(isEven.test(4)); // 输出: true
-
Consumer<T>
:接收一个参数,无返回值。javaConsumer<String> print = s -> System.out.println(s); print.accept("Hello, Lambda!"); // 输出: Hello, Lambda!
-
Function<T, R>
:接收一个参数,返回一个结果。javaFunction<Integer, String> intToString = n -> "Number: " + n; System.out.println(intToString.apply(10)); // 输出: Number: 10
-
Supplier<T>
:无参数,返回一个结果。javaSupplier<Double> random = () -> Math.random(); System.out.println(random.get()); // 输出: 随机数
-
BiFunction<T, U, R>
:接收两个参数,返回一个结果。javaBiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; System.out.println(multiply.apply(3, 4)); // 输出: 12
4. Lambda表达式使用函数式接口的典型例子
-
线程启动:使用 Runnable 接口
java// 传统写法 new Thread(new Runnable() { @Override public void run() { System.out.println("Thread running..."); } }).start(); // 使用Lambda new Thread(() -> System.out.println("Thread running...")).start();
-
集合操作:Comparator接口
javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie"); // 使用Lambda表达式简化排序 names.sort((a, b) -> a.compareTo(b)); System.out.println(names);
-
事件处理:ActionListener接口
javaJButton button = new JButton("Click Me"); button.addActionListener(e -> System.out.println("Button clicked!"));
方法引用(Method Reference)详解
方法引用 是 Java 8 引入的一种简化 Lambda 表达式的语法,允许开发者通过直接引用已有方法来实现函数式接口的抽象方法,从而使代码更简洁、更可读。
Lambda表达式可以进一步简化为方法引用,直接使用现有方法实现接口的抽象方法。
每个方法引用都存在一个等效的 lambda 表达式
1. 方法引用的基本概念
- 方法引用是 Lambda 表达式的简化形式。
- 它使用双冒号
::
操作符来引用方法。 - 适用于 Lambda 表达式仅调用一个已有方法的场景。
方法引用的语法结构:
java
ClassName::methodName
例如:
java
// Lambda 表达式
Function<String, Integer> lambda = s -> Integer.parseInt(s);
// 方法引用
Function<String, Integer> methodRef = Integer::parseInt;
在这两个例子中,Integer::parseInt
是 s -> Integer.parseInt(s)
的简化形式。
2. 方法引用的四种形式
2.1 引用静态方法
适用于 Lambda 表达式调用某个类的静态方法。
语法:
java
ClassName::staticMethod
示例:
java
// Lambda 表达式
Function<String, Integer> lambda = s -> Integer.parseInt(s);
// 方法引用
Function<String, Integer> methodRef = Integer::parseInt;
System.out.println(methodRef.apply("123")); // 输出: 123
分析:
- Lambda 表达式
s -> Integer.parseInt(s)
调用的是Integer
类的静态方法parseInt
。 - 通过
Integer::parseInt
简化了代码。
2.2 引用特定对象的实例方法
适用于 Lambda 表达式调用特定对象的实例方法。
语法:
java
instance::instanceMethod
示例1:
java
// 特定对象
String str = "Hello";
// Lambda 表达式
Supplier<Integer> lambda = () -> str.length();
// 方法引用
Supplier<Integer> methodRef = str::length;
System.out.println(methodRef.get()); // 输出: 5
分析:
- Lambda 表达式
() -> str.length()
调用的是str
对象的length
方法。 - 通过
str::length
简化代码。
示例2:
java
// Lambda 表达式
BiFunction<String, String, Boolean> lambda = (s1, s2) -> s1.equals(s2);
// 方法引用
BiFunction<String, String, Boolean> methodRef = String::equals;
System.out.println(methodRef.apply("abc", "abc")); // 输出: true
System.out.println(methodRef.apply("abc", "def")); // 输出: false
分析:
- Lambda 表达式
(s1, s2) -> s1.equals(s2)
调用的是String
类实例的equals
方法。 - 通过
String::equals
进一步简化。
2.3 引用构造方法
适用于 Lambda 表达式用于创建对象的场景。
语法:
java
ClassName::new
示例:
java
// Lambda 表达式
Supplier<List<String>> lambda = () -> new ArrayList<>();
// 方法引用
Supplier<List<String>> methodRef = ArrayList::new;
List<String> list = methodRef.get();
System.out.println(list); // 输出: []
带参数的构造方法引用:
java
// Lambda 表达式
Function<String, Integer> lambda = s -> new Integer(s);
// 方法引用
Function<String, Integer> methodRef = Integer::new;
System.out.println(methodRef.apply("123")); // 输出: 123
3. 方法引用与函数式接口的关系
方法引用本质上是对函数式接口的实现。
- 函数式接口 要求实现唯一的抽象方法。
- 方法引用 的方法与该抽象方法的签名必须一致。
示例:
java
@FunctionalInterface
interface MyFunction {
void print(String s);
}
// 使用 Lambda 表达式
MyFunction lambda = s -> System.out.println(s);
// 使用方法引用
MyFunction methodRef = System.out::println;
lambda.print("Hello, Lambda!"); // 输出: Hello, Lambda!
methodRef.print("Hello, Method Reference!"); // 输出: Hello, Method Reference!
4. 方法引用的实际应用
- 集合排序
java
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");
// 使用 Lambda 表达式
names.sort((a, b) -> a.compareTo(b));
// 使用方法引用
names.sort(String::compareTo);
System.out.println(names); // 输出: [Alice, Bob, Charlie]
- 流处理(Stream API)
java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 使用 Lambda 表达式
names.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));
// 使用方法引用
names.stream().map(String::toUpperCase).forEach(System.out::println);
常用的函数式接口
1. Consumer接口(级联多个Consumer)
Consumer的基本用法
Consumer<T>
接口用于接收一个输入参数并对其执行某些操作,但不会返回结果。常见用法是打印日志、更新状态、操作集合元素等。
-
核心方法:
javavoid accept(T t);
- 接收一个参数
t
,对其执行操作。 - 没有返回值。
- 接收一个参数
级联Consumer - andThen方法
Consumer
提供了默认方法 andThen
,可以将多个 Consumer
串联起来,按照顺序依次执行每个 Consumer
的逻辑。
-
方法定义:
javadefault Consumer<T> andThen(Consumer<? super T> after)
- 参数
after
: 另一个Consumer
,会在当前Consumer
执行完之后被调用。 - 返回一个新的
Consumer
,依次执行两个Consumer
的操作。
- 参数
-
示例代码:
javapublic static void main(String[] args) { Consumer<String> consumer1 = str -> System.out.println("Consumer 1: " + str); Consumer<String> consumer2 = str -> System.out.println("Consumer 2: " + str.toUpperCase()); // 将两个Consumer级联 Consumer<String> combinedConsumer = consumer1.andThen(consumer2); combinedConsumer.accept("hello"); }
-
输出:
Consumer 1: hello Consumer 2: HELLO
-
更复杂的级联示例
可以串联多个 Consumer
来实现更复杂的操作。
java
public static void main(String[] args) {
Consumer<String> consumer1 = str -> System.out.println("Step 1: " + str.trim());
Consumer<String> consumer2 = str -> System.out.println("Step 2: " + str.toLowerCase());
Consumer<String> consumer3 = str -> System.out.println("Step 3: " + str.toUpperCase());
// 级联三个Consumer
Consumer<String> combinedConsumer = consumer1.andThen(consumer2).andThen(consumer3);
combinedConsumer.accept(" HeLLo WoRLd ");
}
-
输出:
Step 1: HeLLo WoRLd Step 2: hello world Step 3: HELLO WORLD
2. Predicate接口与对象查找
Predicate的基本概念
Predicate<T>
接口主要用于定义一个"判断规则",用于表示一个"布尔值"函数。它接收一个输入参数,并返回一个布尔值,用于条件判断。
-
核心方法:
javaboolean test(T t);
- 参数
t
: 输入值。 - 返回值:布尔值,用于判断输入是否满足条件。
- 参数
-
默认方法:
and
: 将多个Predicate
串联,所有条件均为true
时返回true
。or
: 至少一个条件为true
时返回true
。negate
: 对当前Predicate
结果取反。isEqual
: 检查对象是否相等。
对象查找中的应用
Predicate
通常用于过滤集合、查找符合条件的对象。
-
示例代码(查找满足条件的对象):
javapublic static void main(String[] args) { List<String> names = List.of("Alice", "Bob", "Charlie", "David"); // 定义Predicate,查找长度大于3的名字 Predicate<String> lengthPredicate = name -> name.length() > 3; // 查找符合条件的名字 names.stream() .filter(lengthPredicate) .forEach(System.out::println); }
-
输出:
Alice Charlie David
-
-
级联使用多个Predicate:
-
java
Predicate<String> startsWithA = name -> name.startsWith("A"); Predicate<String> lengthPredicate = name -> name.length() > 3; // 组合条件:以A开头且长度大于3 Predicate<String> combinedPredicate = startsWithA.and(lengthPredicate); names.stream() .filter(combinedPredicate) .forEach(System.out::println);
-
输出:
Alice
-
3. Function接口及相关接口
Function的基本概念
Function<T, R>
是 Java 8 中的函数式接口,此接口定义了一个apply方法,它接收一个T类型的对象,返回一个R类型的对象:
-
核心方法:
javaR apply(T t);
- 参数
t
: 输入值。 - 返回值:类型为
R
的结果。
- 参数
-
默认方法:
andThen
: 先执行当前Function
,再将结果传给另一个Function
。compose
: 先执行参数指定的Function
,再将结果传递给当前Function
。
示例代码
-
基本用法:
javapublic static void main(String[] args) { Function<String, Integer> stringLength = str -> str.length(); System.out.println(stringLength.apply("Hello")); // 输出 5 }
-
使用
andThen
和compose
:javapublic static void main(String[] args) { Function<String, Integer> stringLength = str -> str.length(); Function<Integer, Integer> square = num -> num * num; // andThen: 先计算长度,再平方 System.out.println(stringLength.andThen(square).apply("Hello")); // 输出 25 // compose: 先平方长度,再计算平方 System.out.println(square.compose(stringLength).apply("Hello")); // 输出 25 }
相关接口
-
BiFunction<T, U, R>
作为Function接口的特例,有一个BiFunction<T, U, R>接口,它所定义的apply方法接收两个参数:T和U类型的,然后返回一个R类型的对象。
-
表示一个接收两个参数的函数,返回一个结果。
-
核心方法:
javaR apply(T t, U u);
-
示例:
javaBiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; System.out.println(add.apply(5, 10)); // 输出 15
-
-
UnaryOperator
-
是
Function<T, T>
的子接口,表示输入和输出类型相同的函数。其实就是Function<T,T>的简写。
-
示例:
javaUnaryOperator<Integer> square = x -> x * x; System.out.println(square.apply(5)); // 输出 25
-
-
BinaryOperator
-
是
BiFunction<T, T, T>
的子接口,等价于BiFunction<T,T,T>,表示两个相同类型参数的函数,并返回相同类型的结果。
-
示例:
javaBinaryOperator<Integer> multiply = (a, b) -> a * b; System.out.println(multiply.apply(2, 3)); // 输出 6
-
-
IntToDoubleFunction
-
表示接收一个
int
类型参数并返回一个double
类型结果的函数。 -
示例:
javaIntToDoubleFunction half = x -> x / 2.0; System.out.println(half.applyAsDouble(10)); // 输出 5.0
-