函数式接口与方法引用

Posted 潘明杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式接口与方法引用相关的知识,希望对你有一定的参考价值。

函数式接口与lambda表达式是密不可分的,在Java中,lambda表达式是对象,但是其必须依附于函数式接口,也就是说lambda表达式是函数式接口的实现对象。当然,函数式接口的实现对象还可以通过方法引用来实现。下面我们将介绍函数式接口的含义与其对应的实现。

函数式接口特点

1.    无论是否为函数式接口,都是用interface关键字修饰的。

2.    若在接口的声明加上注解@FunctionInterface,则代表此接口是一个函数式接口。

3.    函数式接口只能有一个抽象方法(那些方法签名与Object类中一致的不算抽象方法,方法签名一致指返回值、方法名、参数列表一致),可以有具体的方法(必须default关键字修饰,称为默认方法)和静态方法。

4.    当一个接口只有一个抽象方法时且没有使用注解@FunctionInterface,编译器也会将其当做一个函数式接口。

通过以上4点,可以明显的看出函数式接口与传统的接口的巨大区别。我们通过以下几个例子来加深对函数式接口的理解。

例一:声明函数式接口

@FunctionalInterface
public interface MyInterface {
//抽象方法
   
void test();

   
//存在于Object类中
   
String toString();

   
//默认方法
   
default void echo(String username) {
System.out.println("hello : " + username);
   
}

//静态方法
   
static void print(String password) {
System.out.println("your password :" + password);
   
}
}

此接口定义了一个抽象方法test、一个默认方法echo、一个静态方法print。注意:这边的toString()方法不算函数式接口的抽象方法,因为其实现类默认会继承Object类,Object类是所有类的父类,所以也会继承自Object类的toString方法。因此,MyInterface接口的实现类就无需再手动实现toString方法,从而JDK编译器就不会将其当做是函数式接口的抽象方法。

例二:函数式接口的实现类

class MyInterfaceIns implements MyInterface {
@Override
   
public void echo(String username) {
//调用父接口的默认方法
       
MyInterface.super.echo(username);
       
System.out.println("override method");
   
}
@Override
   
public void test() {
System.out.println("test method");
   
}
}

public class Client {
public static void main(String[] args) {
//调用接口的静态方法
       
MyInterface.print("hello");

       
//调用函数式接口的默认方法
       
MyInterface myInterface = () -> System.out.println("hello world");
       
myInterface.echo("zhangsan");

       
//调用实现类重写的方法
       
MyInterface myInterfaceIns = new MyInterfaceIns();
       
myInterfaceIns.echo("lisi");
   
}
}

这个例子主要是演示如何调用函数式接口的抽象方法、默认方法和静态方法。

  1. 静态方法调用:接口名.静态方法名(与传统的类名.静态方法名一致)

  2. 默认方法调用:只能通过指向实现对象的引用来调用,有2种方式

    1. 传统方式:声明一个实现类,实现类需重写抽象方法,然后实例化一个对象,通过对象引用即可调用。

    2. lambda表达式:本质上就是函数式接口的实现对象(可理解为匿名对象),通过接口的引用调用   

  3. 抽象方法调用:需实现抽象方法,一共有3种方式,除了通过传统的实现类方式和lambda表达式,还有方法引用(后面介绍,本质上也是lambda表达式,只是使用场景有限制)

    例三:多实现

@FunctionalInterface
interface Interface1 {
void echo();

   default void
testMethod() {
System.out.println("Interface1");
   
}

default void testMethod2() {
System.out.println("Interface1 testMethod2");
   
}
}

@FunctionalInterface
interface Interface2 {
void echo();

   default void
testMethod() {
System.out.println("Interface2");
   
}
}
public class InterfaceImpl implements Interface1, Interface2 {

@Override
   
public void testMethod() {
Interface2.super.testMethod();//调用父接口的默认方法
   
}

@Override
   
public void echo() {
System.out.println("InterfaceImpl");
   
}
}

当一个类实现了多个函数式接口时,且多个函数式接口有相同的默认方法(方法签名相同),这时编译器就强制要求实现类要重写其父接口中相同的默认方法。注意:调用父接口的默认方法用:父接口名.super.默认方法名

lambda表达式

