Java8-行为参数化与Lambda
# Java8-行为参数化与Lambda
行为参数化
就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。
# 从多变的筛选条件说起
实现查找功能,几乎是每个开发者必须要学会的。但若是遇到多变的情况,你会如何处理呢?
第一版:对于一堆苹果,要你实现——筛选出红色的苹果。
于是,你写了一个根据颜色进行筛选的方法。(以下是简要代码)
for(Apple apple: AppleLists){
if(“red”.equals(apple.getColor()) ){
result.add(resultList)
}
}
return resultList;
2
3
4
5
6
第二版:不一会,对方又说,他还想实现——筛选出绿色苹果。
怎么办呢,加一个判断吗?可如果再一会,对方又说它还想筛选其他颜色呢?那我们把颜色作为参数,这样就不怕他再变了。
if("chooseColor".equals(apple.getColor()) )
可实际越比我们想得要复杂。
第三版:对方又说:我不仅想筛选颜色,还想筛选大小。
那只能修改代码了呀。增加一个方法——筛选大小。可,如果对方后面还要增加其他筛选条件,比如产地、甜度...还是继续修改代码。甚至说想多个条件组合在一起,同时筛选呢?除了增加代码,有什么更好的解决方法来应对这种变化吗?
# 行为参数化
让我们后退一步来看看更高层次的抽象。其实那些条件,都是苹果的属性(比如它是红色的吗?大小如何?),或者说都是苹果的谓词(predicate)。上述都是通过苹果的属性来判断,那我们就直接通过一个苹果类实现判断。
public interface ApplePredicate{
boolean test (Apple apple);
}
2
3
首先创建一个接口,这样后续我们需要什么条件,就可按需来编写对应实现类。
public class AppleColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals( apple.getColor() );
}
}
2
3
4
5
这样一来,最后的代码就变成了如下:
public List<Apple> filterApples(List<Apple> AppleLists,ApplePredicate applepredicate){
for(Apple apple: AppleLists){
if(applepredicate.test(apple)){ # 筛选红色苹果
result.add(resultList)
}
}
return resultList;
}
2
3
4
5
6
7
8
如此,以后再有变化,我们也只需改变,苹果的谓词对象类ApplePredicate
即可。比起之前直接写在循环中灵活得多,不仅如此还有一点值得点赞的是,我们将“筛选动作”参数化了,或者说行为参数化
。
其实匿名内部类也是一种行为参数化,比如我们通过Runnable
接口创建线程时。
“行为参数化”是指将行为(通常是方法或函数)作为参数传递给另一个方法的过程。这种编程技术使得代码更加灵活和可复用,因为它允许你在运行时动态地改变程序的行为。行为参数化的一个典型例子是使用接口或抽象类来定义行为,然后在运行时传递具体的实现。
它的实现有助于提高代码的灵活性和可维护性,使得程序能够更好地应对变化。在日常开发中,我们也应该尽量实现行为的分离——比如将筛选行为写在一个类中,添加行为写在一个类中,尽量实现行为参数化
。
# Lambda表达式
Lambda表达式是Java 8引入的一个重要特性,它允许你以一种简洁的方式编写匿名函数。Lambda表达式使得代码更加简洁和易读,并且可以有效地提高开发效率。
也就是说,它是一种技巧,一种将行为参数化变得更加简洁的技巧。
List<Apple> redApples = filterApples(apples, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});
2
3
4
5
6
以上是筛选红苹果的关键代码,使用的是匿名内部类的方式编写。下面我们再来看看使用lambda表达式后:
List<Apple> redApples = filterApples(apples, apple -> "red".equals(apple.getColor()));
代码量比匿名内部类少了许多,咋一看可能有点蒙,但多看一会(对比着看),估计你就能发现其中的改动。
# 前置条件
但也不是所有情况都能使用lambda表达式,上述例子是因为filterApples
的第二个参数是一个接口类型,且只有一个方法要实现,因此才可以。或者说接口只有是函数式接口
才可以,函数式接口就是只定义一个抽象方法的接口。
在Java 8中,接口的定义得到了增强,允许接口中包含默认方法(default method)和静态方法(static method)。
默认方法是一种接口方法,它提供了一个默认实现,使得实现该接口的类可以选择性地覆盖此方法,也可以直接使用接口中提供的默认实现。
public interface Shape {
void draw();
// 默认方法
default String toString() {
return "This is a shape.";
}
}
2
3
4
5
6
7
8
那上述这种接口算是函数式接口吗?它是的,只要它只有一个抽象方法,不管有没有默认实现的方法,它都是函数式接口。
# 表达式
lambda表达式的其实很简单,它的基本语法如下:
(parameters)—— > expression
或者
(parameters)—— > { statements ;}
lambda表达式初期可能用起来不顺利,但时间一长你就会发现,是真的香,它不仅减少了代码量,还让代码含义一目了然。
# 一些案例
Java8才开始引入这种表达式,原先的函数式接口不算很多,只有Comparable、Runnable和Callable。但新版本既然引入了这种新写法,那它肯定也设计了对应的新的函数式接口,否则怎么向大家展示它的强大和进行推广呢?下面就展示一些实际的代码例子,感受下它的强大。
接下来介绍几个常用的,Predicate、Consumer和Function
,更多的请自行查阅资料。
# Predicate
它的定义如下:
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
2
3
4
@FunctionalInterface关于该注解,作用跟@Override类似,若该接口不是函数式接口,则会报错。
只有一个test方法,且接受泛型对象,是不是跟我们上面的例子很类似,没错。看它的名字,其实它就是用来筛选元素的。
public static List<Apple> filterApples(List<Apple> apples, Predicate predicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : apples) {
if (predicate.test(apple)) {
result.add(apple);
}
}
return result;
}
// 获取一个 List
Predicate<Apple> redApple = (Apple apple) -> "red".equals(apple.getColor()) ;
List<Apple> redApples = filterApples(apples, redApple);
2
3
4
5
6
7
8
9
10
11
12
13
筛选行为我们用lambda表达式来说明,选择红色的,且代码也很简洁。
# Consumer
java.util.function.Consumer< T >定义了一个名叫accept的抽象方法,它接受泛型T的对象,并且没有返回(void)。常用于消费(处理)一个给定类型的元素。它包含一个抽象方法 void accept(T t)
,该方法用于处理传入的参数。Consumer
接口常用于集合的流操作中,例如 forEach
方法,以及其他需要对集合中的每个元素执行某种操作的场景。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
2
3
4
5
6
7
8
9
比如我们要打印红苹果的重量:
Consumer<Apple> redApple = (Apple apple) -> System.print.out(apple.getColor()) ;
List<Apple> redApples = filterApples(apples, redApple);
// 当然filter中的对应调用也会有所变化 这里不再展示
2
3
# Function
Java 8中,Function< T, R >
接口是一个函数式接口,用于将一个类型的对象转换为另一种类型的对象。它定义了一个抽象方法 apply(T t)
,该方法接受一个类型为 T
的参数,并返回一个类型为 R
的结果。这个接口常用于数据转换、映射等场景。
接口定义如下:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
<V> Function<V, R> compose(Function<? super V, ? extends T> before);
<V> Function<T, V> andThen(Function<? super R, ? extends V> after);
default <V> Function<V, R> identity() {
return t -> t;
}
}
2
3
4
5
6
7
8
9
10
11
12
同样还是苹果的例子,这次我们想得到每个苹果的重量。
Function<Apple,Integer> redApple = (Apple apple) -> apple.getWeight() ;
List<Integer> redApples = filterApples(apples, redApple);
// 当然filter中的对应调用也会有所变化 这里不再展示
2
3
当然,新增的函数式接口还有,这里就不一一展开了。
# 注意
Java API中的函数式接口的方法不允许抛出
受检异常(checked exception),除非它们是运行时异常(runtime exceptions)或者是错误(errors)。
受检异常 vs 非受检异常
- 受检异常(Checked Exceptions):编译器会强制要求捕获或声明这些异常。常见的受检异常包括
IOException
、SQLException
等。 - 非受检异常(Unchecked Exceptions):编译器不会强制要求捕获或声明这些异常。常见的非受检异常包括
NullPointerException
、IllegalArgumentException
等。
import java.util.function.Function;
import java.io.IOException;
// 这是一个错误的示例,Function 接口不允许抛出 IOException
// Function<String, String> readContent = path -> Files.readString(path); // 编译错误
// 正确的做法是处理异常或将异常转换为 RuntimeException
Function<String, String> readContent = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // 转换为运行时异常
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
但你可自定义抛出受检异常的函数式接口。
# 方法引用
关于lambda表达式还有一种更简洁的写法,或者叫做方法引用。
方法引用的基本语法是使用双冒号来表示方法的位置:
// 使用Lambda表达式
messages.forEach(msg -> Util.printMessage(msg));
// 使用静态方法引用
messages.forEach(Util::printMessage);
2
3
4
5
再或者:
(Apple a)-> a.getWeight();
//方法引用
Apple::getWeight;
//再或者
(String str) -> System.out.println(str);
//方法引用
System.out::println;
//再或者
(str,st)->str.substring(i);
//方法引用
String::substring;
2
3
4
5
6
7
8
9
10
11
12
13
注意:方法引用,不需要括号,因为你没有实际调用这个方法。
其中关于方法引用还有很多细节,这里只是抛砖引玉,让大家了解这个语法,其实就是lambda表达式的变种,以后遇到不会奇怪。
# 扩展-复合表达式
Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。
但需要注意的是,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。
# 比较器复合
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 逆序
inventory.sort(comparing(Apple::getWeight).reversed());
// 按重量递减排序
// 重量相同时 再按价格排序
inventory.sort(comparing(Apple::getWeight).reversed()
.thenComparing(Apple::getPrice))
//
2
3
4
5
6
7
8
9
10
# 谓词复合
Predicate<Apple> notRedApple = redApple.negate();
// 产生现有Predicate对象redApple的非
Predicate<Apple> redAndHeavyApple =
redApple.and(a -> a.getWeight() > 150);
// 链接两个谓词来生成另一个Predicate对象
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
// 链接Predicate的方法来构造更复杂Predicate对象”
2
3
4
5
6
7
8
9
10
11
12
# 函数复合
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // 数学上会写作g(f(x))或(g o f)(x)
int result = h.apply(1); // 这将返回4
2
3
4
5
再或者
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); //数学上会写作f(g(x))或(f o g)(x)
int result = h.apply(1); // 这将返回3
2
3
4
5