tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

    • Java中的集合
    • Java泛型
    • Java中的日志
    • Java8-行为参数化与Lambda
      • 从多变的筛选条件说起
      • 行为参数化
      • Lambda表达式
        • 前置条件
        • 表达式
        • 一些案例
        • Predicate
        • Consumer
        • Function
        • 注意
        • 方法引用
        • 扩展-复合表达式
        • 比较器复合
        • 谓词复合
        • 函数复合
    • Java8-函数式数据处理
    • Java8-代码优化与设计模式
    • Java8-新的日期和时间API
    • Java中的引用
  • 并发合集

  • JVM合集

  • 实战与细节

  • 代码之丑与提升

  • 《Java》学习笔记
  • 高级进阶
EffectTang
2024-09-23
目录

Java8-行为参数化与Lambda

# Java8-行为参数化与Lambda

行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。

# 从多变的筛选条件说起

实现查找功能,几乎是每个开发者必须要学会的。但若是遇到多变的情况,你会如何处理呢?

第一版:对于一堆苹果,要你实现——筛选出红色的苹果。

于是,你写了一个根据颜色进行筛选的方法。(以下是简要代码)

for(Apple apple: AppleLists){
	if(“red”.equals(apple.getColor()) ){
    result.add(resultList)
  }
}
return resultList;
1
2
3
4
5
6

第二版:不一会,对方又说,他还想实现——筛选出绿色苹果。

怎么办呢,加一个判断吗?可如果再一会,对方又说它还想筛选其他颜色呢?那我们把颜色作为参数,这样就不怕他再变了。

if("chooseColor".equals(apple.getColor()) )
1

可实际越比我们想得要复杂。

第三版:对方又说:我不仅想筛选颜色,还想筛选大小。

那只能修改代码了呀。增加一个方法——筛选大小。可,如果对方后面还要增加其他筛选条件,比如产地、甜度...还是继续修改代码。甚至说想多个条件组合在一起,同时筛选呢?除了增加代码,有什么更好的解决方法来应对这种变化吗?

# 行为参数化

让我们后退一步来看看更高层次的抽象。其实那些条件,都是苹果的属性(比如它是红色的吗?大小如何?),或者说都是苹果的谓词(predicate)。上述都是通过苹果的属性来判断,那我们就直接通过一个苹果类实现判断。

public interface ApplePredicate{
    boolean test (Apple apple);
}
1
2
3

首先创建一个接口,这样后续我们需要什么条件,就可按需来编写对应实现类。

public class AppleColorPredicate implements ApplePredicate{
  public boolean test(Apple apple){
    return "red".equals( apple.getColor() );
  }
}
1
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;
} 
1
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());
    }
});
1
2
3
4
5
6

以上是筛选红苹果的关键代码,使用的是匿名内部类的方式编写。下面我们再来看看使用lambda表达式后:

List<Apple> redApples = filterApples(apples, apple -> "red".equals(apple.getColor()));
1

代码量比匿名内部类少了许多,咋一看可能有点蒙,但多看一会(对比着看),估计你就能发现其中的改动。

# 前置条件

但也不是所有情况都能使用lambda表达式,上述例子是因为filterApples的第二个参数是一个接口类型,且只有一个方法要实现,因此才可以。或者说接口只有是函数式接口才可以,函数式接口就是只定义一个抽象方法的接口。

在Java 8中,接口的定义得到了增强,允许接口中包含默认方法(default method)和静态方法(static method)。

默认方法是一种接口方法,它提供了一个默认实现,使得实现该接口的类可以选择性地覆盖此方法,也可以直接使用接口中提供的默认实现。

public interface Shape {
    void draw();

    // 默认方法
    default String toString() {
        return "This is a shape.";
    }
}
1
2
3
4
5
6
7
8

那上述这种接口算是函数式接口吗?它是的,只要它只有一个抽象方法,不管有没有默认实现的方法,它都是函数式接口。

# 表达式

lambda表达式的其实很简单,它的基本语法如下:

(parameters)—— > expression
1

或者

(parameters)—— > { statements ;}
1

lambda表达式初期可能用起来不顺利,但时间一长你就会发现,是真的香,它不仅减少了代码量,还让代码含义一目了然。

# 一些案例

Java8才开始引入这种表达式,原先的函数式接口不算很多,只有Comparable、Runnable和Callable。但新版本既然引入了这种新写法,那它肯定也设计了对应的新的函数式接口,否则怎么向大家展示它的强大和进行推广呢?下面就展示一些实际的代码例子,感受下它的强大。

接下来介绍几个常用的,Predicate、Consumer和Function,更多的请自行查阅资料。

# Predicate

它的定义如下:

@FunctionalInterface
public interface Predicate<T>{
    boolean test(T t);
}
1
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);
1
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); };
    }
}
1
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中的对应调用也会有所变化 这里不再展示
1
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;
    }
}
1
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中的对应调用也会有所变化 这里不再展示
1
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); // 转换为运行时异常
    }
};
1
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);
1
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;
1
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))
//
1
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对象”

1
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

1
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

1
2
3
4
5
上次更新: 2025/04/23, 16:23:16
Java中的日志
Java8-函数式数据处理

← Java中的日志 Java8-函数式数据处理→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式