函数式接口与方法引用
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");
}
}
这个例子主要是演示如何调用函数式接口的抽象方法、默认方法和静态方法。
静态方法调用:接口名.静态方法名(与传统的类名.静态方法名一致)
默认方法调用:只能通过指向实现对象的引用来调用,有2种方式
传统方式:声明一个实现类,实现类需重写抽象方法,然后实例化一个对象,通过对象引用即可调用。
lambda表达式:本质上就是函数式接口的实现对象(可理解为匿名对象),通过接口的引用调用
抽象方法调用:需实现抽象方法,一共有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种形式:
类名::静态方法名
实例名::实例方法名
类名::实例方法名
类名::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会更加的得心应手。
以上是关于函数式接口与方法引用的主要内容,如果未能解决你的问题,请参考以下文章