函数式接口与方法引用

前言

之前我们说到了Java8中的一个新的特性,就是lambda表达式的使用,不过当时只是简单的了解了一下,没有与Java8中其他的新的特性做一个全面的了解。这里我们要到的Java中的函数式接口和方法引用就是Java8种新增加的东西,而且这两个新东西和lambda表达的式的联系还非常的密切。

什么是函数式接口

之前说了和其他的编程语言不同的是,Java是一门非常面向对象的语言,对象在Java的世界中就是最重要的玩意。其他语言中的lambda表达式都是一种函数,而Java中的lambda表达式不是函数,之前我们说过了是一种依赖于接口的一个玩意。作用和匿名内部类非常的相似。(其实lambda这个玩意就是Java抄袭其他的语言的,其他的语言基本上很早就有了,而且大家都非常喜欢用,那Java可不要趁一波“热度“嘛。不过热度也不能瞎趁,其他的语言之中,函数的地位其实才是老大,比如说python就是一门函数式的原因,Java是一门对象为中心的语言,那么lambda表达式代表的就是一个对象。)上面扯多了,不瞎扯这个玩意了。先回到正题。

不过lambda表达式依赖的接口是有一定的条件的。他必须是只一个函数需要我们来实现。比如说新建一个线程的Runnable接口中只有一个函数run()需要我们实现。这样的接口就可以使用lambda。使用的方法如下。

Runnable a = () -> System.out.println("Hello World");

其实这样的接口就是所谓的函数式接口,也就是所有的只有一个抽象函数需要实现的接口就是函数式接口。Java8种函数式接口的上面有一个注解(就像重写父类的方法的注解@override一样)叫做@FucntionalInterface。比如我们要写一个函数式接口

@FunctionalInterface
interface A{
    void func();
}

当然这个@FunctionalInterface不加也是可以的,就像你函数重写不加@override也是对的,毕竟这个玩意只是一个注解。不过还是要明白加上去的含义是什么的。

那么我们现在就可以说清楚Java当中的lambda表达式是一个什么东西了。lambda表达式就是函数式接口是实例。lambda表达式的存在必须要依赖于函数式接口,不过虽然lambda表达式是一个对象,但是他的作用相当于是一个函数。就像c++当中的仿函数一样,虽然是一个对象,但是是可以调用的,作用也是相当于一个对象。其实C++中的lambda表达式虽然说是一个函数,不过实际上也是一个对象——std::function,所有的可调用的变量都可以是这个std::function,其本质上也是一个类,也就是仿函数Function Class

函数式接口的作用

那么说了那么多没用的废话,这个函数式接口的作用到底是什么呢?仅仅是给了一个定义??

我们都知道OOP是面向对象编程的意思。其实今年来又兴起了一种的新的编程的方式——OOF面向函数编程。函数式接口的存在使得Java也可以进行OOF的编程方式。我们可以将函数式接口作为参数,然后使用lambda表达式进行参数的传递。

@FunctionalInterface
interface A{
    void func(String str);
}

public void test1(String str, A a) {
    a.func(str);
}

看上面的代码,我们定义了一个函数式接口,然后将这个接口作为函数test1()的参数传入。然后再我们使用test1()函数的时候就可以指定不同的函数来做不同的事情。比如说

@Test
public void test2() {
    String str = "Hello World";
    test1(str, new A() {
        @Override
        public void func(String str) {
            System.out.println(str.toUpperCase());
        }
    });
}

使用这种方式,我们使用了一个匿名内部类传递了我们需要是使用的函数,使得给定的字符串变成全是大写的。不过之前我们说的是可以使用lambda表达式的方式进行参数式的传递,下面就使用lambda表达式来试一下。

test1(str, s -> System.out.println(s.toLowerCase()));

使用lambda表达式的话只需要一行的代码而且非常的简洁,这里lambda表达式的优点就充分的体现出来了。

不过说实话,这个写法还是非常的扯的,因为我们还定义了一个接口,要是每一个函数都要去定义一个相应的接口,那岂不是要烦死了。不过我们发现了一个问题,这些接口似乎都是通用的。比如说上面的这个接口中需要实现的方法是接受一个参数String,没有返回值的。那么其余的需要这种接口的函数都可以使用这个接口。那么我们需要的接口就是有限的,按理来说标准库是会提供这些接口给我们使用而不是让我们自己去定义的。

四大核心函数式接口

