Java 函数式编程基础


函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称 Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为 Lambda 计算。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

虽然Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this参数的函数。从 Java 8 开始,Java 平台支持函数式编程。

将一个实例传入一个方法中,实例的成员方法就相当于作为参数传入其他函数的函数,这样就可以实现函数式编程。

// 定义接口
interface IPrint {
    void print();
}

// 定义接口的实现子类
class Print implements IPrint {
    public void print() {
        // 覆写IPrint中的print()方法。
        System.out.println("实现子类的传入信息");
    }
}

public class Lambda {

    public static void main(String[] args) {
        // 传入子类的实例化对象
        fun(new Print());
    }

    public static void fun(IPrint msg) { // 接收接口对象
        msg.print();
    }

}

只需要编写IPrint接口的不同实现子类,设置其中的成员方法内容,就可以在不动方法fun(IPrint msg)本身的前提下改变传入到方法的“函数”,进而改变方法的执行结果,实现传入普通参数难以达到的灵活性。比如:

class Print implements IPrint {
    public void print() {
        // 覆写IPrint中的print()方法。
        System.out.println("打印普通信息");
        System.err.println("打印错误信息");
    }
}

同时打印普通信息和错误信息,甚至做出更复杂的效果,这是只接收字符串等参数传入的方法无法灵活做到的。以往想要改变一个方法的逻辑,必须要改写方法本身,现在只需要改变传入的“函数”即可。

匿名内部类

如果Print类只使用一次,还要将其定义为一个具体的类,就很麻烦。这时就可以用到匿名内部类了。

内部类是指在类内部定义的类结构,利用内部类可以方便地实现私有属性的互相访问,一般的内部类需要明确地使用class进行定义,而匿名内部类较为特殊,是没有名字的类。必须在抽象类或接口的基础上才能定义,可以实例化抽象类或接口,适合用来创建一次性子类

语法:new 抽象类名或接口名() {},大括号内部的内容即是该实例化子类的内容。

interface IPrint {
    void print();
}

public class Lambda {

    public static void main(String[] args) {
        //匿名内部类
        fun(new IPrint() { // 实例化接口IPrint
            //覆写IPrint中的print()方法。
            @Override
            public void print() {
                System.out.println("匿名内部类");
            }
        });
    }

    public static void fun(IPrint msg) {
        msg.print();
    }

}

这样就相当于创建了一个一次性的子类,简化了实例化抽象类接口的操作。可能由于这个类是在一个类内部声明的,所以被叫作内部类。但实际上它跟真正的内部类关系不大,相反具有更多子类的性质,所以叫匿名子类或者匿名类更合适。其匿名内部类的名称可能只是一个惯例,其来历这里就不深究了,读者只需理解它的性质,不要被它的名称搞晕即可。

函数式接口

我们把只定义了一个抽象方法的接口称之为函数式接口(Functional Interface),用注解@FunctionalInterface标记。例如,Callable接口:

@FunctionalInterface
public interface Callable {
    V call() throws Exception;
}

再来看Comparator接口:

@FunctionalInterface
public interface Comparator {

    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator thenComparing(Comparator<? super T> other) {
        ...
    }
    ...
}

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是普通方法(default方法)或静态方法(static方法)。另外注意到boolean equals(Object obj)Object定义的方法,不算在接口方法内。因此,Comparator接口也是一个函数式接口。

由于只有一个抽象方法,函数式接口可以用Lambda表达式来实例化。

Lambda 表达式

应用在单一抽象方法(Single Abstract Method, SAM)接口环境下的一种简化定义形式,用于解决匿名内部类的定义复杂问题。

Lambda 表达式能简化匿名内部类的书写,但并不能取代所有匿名内部类,只能取代函数式接口的简写。

姑且将其看作一种匿名函数,没有声明的方法,即没有访问修饰符、返回值声明、和名字。它允许你将函数作为参数传递进方法中。使代码更加简洁紧凑。

语法:(参数) -> 方法体

@FunctionalInterface
interface IPrint {
    // 单一抽象方法接口
    void print();
}
@FunctionalInterface
interface ICalc {
    //单一抽象方法接口
    int calc(int x, int y);
}

public class Lambda {

    public static void main(String[] args) {
        // 匿名内部类写法
        fun(new IPrint () {
            @Override
            public void print() {
                System.out.println("匿名内部类");
            }
        });

        // Lambda表达式
        fun1(() -> System.out.println("Lambda"));
        //方法体为多行代码时打大括号
        String info = "信息";
        fun1(() -> {
            System.out.println("题目");
            System.out.println(info);
        });

        //覆写有参数有返回值的方法
        fun2((s1, s2) -> {
            return s1 + s2;
        });
        //可以省略return,方法体就是一个表达式
        fun2((s1, s2) -> s1 + s2);
    }

    public static void fun1(IPrint msg) {
        msg.print();
    }
    public static void fun2(ICalc msg) {
        System.out.println(msg.calc(2, 10));
    }
}

假如fun1(IPrint msg)fun2(ICalc msg)方法不能或不应该修改,就能体现出函数式编程的灵活性了。可以通过编写 Lambda 表达式来随意设置fun2(ICalc msg)方法中对2和10两个数字的计算方式。想改成乘法,乘方,甚至从2加到10等等……

方法引用

在Java8中,我们可以直接通过方法引用(Method Reference)这一特性来简写 Lambda 表达式。方法引用用来直接访问类或者实例的已经存在的方法或者构造方法。 方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。

计算时,方法引用会创建函数式接口的一个实例。 当 Lambda 表达式中只是执行一个方法调用时,不用 Lambda 表达式而是直接用方法引用可读性更高。 方法引用是一种更简洁易懂的 Lambda 表达式

两个方法的方法参数的数量和类型一致,返回类型相同,我们就说这两者方法签名一致。不看方法名称,也不看类的继承关系。如果某个方法的签名和接口恰好一致,就可以直接传入方法引用。

方法引用的操作符是双冒号::

  • Lambda表达式的写法:

    Arrays.sort(array, (s1, s2) -> {
        return s1.compareTo(s2);
    }
    
  • 引用静态方法:类名称::静态方法名称

    public class Main {
    
        public static void main(String[] args) {
            String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
            Arrays.sort(array, Main::cmp);// 静态方法
            System.out.println(String.join(", ", array));
        }
    
        static int cmp(String s1, String s2) {
            return s1.compareTo(s2);
        }
    
    }
    
  • 引用特定类型的方法:特定类::普通方法

    注意,如果引用了String.compareTo()这种实例方法,需要一个本类的对象A来调用,比如:A.compareTo(B)。观察String.compareTo()方法:

    public final class String {
        public int compareTo(String o) {
            ...
        }
    }
    

    这个方法的签名只有一个参数,但实际上可以作为参数传入。能与int Comparator.compare(String, String)匹配。

    Arrays.sort(array, String::compareTo);
    

    为什么?因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用时,第一个隐含参数总是传入this,相当于静态方法:

    public static int compareTo(this, String o);
    
  • 引用某个对象的方法:实例化对象::普通方法

    也是引用实例方法,故同上。

  • 引用构造方法:类名称::new

    构造方法虽然没有return语句,但它会隐式地返回this实例

总结

无论匿名内部类、Lambda 表达式还是方法引用,都是将一个签名相对应的函数作为参数传入方法中,来灵活地参与方法内的运算,进而简化代码编写。