3. Java8新特性-通过行为参数化传递代码

行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。

行为参数化可以让代码更好的适应不断变化的要求,减轻未来的工作量。

过去我们也一直实践行为参数化传递代码,如执行集合排序,java API提供了Collections.sort方法,传递两个参数,第一个是待排序集合,第二个是Comparator。这是一个典型的策略模式,即针对一组算法,将每个算法封装到具有共同接口的独立类中,从而使得它们可以互相替换,却不影响主体逻辑。

过去我们行为参数化很多情况下采用传递匿名内部类实例的形式,如:

/**
 * 演示行为参数化-匿名内部类形式
 */
public class InnerClassSample {

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>(List.of(new Animal("goldfish", 2),
                new Animal("dog", 5),
                new Animal("cat", 10),
                new Animal("bird", 3),
                new Animal("turtle", 50)));

        Collections.sort(animals, new Comparator<Animal>() {
            @Override
            public int compare(Animal o1, Animal o2) {
                return o1.getAge() - o2.getAge();
            }
        });

        animals.forEach(System.out::println);
    }
}
// result:
// Animal(name=goldfish, age=2)
// Animal(name=bird, age=3)
// Animal(name=dog, age=5)
// Animal(name=cat, age=10)
// Animal(name=turtle, age=50)

示例是给指定的集合做排序,排序逻辑通过匿名内部类实例的形式实现,最后打印排序后的实例列表。

那一直使用匿名内部类实例的方式实现行为参数化行不行?
用是没问题,但是非常繁琐,也不优雅。一旦繁琐程序员们就倾向于不用。还好Java8设计者引入了Lambda表达式-一种更简洁的传递代码的方式来解决这个问题。

使用Lambda来改进一下上面的代码:

/**
 * 演示使用lambda实现行为参数化
 */
public class LambdaSample {

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>(List.of(new Animal("goldfish", 2),
                new Animal("dog", 5),
                new Animal("cat", 10),
                new Animal("bird", 3),
                new Animal("turtle", 50)));

        animals.sort(((o1, o2) -> o1.getAge() - o2.getAge()));
        animals.forEach(System.out::println);
    }
}
// result:
// Animal(name=goldfish, age=2)
// Animal(name=bird, age=3)
// Animal(name=dog, age=5)
// Animal(name=cat, age=10)
// Animal(name=turtle, age=50)

可以看出确实要简洁很多,因为看上去像是在描述问题本身。

Lambda

本节开始正式介绍Lambda。可以把Lambda表达式理解为简洁的表示可以传递匿名函数的一种方式:它没有名称,有参数列表,函数主体,返回类型。

匿名 - 即跟普通方法不同,它没名字
函数 - 它不属于某个特定的类,但和方法一样,有参数列表,函数主体,返回类型,还可能有可以抛出的异常列表。
传递 - 可以作为参数传递给方法或存储在变量中。
简洁 - 无需像匿名内部类一样写很多模板代码。

lambda这个词从哪里来?
来源于学术界开发出来的一套用来描述计算的λ演算法。

理论上lambda能做的事,java8之前也能做(采用匿名内部类,实现类)。但lambda更简洁,灵活,不再需要写一堆冗余的代码。

lambda表达式的三个部分

参数列表: 描述函数的参数
箭头:箭头用于把参数列表和函数体分割
lambda主体:即函数体

lambda基本语法:(parameters) -> expression。 为什么采用此种格式?因为在C#和Scala等语言中类似功能很受欢迎

常见Lambda使用示例:

() -> {}
() -> "hello world"
() -> {return "hello world";}
(String s) -> "hello world : " + s
s -> "hello world : " + s
在哪里以及如何应用Lambda

一句话:在函数式接口上使用Lambda表达式

函数式接口

上面演示lambda实现行为参数化时使用的是集合的sort方法,说明sort方法的参数类型是函数式接口,我们看一下它的定义:

@FunctionalInterface
public interface Comparator<T> {
    