果然,Java替我们提供了丰富的函数式接口,都在java.util.function这个包下。不过有四个接口是非常重要的,被称为四大核心函数借口,下面就来一一介绍一下。

函数式接口 参数类型 返回值类型
Consumer T void
Supplier none void
Function<T, R> T R
Predicate T boolean

下面来一一介绍一下上面的四个接口。

  • Consumer<T> 消费型接口
    • 名副其实,所谓的消费型接口就是接口里面的函数是需要提供一个参数,但是没有任何的返回值。就像我们消费了这个东西一样。
    • 接口里面的抽象方法的格式为void accept(T t)
    • 上面我们使用的那个接口其实就是一个Consumer<String>的一个接口。
  • Supplier<T>接口型接口
    • 和上面的消费型接口相反,这个接口里面的函数不需要任何的参数,但是会返回一个东西。这个就是典型的雷锋行为啊。
    • 接口里面的抽象方法的格式为T get()
  • Function<T>函数型接口
    • 这个接口应该是最常用的一个接口。他有一个参数,还有一个返回值,参数和返回值的类型不需要是相同的。
    • 接口里面的格式为R apply(T t)
  • Predicate<T>断定型接口
    • 这个接口是给定一个参数,然后返回真假。也是比较常用的一个接口。
    • 接口里面的函数的格式为boolean test(T t)

需要注意的是,上面我给出的每个接口里面的函数的格式并不是瞎给的。至少调用的话不是瞎给的,我们使用这个接口,都需要调用接口中的函数,函数名就是上面的那样,是固定的。

比如我们上面举的例子就可以使用第一个Consumer<String>这样的一个接口。

public void test11(String str, Consumer<String> consumer) {
    consumer.accept(str);
}

@Test
public void test3() {
    String str = "Hello Wrold";
    test11(str, s -> System.out.println(s.toLowerCase()));
}

遮掩我们就可以是用Java已经给我们定义好的接口了,可谓是非常的方便了。

其他的接口也是可以来简单的写个小例子来示范一下的。

public void testSpplier(String str, Supplier<Integer> supplier) {
    System.out.println(str + supplier.get());
}

@Test
public void test3() {
    testSpplier("Hello", () -> new Random().nextInt(100));
}

public void testFunction(String str, Function<String, String> function) {
    System.out.println(str + function.apply(str));
}

@Test
public void test4() {
    String str = "Hello";
    testFunction(str, s -> s.toUpperCase());
}

public void testPredicate(int a, Predicate<Integer> predicate) {
    if (predicate.test(a)) {
        System.out.println("" + a + " is a great num!");
    } else {
        System.out.println("" + a + "is a bad num!");
    }
}

@Test
public void test5() {
    int a = 10;
    testPredicate(a, s -> (s & 1) == 0);
}

上面就是这些接口的使用的方法的一些示例。不过,说实话这个例子都是这种蛮沙雕的例子,完全是没有什么必要的。不过之二十为了演示函数式接口的使用的形式而已。

方法引用

上面的函数式接口基本上就讲完了,其实也没有什么需要注意的地方。用起来也非常的容易理解。下面要说的Java8种的一个想你的特性——方法引用。这个东西也是和lambda表达式相关的,也是为了简化我们的代码。

方法引用的意思就是我们可以在需要lambda的地方,使用我们已经定义过的相同的形式的方法。这么说可能是非常难以理解,下面直接上代码进行演示好了。

public class MethodReference {
    @Test
    public void test1() {
        Consumer<String> consumer = s -> System.out.println(s);
        consumer.accept("Hello World");
    }
}

我写了如上的代码,但是非常智能的idea给我的System.out.println()标记了打了一个提示,说lambda can be replaced with method reference。那我就听他的话,是用万能的alt + enter快捷键,idea将我的代码修改了

public class MethodReference {
    @Test
    public void test1() {
        Consumer<String> consumer = System.out::println;
        consumer.accept("Hello World");
    }
}

这就是所谓方法引用。System.out.println()这个方法是System.out的一个方法。这个方法已经定义过了。当我们在lambda中使用的时候(值使用这个函数,其他不用),就可以使用方法引用的方式来替代lambda表达式。

@Test
public void test2() {
    Comparator<Integer> comparator = (a, b) -> Integer.compare(a, b);
    System.out.println(comparator.compare(3, 4));
}

上面的Comparator也是Java中的一个函数式接口,他的实现函数的格式为int compare(T a, T b)

