Java8 新特性 —— 函数式编程

Java8 新特性 —— 函数式编程

本文部分摘录自 On Java 8

概述

通常,传递给方法的数据不同,结果也不同。同样的,如果我们希望方法被调用时的行为不同,该怎么做呢?结论是:只要能将代码传递给方法,那么就可以控制方法的行为。

说得再具体点,过去我们总是创建包含所需行为的对象,然后将对象传递给想要控制的方法,一般使用匿名内部类来实现。假设现在有这么一个需求:有一个员工信息列表,根据年龄过滤出符合条件的员工信息

// 过滤出大于35岁的员工
public List<Employee> filterEmployee(List<Employee> list) {
    List<Employee> emps = new ArrayList<>();
    for(Employee emp : list) {
        if(emp.getAge() > 35) {
            emps.add(emp);
        }
    }
    return emps;
}

// 过滤出大于45岁的员工
public List<Employee> filterEmployee2(List<Employee> list) {
	...   
}

这样写当然能实现需求,但如果需求变了,要过滤 45 岁的,那岂不是又得写一个 filterEmplyee2() 方法?如果还要过滤 50 岁的,60 岁的,那就没完没了了,而且代码的实现逻辑几乎没有区别。于是我们借助策略模式的思想来简化代码。

public interface MyPredicate<> {
    boolean predicate(T t);
}

// 如果有其他过滤需求,只需要实现 MyPredicate 接口即可
public class EmployeeFilter implements MyPredicate<Employee> {
    @Override
    public boolean predicate(Employee employee) {
        return t.getAge() >= 35;
    }
}

// 根据传入的 MyPredicate 对象来实现不同的过滤逻辑
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
    List<Employee> emps = new ArrayList<>();
    for(Employee emp : list) {
        if(mp.predicate(emp)) {
            emps.add(emp);
        }
    }
    return emps;
}

public void test(List<Employee> list) {
    // 创建实现类对象,传入过滤方法
    MyPredicate<Employee> predicate = new EmployeeFilter<>();
    List<Employee> res = filterEmployee(list, predicate);
    // 更简单的方式是使用匿名内部类
    List<Employee> res2 = filterEmployee(list, new MyPredicate<Employee>() {
        @Override
        public boolean predicate(Employee employee) {
            return t.getAge() >= 100;
        }
    });
}

通过观察我们发现,我们需要的只有 predicate() 方法的代码,其他的我们一律不关心。如果 MyPredicate 接口还有其他抽象方法,我们又必须每一个做一次实现,但真正用上的只有 predicate() 方法,不仅显得冗余,而且可读性也很低。为了解决这个问题,Java8 为我们提供了 Lambda 表达式和方法引用两种更加简洁的方式。

Lambda 表达式

Lambda 表达式是一个匿名函数,可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样传递)。虽然在 JVM 规范规定一切都是类,但其幕后执行的各种操作使得 Lambda 看起来像是函数。因此我们可以大胆假设 Lambda 表达式产生的就是一个函数,而不是类。

Lambda 的基本语法有是:(参数) -> {方法体}

  • 其中 -> 可以视为将参数传递给方法体使用的一个中间桥梁
  • 左侧为表达式的参数列表。使用括号包裹参数,当只有一个参数时,可以不需要括号,如果没有参数,则必须使用括号表示空参数列表。参数列表的数据类型可以省略不写,因为 Java 的编译器可以帮助我们根据上下文推断数据类型
  • 右侧为表达式中所需执行的功能。方法体如果只有单行,可以省略花括号,此时执行结果自动转化为 Lambda 表达式的放回值,使用 return 关键字是非法的;如果方法体有多行,则必须放在花括号中,这时如果有返回值,就需要使用 return

Lambda 表达式能产生比匿名内部类更易读的代码,因此我们应该尽可能使用 Lambda 表达式。回到之前的例子,我们可以用 Lambda 表达式来替换匿名内部类。

public interface MyPredicate<> {
    boolean predicate(T t);
}


// 根据传入的 MyPredicate 对象来实现不同的过滤逻辑
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
    List<Employee> emps = new ArrayList<>();
    for(Employee emp : list) {
        if(mp.predicate(emp)) {
            emps.add(emp);
        }
    }
    return emps;
}

public void test(List<Employee> list) {
    // 使用 Lambda 表达式
    List<Employee> res = filterEmployee(list, e -> e.getAge() <= 5000);
}

Lambad 表达式通常比匿名内部类产生更易读的代码,因此我们应该尽可能使用 Lambda 表达式。

如果我们想编写递归的 Lambda 表达式,必须注意:

方法引用

Lambda 表达式可以帮助我们实现仅调用方法,而不做其他多余动作(如创建对象)的目的,而有些情况下,已经存在能满足需求的方法,我们可以不必再编写 Lambda 表达式,而通过方法引用直接使用该方法。可以理解为方法引用是 Lambda 表达式的另一种表现形式。