    int compare(T o1, T o2);

什么是函数式接口?
一句话概括就是:函数式接口就是只定义一个抽象方法的接口。通常为了便于识别,通常使用@FunctionalInterface注解(非强制)

可以推测出我们以前常用的接口,但只带一个方法的还有:

public interface Runnable {
    void run();
}

public interface ActionListener extends EventListener{
    void actionPerformed(ActionEvent e);
}

public interface Callable<V>{
    V call();
}

注意:现在接口可以定义默认方法,只要接口只定义了一个抽象方法,哪怕有很多默认方法,也算函数式接口。

函数式接口的用处?

只要方法的一个参数式是函数式接口,就可以应用lambda表达式进行传递,整个表达式可作为函数式接口的实例。

函数描述符

函数式接口的抽象方法的签名基本就是lambda表达式的签名,我们将这种抽象方法叫做函数描述符。
当一个lambda表达式可以合法的传递给函数式接口变量时,意味着lambda表达式的格式符合函数描述符的定义。

@FunctionalInterface

这个注解之前已经提到过,被这个注解标注的接口意味着是函数式接口,如果不满足编译器会提示错误。

再次说明:@FunctionalInterface是可选的,但强烈推荐自定义的函数式接口都加上,就跟使用@Override注解一样。

Execution Around(环绕执行模式)

某些操作有固有的步骤和模式,如资源处理的常见模式:打开 -> 处理 -> 关闭。打开和关闭总是相似,并且会围绕着核心处理的逻辑,这就是Execution Around。

以处理文本文件为例,实现一个Execution Around

package win.elegentjs.java8.lambda.txtprocess;

import java.io.BufferedReader;
import java.io.IOException;

/**
 * 自定义函数式接口,用于处理文本,可应用lambda表达
 */
@FunctionalInterface
public interface BufferedReaderProcessor {

    String process(BufferedReader br) throws IOException;
}

package win.elegentjs.java8.lambda.txtprocess;

import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

@Slf4j
public class TxtProcessSample {

    public static void main(String[] args) {
        String result;
        result = processFile(br -> br.readLine());
        log.info("result: {}", result);

        result = processFile(br -> br.readLine() + " " + br.readLine());
        log.info("result: {}", result);

        result = processFile(br -> br.readLine() + " " + br.readLine() + " " + br.readLine());
        log.info("result: {}", result);
    }

    /**
     * 通用处理文本文件方法
     */
    public static String processFile(BufferedReaderProcessor processor) {
        try (BufferedReader br = new BufferedReader(new FileReader("/Users/liupeijun/git/learn/1.txt"))) {
            return processor.process(br);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

// 打印:
2021-08-18 17:40:32.440 [main] INFO  w.e.java8.lambda.txtprocess.TxtProcessSample-result: 12313
2021-08-18 17:40:32.460 [main] INFO  w.e.java8.lambda.txtprocess.TxtProcessSample-result: 12313 13123123
2021-08-18 17:40:32.468 [main] INFO  w.e.java8.lambda.txtprocess.TxtProcessSample-result: 12313 13123123 123123123123
Java8自带的函数式接口

函数式接口这么有用,JDK的设计师肯定会新内置很多有用的函数式接口,本节来一起看一下。
在JDK8之前已经有一些接口误打误撞正好符合函数式接口的定义,常见的如下:

Comparator
Runnable
Callable

在JDK8中,设计师们为我们提供了新的常用的函数式接口,在未来的开发中会大规模使用,如下:

Predicate

java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受范型T作为入参,并返回一个boolean。

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

	...

Predicate是一个谓词,非常常用,如用来过滤满足条件的数据集合, 示例如下:

public class PredicateSample {

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>(List.of(new Animal("goldfish", 2),
                new Animal("dog", 5),
                new Animal("cat", 10),
                new Animal("bird", 3),
                new Animal("turtle", 50)));

        animals = animals.stream().filter(e -> e.getAge() >= 10).collect(Collectors.toList());

        System.out.println(animals);
    }
}

// result:
// [Animal(name=cat, age=10), Animal(name=turtle, age=50)]

Consumer

java.util.function.Consumer。 这个比Predicate好理解一些,消费者的意思,即取到当前元素T的实例,要做一些操作(如打印某个字段值),但无返回值,函数描述符如下:

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

它常常和foreach搭配使用,迭代每个元素并做一些事情,示例如下:

public class ConsumerSample {

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>(List.of(new Animal("goldfish", 2),
                new Animal("dog", 5),
                new Animal("cat", 10),
                new Animal("bird", 3),
                new Animal("turtle", 50)));
        
        animals.forEach(e -> System.out.println(e));
    }
}

// result:
Animal(name=goldfish, age=2)
Animal(name=dog, age=5)
Animal(name=cat, age=10)
Animal(name=bird, age=3)
Animal(name=turtle, age=50)
Function

java.util.function.Function<T, R>接口定义了一个叫做apply的方法,它接受一个范型T对象,并返回一个R对象。用于对象结果映射。该接口非常常用。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

示例:


public class FunctionSample {

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>(List.of(new Animal("goldfish", 2),
                new Animal("dog", 5),
                new Animal("cat", 10),
                new Animal("bird", 3),
                new Animal("turtle", 50)));

