【Java】从匿名内部类到函数式接口
文章目录
- 前言
- 一、匿名内部类
- 1.1 什么是匿名内部类
- 1.2 继承普通类(抽象类)
- 1.3 实现接口
- 1.4 关于匿名内部类
- 二、函数式接口
- 2.1 函数式接口和Lambda是什么
- 2.2 JDK内置的核心函数式接口
- 三、从匿名内部类到函数式接口
- 总结
前言
假想一下你需要对一个集合进行排序,但这是一个临时排序,排序规则只在这一处使用。我们知道,排序需要一个类实现Comparator
接口,并且该类需要实现接口中的compare
方法,最后将这个实现类的实例传入Collections.sort
方法,该方法会在内部调用我们定义的compare
方法。为这一处临时排序逻辑单独定义一个类,重写方法后再实例化,实在过于麻烦,实现起来也不够优雅。
考虑到这种情况,Java提供了两种解决方案——匿名内部类和函数式接口。
匿名内部类可以用来临时实现接口或继承类,它没有类名,只能在创建时使用一次,非常适合封装临时逻辑。而2014年Java 8引入的Lambda表达式改变了这一局面:用Lambda简化函数式接口的实现变得非常流行,链式调用也更为普遍。Lambda表达式的背后正是函数式接口(即仅包含一个抽象方法的接口),我们可以通过函数式接口(及Lambda表达式)简洁地封装临时逻辑。但Lambda真能完全替代匿名内部类吗?为什么说Lambda依赖于函数式接口?下面就让我们对比分析匿名内部类与函数式接口。
一、匿名内部类
1.1 什么是匿名内部类
在聊匿名内部类之前我们得先回顾下接口和普通类。
- 接口本质上是定义的规范,不能够直接被实例化。接口包含抽象方法,默认方法和静态方法。只有抽象方法是没有方法体的,其实现类必须重写。默认方法虽然直接在接口里带方法体,但不是必须要求实现类里重写。抽象方法,默认方法都是需要在实现类的实例里调用,只有静态类是直接从接口名调用,并且不能被重写。
- 普通类主要是用来封装属性与方法,是直接能实例化的(抽象类除外)。
普通类可以直接创建实例,而接口必须通过实现类才能实例化。
匿名内部类的核心是为临时逻辑提供临时载体,它是Java中一种特殊的内部类,无需显式定义类名,直接在内部编写临时逻辑。通过匿名内部类,既可以继承普通类编写自定义逻辑,也能实现接口编写自定义逻辑。
1.2 继承普通类(抽象类)
假设我有个抽象日志方法需要在子类中实现具体格式输出。日志输出的格式根据业务逻辑需要,得非常灵活。匿名内部类能很好的将根据业务插入对应的逻辑,并且无需重复定义多个日志子类
抽象日志类
abstract class Logger {//log需要被匿名内部类实现public abstract void log(String message);
}
模拟用户业务
class UserService {private String username;public UserService (String username) {this.username = username;}//模拟用户登录操作时记录带用户名的日志public void login() {//用匿名内部类继承Logger,定制用户日志格式Logger userLogger = new Logger() {@Overridepublic void log(String message) {System.out.println("[用户操作] 用户名: " + username + " - " + message);}};userLogger.log("登录成功");}
}public class LoggerExample {public static void main(String[] args) {//记录用户登录日志UserService userService = new UserService ("araby");userService.login();}
}
这里再临时重写某个类的方法,通过匿名内部类直接继承并修改逻辑。重点在于通过new Logger()
,并且对void方法直接重写,大括号包含的就上子类的逻辑。
1.3 实现接口
匿名内部类不光能继承类,也能实现接口。我们把上面的抽象日志类换成日志接口
抽象日志接口
interface Logger {void log(String message);
}
模拟用户业务
class UserService {private String username;public UserService (String username) {this.username = username;}//模拟用户登录操作时记录带用户名的日志public void login() {//用匿名内部类实现Logger,定制用户日志格式Logger userLogger = new Logger() {@Overridepublic void log(String message) {System.out.println("[用户操作] 用户名: " + username + " - " + message);}};userLogger.log("登录成功");}
}public class LoggerExample {public static void main(String[] args) {//记录用户登录日志UserService userService = new UserService ("araby");userService.login();}
}
1.4 关于匿名内部类
上面,我们分别演示了通过匿名内部类继承类和实现接口的方式。对比这两种实现,初看匿名内部类的实现没有什么区别,但其实这是一种误解。对于普通类(抽象类)的匿名内部类,它内部是通过new来完成继承,对应接口的的匿名内部类,它内部是通过new完成实现。
一个显著差异是:若接口包含多个抽象方法,匿名内部类必须全部实现;而继承类(非抽象类)时,仅需重写需要定制的方法,无需强制重写所有方法(抽象类需重写全部抽象方法,与接口规则一致)。
虽然匿名内部类已经帮我们极大的简化了对临时逻辑的封装,但是对于仅含一个抽象方法接口的情况,我们有另外一种更加简洁方便的方案,也就是接下来要说到函数式接口。
二、函数式接口
2.1 函数式接口和Lambda是什么
函数式接口是指仅包含一个抽象方法的接口,随着Java 8一并引入的还有一个重要概念——Lambda表达式,函数式接口Lambda的载体,通过Lambda能大大的简化通过匿名内部类实现仅含一个抽象方法的接口
要编写的代码。值得注意的是Lambda并不能完全替代匿名内部类,如果这个接口含有多个抽象方法,用Lambda表达式传入逻辑,Java是无法判断出该临时逻辑是要重写到哪个抽象方法里。
虽然函数式接口仅包含1个抽象方法,但是能有多个默认方法(default修饰)或静态方法(static修饰)。函数式接口的规范是在接口上用@FunctionalInterface注解修饰。
自定义函数式接口示例
@FunctionalInterface
interface FunctionalInterfaceExample {//唯的一抽象方法int calculate(int a, int b); // 唯一抽象方法//默认方法default void defaultMethod() {System.out.println("默认方法");}//静态方法static void staticMethod() {System.out.println("静态方法");}
}
以上就一个典型的函数式接口。调用这个函数式接口有两种方式,一种是传统的实现接口重写方法,另一种就是大家更为习惯的Lambda
实现接口重写方法
//显式实现接口的类
class Calculator implements FunctionalInterfaceExample {//必须重写唯一的抽象方法 calculate@Overridepublic int calculate(int a, int b) {//实现两数相加的逻辑return a + b;}
}public class Main {public static void main(String[] args) {//创建实现类对象FunctionalInterfaceExample calculator = new Calculator();//调用抽象方法int sum = calculator.calculate(3, 5);System.out.println("3 + 5 = " + sum);//调用默认方法calculator.defaultMethod();//调用静态方法FunctionalInterfaceExample.staticMethod(); // 输出:静态方法}
}
而Lambda表达式是函数式接口中唯一抽象方法的简化实现,用于快速实现函数式接口的抽象方法。
Lambda表达式
//(参数列表) -> {方法体}
public class Main {public static void main(String[] args) {FunctionalInterfaceExample add = ( int a,int b) -> {int sum = a + b;return sum;};//省略参数类型FunctionalInterfaceExample add1 = ( a,b) -> {int sum = a + b;return sum;};//省略大括号和 returnFunctionalInterfaceExample add2 = ( a,b) -> a + b;System.out.println(add.calculate(1, 2));}
}
若参数类型可推导,可省略类型,比如 (a,b) -> a + b;若只有一个参数,可省略括号,比如s -> s.length();
若只有一行代码,可省略大括号和 return,比如(a, b)-> a + b;多行代码则必须用大括号包裹
回到我们最开始使用函数式接口的初衷,无非是需要一个容器去承载我们要自定义的逻辑去执行。对于一段逻辑来说,无非是输入,执行,输出。有的逻辑不需要输入参数,有的逻辑执行不需要输出结果。为此Java帮我们预定义了大量函数式接口,覆盖了工作生成中遇到的绝大多数需要临时传递逻辑执行的场景。
有过C#开发经验的伙伴看到这里应该会非常熟悉。这有点类似于C#中的委托,也是将逻辑作为参数传来传去。但是C#中的委托是一种类型,可以多播支持绑定多个方法。而Java的函数式接口是接口,不支持绑定多个方法。
2.2 JDK内置的核心函数式接口
JDK里预定义了大量函数式接口,这里拿常见的Consumer函数式接口举例分析
Consumer<T> 接口的源码实现
package java.util.function;
import java.util.Objects;@FunctionalInterface
public interface Consumer<T> {void accept(T t);default Consumer<T> andThen(Consumer<? super T> after) {Objects.requireNonNull(after);return (T t) -> { accept(t); after.accept(t); };}
}
首先Consumer函数式接口有@FunctionalInterface注解修饰,并且仅有一个accept抽象方法,可以用 Lambda 表达式直接实现。我们通过Lambda表达式传入的自定义逻辑会自动重写这个accept方法。
此接口还有一个默认方法andThen()。它的参数是 Consumer<? super T> after,可以接收T类型的对象,也可以接收T的父类型的对象。用于先执行当前Consumer的accept,再执行参数after对应的 accept,实现链式调用。
Consumer<Object> objectConsumer = o -> System.out.println("打印对象:" + o);
Consumer<String> stringConsumer = s -> System.out.println("打印字符串:" + s);
Consumer<String> chain = stringConsumer.andThen(objectConsumer);
chain.accept("hello");----输出结果----
打印字符串:hello
打印对象:hello
常用内置函数式接口
接口类别 | 接口名称 | 抽象方法 | 逻辑用途描述 | 示例场景 |
---|---|---|---|---|
消费型 | Consumer< T> | void accept(T t) | 接收参数 T,无返回值(消费数据) | 遍历打印、修改对象属性 |
消费型 | BiConsumer<T,U> | void accept(T,U) | 接收两个参数,无返回值 | 处理 Map 键值对 |
供给型 | Supplier< T> | T get() | 无参数,返回 T(提供数据) | 生成随机数、获取配置 |
函数型 | Function<T,R> | R apply(T t) | 接收 T,返回 R(数据转换) | 字符串转整数、提取对象属性 |
函数型 | BiFunction<T,U,R> | R apply(T,U) | 接收两个参数,返回 R | 两数相加并返回结果 |
断言型 | Predicate< T> | boolean test(T) | 接收 T,返回布尔值(条件判断) | 过滤集合、数据校验 |
操作型 | UnaryOperator< T> | T apply(T t) | 参数和返回值类型相同(一元操作) | 数值自增、字符串转大写 |
三、从匿名内部类到函数式接口
前面我们发现匿名内部类的实现语法冗余,并且需要显式声明子类型重写方法并且再实例化,但是适合有多方法实现这种逻辑复杂的场景。通过函数式接口,比如Lambda的实现,虽然简洁,但是这种写法是直接对应抽象方法的实现,只适合单一抽象方法的简单逻辑。
显然匿名内部类适用范围更加广,它不仅能实现多抽象方法接口,也能继承普通类或抽象类并重写方法。而函数式接口的定义规则已经将Lambda限制在单抽象方法的接口中。
但是通过匿名内部类编译后生成独立.class 文件,存在类加载开销,而Lambda表达式是编译后通过 invokedynamic 指令,开销更低。
值得注意的是Lambda表达式中的this是指向外部类,它内部没有独立实例。也就是Lambda内部代码的作用域范围和外部类一致
总结
匿名内部类本质上还是通过对类的临时实现来实现逻辑封装,而Lambda表达式更多的是将函数逻辑作为一个参数传入,是函数式编程思想的体现。灵活的使用二者能帮我们更加优雅的封装代码里的逻辑,写出简洁的代码。