方法引用的组成:类名或对象名,后面跟 ::,然后跟方法名称,如果要分类的话,可以用如下组合:

  • 引用静态方法 className::staticMethod

  • 引用某个对象的实例方法 instance::instanceMethod

  • 引用某个类型的任意对象的实例方法 className::instanceMethod

  • 引用构造方法 className::new

interface Callable {
	void call(String s);
}

class Describe {
    void show(String msg) {
        System.out.println(msg);
    }
}

public class MethodReferences {
    
    static void hello(String name) {
        System.out.println("Hello, " + name);
    }

    public static void main(String[] args) {
		// 对象名:: 方法名称
        Describe d = new Describe();
        Callable c = d::show;
        c.call("call()");
		// 类名::方法名
        c = MethodReferences::hello;
        c.call("Bob");
    }
}

要注意的是,方法引用的签名(参数类型和返回类型)必须符合 Callable 的 call() 的签名。上述代码我没有演示 className::instanceMethodclassName::new 的情况,这两个有点特殊,待会再介绍。

Runnable 接口

通过之前的学习,我们发现 Runnable 接口也符合特殊的单方法接口格式:它的 run() 方法不带参数,也没有返回值,因此我们可以使用 Lambda 表达式和方法引用作为 Runnable

class Go {
    static void go() {
        System.out.println("thread go");
    }
}

public class RunnableMethodReference {
    public static void main(String[] args) {
		// 匿名内部类方式
        new Thread(new Runnable() {
            public void run() {
                System.out.println("Anonymous");
            }
        }).start();
		// Lambda 表达式方式
        new Thread(
            () -> System.out.println("lambda")
        ).start();
		// 方法引用方式
        new Thread(Go::go).start();
    }
}

未绑定的方法引用

未绑定的方法引用是指没有关联对象的普通(非静态方法),使用未绑定的引用,我们必须先提供对象

class X {
    String f() { return "X::f()"; }
}

interface MakeString {
    String make();
}

interface TransformX {
    String transform(X x);
}

public class UnboundMethodReference {
    public static void main(String[] args) {
        // MakeString ms = X::f; // 无法通过编译
        TransformX sp = X::f;
        X x = new X();
        System.out.println(sp.transform(x));
        System.out.println(x.f());	// 同等效果
    }
}

我们看到在 MakeString ms = X::f; 中,即使 make()f() 有相同的方法签名,却无法通过编译。这是因为实际上还有另一个隐藏参数 this 没有考虑,你不能在没有 X 对象的情况下调用 f(),因为它尚未绑定到对象。

要解决这个问题,我们需要一个 X 对象,所以我们的接口需要一个额外的参数如 TransformX,用来接收一个 X 对象。同样的,在调用 transform(X x) 方法时,也必须传递一个 X 对象作为参数。如果你的方法有多个参数,就以第一个参数接受 this 的模式来处理。

构造函数引用

还可以捕获构造函数的引用,然后通过引用去调用该构造函数。

class Dog {
    String name;
    int age;
    Dog() { name = "stray"; }
    Dog(String nm) { name = nm; }
    Dog(String nm, int yrs) { name = nm; age = yrs; }
}

interface MakeNoArgs {
    Dog make();
}

interface Make1Arg {
    Dog make(String name);
}

interface Make2Args {
    Dog make(String name, int age);
}

public class CtorReference {
    public static void main(String[] args) {
        MakeNoArgs mna = Dog::new;
        Make1Arg m1a = Dog::new;
        Make2Args m2a = Dog::new;

        Dog dn = mna.make();
        Dog d1 = m1a.make("Comet");
        Dog d2 = m2a.make("Ralph", 4);
    }
}

函数式接口

接口中只有一个抽象方法的接口,称为函数式接口,可以使用注解 @FunctionalInterface 检查一个接口是否符合函数式接口的规范。

Lambda 表达式和方法引用都要赋值给对应的函数式接口引用。Java8 提供了一组 java.util.function 包,它包含一组完整的函数式接口,一般情况下,我们可以直接使用,而不需要自己再定义。

Java 为我们提供了内置的四大核心函数式接口:

  • 消费型接口

    有参数,无返回值类型的接口

    @FunctionalInterface
    public interface Consumer<T> {
    
        void accept(T t);
    }
    
  • 供给型接口

    只有产出,没有输入,就是只有返回值,没有入参

    @FunctionalInterface
    public interface Supplier<T> {
    
        T get();
    }
    
  • 函数型接口

    既有入参,也有返回值,T 表示函数的参数类型,R 表示函数的返回类型

    @FunctionalInterface
    public interface Function<T, R> {
    
        R apply(T t);
    }
    
  • 断言型接口

    输入一个参数,返回一个 boolean 类型的返回值

    @FunctionalInterface
    public interface Predicate<T> {
    
        boolean test(T t);
    }
    

除了上述的四个核心内置接口,Java 还为我们提供其他常用的函数式接口,如 BiFunction<T, U, R> 也是函数型接口,但可以接收两个参数,我们可以根据需要去查阅 API 文档。