        List<String> animalNames = animals.stream().map(e -> e.getName()).collect(Collectors.toList());

        System.out.println(animalNames);
    }
}
// result
// [goldfish, dog, cat, bird, turtle]

上述示例从对象中取属性最后合成一个集合。

Supplier

java.util.function.Supplier定义了一个get方法,该方法不接受任何参数,只返回T实例,如下:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

示例:
```java

/**
 * 定一个了从缓存中取数据的方法get,lambda是缓存未命中写缓存的逻辑,
 * 此时不需要参数,只要结果,所以用Supplier很合适
 */
public class SupplierSample {

    public static void main(String[] args) {
       Object o =  get("cache_key", () -> new Object(), 3600);
        System.out.println(o);
       
    }


    /**
     * 根据指定key从redis缓存中获取数据(需要指定缓存时间,单位:秒)
     * @param key redis key
     * @param supplier 执行方法体
     * @param seconds ttl时间,单位:秒
     * @param <T> 返回结果数据类型
     */
    public static <T> T get(String key, Supplier<T> supplier, Integer seconds) {
        AssertUtil.notEmpty(key, "缓存key不能为空!");

        if (RedisUtil.isExist(key)) {
            return RedisUtil.get(key);
        }

        T result = supplier.get();

        RedisUtil.put(key, result, seconds);

        return result;
    }
}
类型检查,类型推断,类型限制

当我们定义一个方法,如果参数包含函数式接口,那实际调用时可传入匹配的lambda表达式,那此时的lambda表达式可看作是个变量,是函数式接口的实例,甚至可以用一个类型接收它,如:

Comparator<Animal> c = (o1, o2) -> o1.age > o2.age;
Runnable r = () -> {};
Function<String, Integer> f = (k, v) -> k.length();

然而lambda表达式本身并不会跟具体某个函数式接口绑定,不会固定属于哪个接口类型。

类型

lambda表达式的类型是从当前使用它的上下文推断而来。一个典型的类型检查过程如下:
在这里插入图片描述

同样的Lambda,不同的函数式接口

只要lambda表达式可以跟目标函数描述符匹配,就可以赋值。如:

Callable<Integer> c = () -> 40;
Supplier<Integer> s = () -> 40;

// ?
boolean Predicate<String> p = s -> list.add(s); 
Consumer<String> b = s -> list.add(s);
类型推断

Java编译器会从上下文(目标类型)推断出用什么函数式接 口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通 过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。

示例如下:

// 无类型推断的情况
Comparator<Apple> c =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 自动类型推断的情况
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
使用局部变量

迄今为止介绍的lambda表达式中的用到的参数都是表达式主体的参数,lambda也允许使用自由变量(外层作用域提供),跟匿名内部类类似,如:

int port = 8848;
Runnable r = () -> System.out.println(port);

但对变量有限制。
注意:
如果是变量是所在类的实例变量和静态变量无限制
如果是当前方法的局部变量,则要求变量是显式final或事实上的final。

以下的写法是错误的:

int port = 8848;
Runnable r = () -> System.out.println(port);
port = 3306;
// wrong

为什么需要对局部变量有限制?
本质原因lambda表达式实际是函数式接口的实例,实例方法(函数主体区)中引用外部宿主的局部变量实际是外部变量的副本(通过值传递)。而lambda表达式并不是一定立即执行,所以限制引用的局部变量必须是final的,或者是事实上final。(如果局部变量没有定义为final,事实上jvm在编译为字节码时也会自动加上final)

其次,这一限制也是不鼓励大家使用引用外部变量的模式,这种模式会阻碍并行处理。

关于为什么局部变量必须final(或事实上)而静态变量和成员变量无此限制,这里进一步解释原因

lambda中访问宿主局部变量和成员变量的示例:

class Student {
    private String className = "一班";

    public void play() {
        String name = "张三";

        IntConsumer consumer = e -> {
            System.out.println(name); // compile error!
            System.out.println(className);
        };
        
        name = "李四";
        className = "二班";

        Arrays.stream(new int[] {1, 2, 3, 4}).forEach(consumer);
    }

}

以上示例在lambda中分别访问方法局部变量和类成员变量,结果访问非final的方法局部变量name会报编译错误,而成员变量正常。

原因:
lambda的函数体是函数描述符的实现,lambda本质上是函数式接口的实例,即对象,跟JDK8之前的匿名内部类的实例是一致的。当lambda实例初始化时本身就能引用宿主类的实例,即任何时刻都能访问宿主对象的属性,这也是为什么在lambda函数体中访问宿主实例的成员属性不会报错,即使在lambda后又更改了实例属性,因为lambda体运行时还是能通过宿主引用访问到变化后的实例属性。

而宿主的局部变量是无法通过实例对象引用的,只能是lambda实例化时通过构造函数传入,即复制一个副本传给lambda实例,所以当局部变量是final(或事实上final)则相安无事,但如果发生了变化(即上述示例)则编译器会识别并报错。

方法引用

让你可以重用现有的方法定义,并像lambda一样传递,一些情况下比使用lambda更简洁,易读。如:

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

// 可用方法引用改写
inventory.sort(comparing(Apple::getWeight));

方法引用可看作lambda的一种快速写法。如上面的实例,实际等价于:

inventory.sort(comparing(e -> e.getWeight()));
一些常见的lambda及其等效方法引用的示例

在这里插入图片描述

构造方法引用
() -> new Apple();

// 可等价于
Apple:new

注意:方法引用不需要强记,现在的IDE非常智能,如果能做方法引用会给出提示。

复合lambda表达式的有用方法

如常见的Predicate,Comparator和Function都设计出允许复合的方法,能组成成强大的编程语义。

以下做一些简单的示例

比较器复合

最简单的是可以根据实体的属性生成Comparator,即按某个属性正序排:

Comparator<Apple> c = Comparator.comparing(Apple:getWeight);

逆序

Comparator<Apple> c = Comparator.comparing(Apple:getWeight).reversed();

比较器链

Comparator<Apple> c = Comparator.comparing(Apple:getWeight).reversed().thenComparing(Apple::getArea));
// 即先根据重量逆序,再根据产地排序
谓词复合

谓词接口(Predicate)包括三个方法:negate, and和or,让我们可以重用已有的Predicate来创建更复杂灵活的谓词,如:

// 取反,意思是获取所有不是红色的苹果
Predicate<Apple> redApple = e -> e.getColor().equals("red");
Predicate<Apple> p = redApple.negate();

// 意思是:要么是大于150克红苹果,要么是绿苹果
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(e -> e.getWeight() > 150).or(e -> "green".equals(e.getColor()));

// 注意:and和or混用太多时容易混乱,不可滥用
函数复合

可以把Function接口所代表的lambda表达式复合起来,Function接口提供了andThen和compose两个默认方法,它们都会返回Function实例。

示例:

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);

// 返回结果是4,andThen的语义是先执行本函数,再将结果作为参数执行第二个函数,
// 数学上会写作g(f(x))

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);

// 返回结果是3,compose的语义是组合,先执行目标函数,再将结果作为参数执行本函数,
// 数学上会写作f(g(x)) 
小结

本节比较长,系统学习了Lambda的用法,能理解以下概念:

  1. lambda表达式可以理解为匿名函数
  2. 函数式接口
  3. 只有在接收函数式接口的地方才能应用lambda表达式,意味着lambda表达式本质上是函数式接口的实例。
  4. Java8自带了常用的函数式接口:Predicate, Function, Consumer, Supplier等
  5. 理解ExecutionAround
  6. 方法引用
  7. Comparator, Predicate和Function提供了一些常用的复合方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值