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