函数组合

意为多个组合成新的函数,一些 java.util.function 接口包含支持函数组合的方法

  • andThen(Function<? super R,? extends V> after)

    返回一个组合函数,前一个函数的结果作为后一个函数的入参

  • compose(Function<? super V,? extends T> before)

    返回一个组合函数,后一个函数首先处理原始入参,再将结果交给前一个函数处理

  • and(Predicate<? super T> other)

    返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑与

  • or(Predicate<? super T> other)

    返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑或

  • negate()

    返回表示此谓词的逻辑否定的谓词

闭包

考虑一个函数,x 是 其中的一个入参,i 则是其中的一个局部变量,返回一个 Lambda 表达式

public class Closure {
    IntSupplier makeFun(int x) {
        int i = 0;
        return () -> x + i;
    }
}

我们知道,函数的入参的局部变量只在方法的生命周期内有效,正常情况下,当 makeFun(int x) 方法执行完后,x 和 i 就会消失,但它返回的 Lambda 表达式却依然保存着 x 和 i 的值。相当于 makeFun(int x) 返回的 IntSupplier 关住了 x 和 i

另外要注意的一点是:被 Lambda 表达式引用的局部变量必须是 final 或是等同 final 效果的。所谓等同 final,意思是即使你没有明确声明变量是 final,但因变量值没被改变过而实际上有了 final 同等的效果。Java8 默认 Lambda 中的局部变量具有等同 final 效果。

柯里化

柯里化意为:将一个多参数的函数,转换为一系列单参数函数

public class CurryingAndPartials {
    // 未柯里化
    static String uncurried(String a, String b) {
        return a + b;
    }
    public static void main(String[] args) {
        // 柯里化的函数
        // a -> b -> a + b,意思是传入参数 a,返回 b -> a + b 的函数
        // 由于 Lambda 表达式的闭包特性,b -> a + b 中的 a 是有保存值的
        Function<String, Function<String, String>> sum = a -> b -> a + b;

        System.out.println(uncurried("Hi ", "Ho"));
        
        Function<String, String> hi = sum.apply("Hi ");
        System.out.println(hi.apply("Ho"));

        Function<String, String> sumHi = sum.apply("Hup ");
        System.out.println(sumHi.apply("Ho"));
        System.out.println(sumHi.apply("Hey"));
    }
}

柯里化的目的是通过提供一个参数来创建一个新函数,根据上述的例子,我们可以通过添加级别来柯里化具有更多参数的函数

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/2601.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 向量的内积和叉积_点乘和叉乘的区别

    向量的内积和叉积_点乘和叉乘的区别向量是由n个实数组成的一个n行1列(n*1)或一个1行n列(1*n)的有序数组;向量的点乘,也叫向量的内积、数量积,对两个向量执行点乘运算,就是对这两个向量对应位一一相乘之后求和的操作,点乘的结果是一个标量。点乘公式对于向量a和向量b:

  • cv::imread读不出图片的解决办法「建议收藏」

    cv::imread读不出图片的解决办法「建议收藏」imread()函数无法读取图片的原因测试程序:intmain(){ //读入一张图片 Matimg=imread("longmao.jpg"); if(img.empty()) { cout&lt;&lt;"Can’treadimage"&lt;&lt;endl; return-1; } //创建一个名为“龙猫”窗口 namedWindow("longmao"); …

    2022年10月10日
  • 事务ACID理解

    事务ACID理解事务管理(ACID)谈到事务一般都是以下四点原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。一致性(Consistency)事务前后数据的完整性必须保持一致。隔离性(Isolation)事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔…

    2022年10月12日
  • 基于豆瓣影评数据的文本分析系统【数据爬取+数据清洗+数据库存储+LDA主题挖掘+词云可视化】「建议收藏」

    基于豆瓣影评数据的文本分析系统【数据爬取+数据清洗+数据库存储+LDA主题挖掘+词云可视化】「建议收藏」本分析中很多的工作都是基于评论数据来进行的,比如:滴滴出行的评价数据、租房的评价数据、电影的评论数据等等,从这些语料数据中能够挖掘出来客户群体对于某种事物或者事情的看法,较为常见的工作有:舆情分析、热点挖掘和情感分析。如果想要了解关于文本分类或者是情感分析相关的工作内容,可以阅读我的《数据建模实战》专栏文章,下面是链接信息:…

  • Python中break和continue区别「建议收藏」

    Python中break和continue区别「建议收藏」break跳出整个循环,而continue跳出本次循环continue语句用来告诉python跳过当前循环,进行下一个循环break语句用来终止循环语句,即循环条件没有False条件或者序列还没被完全递归完,也会停止执行循环语句。break和continue语句用在while和for循环中#continue,跳过循环a=’2123456’forletterina:…

  • BBSSDK 产品分析

    BBSSDK 产品分析

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号