上面的Integer.compare方法也是我们定义过了的方法。所以说上面的lambda表达式也可以修改成为方法引用。

@Test
public void test2() {
    Comparator<Integer> comparator = Integer::compare;
    System.out.println(comparator.compare(3, 4));
}

上面就是使用方法引用的形式。

不过应该发现了一个问题,上面的System.out::println::左面是一个PrintWriter对象,但是Integer::compare的左面是一个类。学过C++的应该都知道::的左面应该是类的。虽然Java可能不是的,不过我们也要搞清楚,左面到底应该是什么,是对象还是类?

不过还是比较容易分析的。println是一个实例方法。而compare是一个对象的方法。众所周知类是不可以调用实例方法的,那么就是说实例方法的左面需要是类,而静态方法的左面类和对象都是可以的吗?就像函数调用那样子一样吗?

那我们来做一些测试。我们将System.out::println换成了PrintStream::println之后发现报错了。不过也容易理解,没有System.out,我们都不知道往哪儿去输出。

我们再来测试一下使用左面是对象,右面是静态方法的情况。

@Test
public void test2() {
    Integer i = new Integer(3);
    Comparator<Integer> comparator = i::compare;
    System.out.println(comparator.compare(3, 4));
}

又失败了。不过这是报错是出乎我们的意料的。对象是可以调用静态方法的啊,那为什么这种失败了呢?这是不是就是意味着方法引用只有如下的两种形式?

对象::实例方法类::静态方法

因为其余的两种形式我们都测试过了。不过经过查阅资料,其上还有一种方式是可行的。类::实例方法也是可行的。

哈?这个也是可行的?不是上面我们已经测试过了吗?不是不可行的嘛?

其实实例方法的引用是有点儿特殊的。上面的对象::实例方法类::静态方法都是比较简单。只要满足参数相同,返回值相同就是可以调用的。但是使用类::实例方法不一样。他的参数必须要是调用对象的参数,方法的其他参数。举个例子来说吧。

@Test
public void test3() {
    BiPredicate<String, String> biPredicate = (x, y) -> x.equals(y);
    System.out.println(biPredicate.test("hello", "Hello"));

    biPredicate = String::equals;
    System.out.println(biPredicate.test("Hello", "Hello"));
}

比如上上面的代码都是正确的。上面的那个BiPredicate也是java.util.function下面的一个接口。他的实现函数的形式为boolean test(T t, U u)

上面的那个lambda比较容易理解。不过下面的方法引用是不是有点奇怪了?是用的是类::实例方法的形式。关键是equals()方法在类中的声明如下

public boolean equals(String s);

参数是不匹配的。这就是我们需要注意的地方。当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二 个参数是需要引用方法的参数(或无参数)时,可以使用类::实例方法的形式。

也就是PrintStream::println这个函数可以实现的函数式接口为Biconsumer<PrintStream, String>,第一个参数是println函数的调用对象(比如说System.out)。第二个参数是String,就是输出的内容。(也可以是其他可以输出的类型)。

构造器引用和数组引用

除了方法引用之外还有构造器引用和数组引用,其实都是一样的不需要再说那么多了也可以轻松的理解。

下面来举一个简单的小例子就行了。

@Test
public void test4() {
    Supplier<A> supplier = () -> new A();
    supplier = A::new;

    Function<Integer, A> function = i -> new A(i);
    function = A::new;
}

class A {
    A() {

    }

    A(int a) {

    }
}

其实构造器也是一个方法,如果使用方法引用的话,按道理来说应该写成A::A,不过人家构造器是特殊的方法,当然要有特殊的形式,使用的是xx::new的形式。

还有一种引用是数组引用。这中引用就是比较特殊的了,不过和构造器引用却是比较类似的。比如下面的代码

@Test
public void test5() {
    Function<Integer, String[]> function = s -> new String[s];

    function = String[]::new;

    System.out.println(function.apply(6));
}

这就是数组引用,和构造器引用是有点儿类似的,简单的了解一下就好了。

总结

上面介绍的主要是函数式接口和方法引用。这两个东西都是跟随lambda在Java8中被引用的。虽然他们不是Java8种比较重要的特性,不过还是需要做一些了解的。Java8种最重要的特性是lambda和StreamAPI。其中lambda我们已经简单的认识过了,下面还需要了解一下StreamAPI的使用。


一枚小菜鸡