lambda表达式本质上就是函数式接口的一个实现对象,其形式为(参数列表) -> {逻辑代码},参数列表对应函数式接口中抽象方法的参数列表,{逻辑代码}对应抽象方法的具体实现,这也是为什么函数式接口只能有一个抽象方法的原因,因为lambda表达式并没指定函数名之类的,只能看到其中的参数和实现,但是指向lambda表达式的引用一样可以调用该接口的默认方法以及静态方法。例如:

MyInterface myInterface = () -> System.out.println("hello world");

myInterface接口的引用直接指向lambda表达式(可理解为抽象方法的具体实现),根据多态,我们可知lambda表达式就是该接口的一个实现对象。

方法引用

方法引用其实就是lambda表达式的一个语法糖,相当于一个函数指针,指向一个函数对象,在某些场景下可以替换掉lambda表达式,使代码更加简洁,主要有以下4种形式:

  1. 类名::静态方法名

  2. 实例名::实例方法名

  3. 类名::实例方法名

  4. 类名::new

我们通过下面的例子来了解以上4种形式的使用场景

public class Person {
private String name;
   private int age;
   public Person(){}
public Person(String name, int age) {
this.age = age;
       this.name = name;
   }
//省略get和set方法
}
public class PersonComparator {
//比较规则,按升序排序,静态方法
   public static int compareByAge(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
   }
//比较规则,按升序排序,实例方法
   public int compareByAgeImpl(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
   }
}

以上这个例子,主要是对list列表中的person根据年龄进行升序排序,注意这一行:

personList.sort((item1, item2) -> item1.getAge() - item2.getAge());

sort的参数是一个函数式接口Comparator,里面的抽象方法接收待排序的两个参数。此时,我们用自己实现的lambda表达式作为参数。当此时存在这样的一个方法(参数列表与返回值都和lambda表达式的一致,换句话说,其恰好实现了该接口的抽象方法,当然理论上是不太准确的,只是方便理解),就可将其替代掉lambda表达式,如

personList.sort(PersonComparator::compareByAge);

这边就是使用类名::静态方法名。

若当此方法不是是实例方法而不是静态方法时,就可通过实例名::实例方法名来调用,如

PersonComparator personComparator = new PersonComparator();
personList.sort(personComparator::compareByAgeImpl);

从最终打印的方式也可看出此方式的使用场景,out是PrintStream的实例名,println是实例方法,该方法接收一个参数。

personList.forEach(System.out::println);

另外一种形式类名::实例方法名稍微有点不同,修改Student类为

public class Person {
private String name;
   private int age;
   public Person(){}
public Person(String name, int age) {
this.age = age;
       this.name = name;
   }
public int compareByAge(Person person) {
       return this.getAge() - person.getAge();
   }
   //省略get和set方法

此时,我们对list排序就可用此形式

personList.sort(Person::compareByAge);

这边实例方法只接收一个参数,可以理解为lambda表达式中第一个参数为调用此方法的对象,lambda表达式之后的参数作为此实例方法的参数。

最后,类名::new形式的方法引用也称为构造方法引用,主要生成对象使用,这时我们添加一个person的工厂

@FunctionalInterface
public interface PersonFactory {
Person getPerson(String name, int age);
}

并在测试类中添加此方法

客户端调用

Person person = MethodReferenceStatic.
getPerson("hello", 11, Person::new);

再次强调一下,方法引用也是函数式接口的实现方式,所以这个例子中我们可以将Person::new传递给函数式接口PersonFactory,该接口中的抽象方法接收两个参数,而编译器会自动识别Person::new为

public Person(String name, int age) {
this.age = age;
   this.name = name;
}

到此,方法引用的4种形式的用法我们就介绍结束了。

总结

函数式接口、lambda表达式及方法引用可以说是jdk8中最基础的内容,而jdk8为我们提供了大量的函数式接口和相关极其强大的辅助类,当掌握这些基础性原理之后再去学习官方提供的API会更加的得心应手。

以上是关于函数式接口与方法引用的主要内容,如果未能解决你的问题,请参考以下文章

函数式接口与方法引用

Java8学习笔记-函数式接口与方法引用

Java8新特性:函数式接口,方法与构造器引用

12函数式接口方法引用

函数式接口方法引用

Java8函数式编程