Java 一直是一种面向对象的编程语言。这意味着 Java 编程中的一切都围绕着对象(为了简单起见,除了一些基本类型)。我们不仅有 Java 中的函数,它们还是 Class 的一部分,我们需要使用 class/object 来调用任何函数。
函数式接口
当我们研究一些其他的编程语言时,比如C++
,JavaScript
,它们被称为函数式编程语言,因为我们可以编写函数并在需要的时候使用它们。其中一些语言支持面向对象编程和函数式编程。
面向对象有很多优势,但是它也使得程序变得冗长。例如,假设我们必须创建Runnable
的一个实例。通常我们使用如下的匿名类:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("This is Runnable");
}
};
从上面的代码中我们可以发现,实际使用的部分是run()
方法中的代码。剩下的所有代码都是因为Java程序结构化的方式。
Java 8函数式接口和Lambda表达式通过删除大量的固定代码,帮助我们编写更少、更简洁的代码。
Java 8 函数式接口
有且只有一个抽象方法的接口称为函数式接口。
Java 8引入了@FunctionalInterface
注解将接口标记为函数式接口。
不是使用
@FunctionalInterface
注解的接口才是函数式接口,使用它是为了检查函数式接口的正确性,使用它是一种规范,就像@Override
用来检查重写父类或实现接口的方法的正确性。
例如,我们在一个接口之上使用了该注解,并在其中添加多个抽象方法,此时会引发编译器错误。
java.lang.Runnable
就是使用单个抽象方法run()
的函数式接口的一个很好的例子。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Java 8函数式接口的主要好处是,我们可以使用Lambda表达式来实例化它们,并避免使用笨重的匿名类实现。
Java 8 Collections API
已经被重写,并且引入了新的Stream API
,其中使用了大量的函数式接口。Java 8在java.util.function
包中定义了很多函数式接口,一些常用的函数式接口包括Consumer
、Supplier
、Function
和 Predicate
等等。
下面是一些代码片段,以便我们更好的理解函数式接口
interface Test {
boolean equals(Object obj);
}
//Test不是函数式接口,equals是Object对象的一个成员方法
interface Comparator<T> {
boolean equals(Object obj);
int compare(T o1, T o2);
}
//Comparator是函数式接口,Comparator只有一个抽象的非Object的方法
interface Test2 {
int test2();
Object clone();
}
//Test2不是函数式接口,因为Object.clone()方法不是public而是protected
interface X {
int test(String str);
}
interface Y {
int test(String str);
}
interface Z extends X, Y {
}
//Z是函数式接口,继承了两个相同签名相同返回值的方法
interface X {
List test(List<String> list);
}
interface Y {
List<String> test(List list);
}
interface Z extends X, Y {
}
//Z是函数式接口,Y.test 是一个 subsignature & return-type-substitutable
//关于subsignature & return-type-substitutable参考https://www.ssymon.com/archives/subsignature-return-type-substituable
//这里Y.test签名是X.test签名的subsignature,并且Y.test的返回值类型和X.test返回值类型可以兼容
interface X {
int test(List<String> list);
}
interface Y {
int test(List<Integer> list);
}
interface Z extends X, Y {
}
//Z不是函数式接口,两个抽象方法没有一个是subsignature
//虽然方法签名与泛型无关,但X.test和Y.test无法兼容,Z的编译就会出错
interface X {
long test();
}
interface Y {
int test();
}
interface Z extends X, Y {
}
//编译出错:methods have unrelated return types
//Z不是函数式接口,两个方法返回值不相关不兼容,没有一个是return-type-substitutable
interface A<T> {
void test(T arg);
}
interface B<T> {
void test(T arg);
}
interface C<X, Y> extends A<X>, B<Y> {
}
//编译错误:both methods have same erasure, yet neither overrides the other
//C不是函数式接口,两个方法签名不同,擦除之后变成相同的原生类型
Lambda表达式
通过Lambda表达式,我们可以在面向Java对象的世界中可视化函数式编程。对象是Java编程语言的基础,没有对象就没有函数,这就是为什么Java语言只支持在函数式接口中使用Lambda表达式。由于函数式接口中只有一个抽象函数,因此将Lambda表达式应用于该方法时不会出现混淆。
什么是Lambda表达式:Lambda表达式是一个匿名函数,即没有函数名的函数。
在Java中:所有函数都是类的成员,被称为方法。要创建方法,您需要定义其所属的类。
Lambda表达式语法:箭头前面部分是方法的参数列表,后一部分方法主体
(parameters) -> expression
或
(parameters) -> { statements; }
Lambda表达式使得我们可以使用非常简洁的语法定义一个类和单个方法,以实现具有单个抽象方法的接口,即函数式接口。
下面我们用一些代码来弄明白Lambda表达式如何简化和缩短代码,并使得代码更具备可读性和可维护性。
实现接口:在Java 8之前,如果要创建线程,首先要定义一个实现可运行接口的类。即函数式接口java.lang.Runnable
,其抽象方法run()
不接受任何参数。我们需要定义一个实现类来实现它。
public class TestRunnable implements Runnable{
@Override
public void run() {
System.out.println("TestRunnable is running");
}
public static void main(String[] args) {
TestRunnable r = new TestRunnable();
Thread thread = new Thread(r);
thread.start();
}
}
在此示例中,我们实现了run()
方法将一串字符串打印在控制台。然后创建一个名为 r 的实例对象,并将其传递给线程类的构造函数来创建一个线程对象,并调用线程的start方法。
匿名内部类:我们对上面的代码进行一些改进,使用匿名内部类的方式来实现。
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable inner class is running");
}
});
thread1.start();
}
相比实现接口的方式,匿名内部类的方式更加简洁,无序额外新增实现类。
使用Lambda表达式:在Java 8中,使用Lambda重构的代码更简洁,更具可读性。
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable lambda is running"));
thread.start();
}
从上方的Lambda表达式中我们可以发现:
Runnable
是一个函数式接口,所以我们可以使用Lambda表达式来创建它的实例。run()
方法没有参数,所以Lambda表达式也没有参数。- 因为方法体中只有一条语句,所以可以不使用大括号({})。对于多个语句,则必须像其他方法一样使用大括号,就像
if-else
块一样。
为什么需要Lambda表达式
-
代码行数减少,通过对以上的代码对比我们可以发现使用Lambda表达式可以减少需要编写的代码量以及减少必须创建和维护的自定义类的数量。
比如要实现只使用一次的接口,那么创建另一个代码文件或另一个命名类并不总是很有意义。Lambda表达式可以定义一次匿名实现,以供一次性使用,并显着简化代码。
-
行为参数化,通过行为参数化来传递代码。
举个例子,假如我们对列表中符合给定条件的数字进行求和,使用谓词方式如下:
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); }
使用实例:
List<Integer> numbers = new ArrayList<>(); for (int i = 0; i < 20; i++) { numbers.add(i); } //对所有数字求和 sumWithCondition(numbers, n -> true); //对偶数数字求和 sumWithCondition(numbers, i -> i % 2 == 0); //对大于5的数字求和 sumWithCondition(numbers, i -> i > 5);
再举个例子,求出列表中3到11范围内的最大奇数并返回它的平方:
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() .filter(FunctionalInterfaceTest::isOdd) .filter(FunctionalInterfaceTest::isGreaterThan3) .filter(FunctionalInterfaceTest::isLessThan11) .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isOdd(int i) { return i % 2 != 0; } public static boolean isGreaterThan3(int i) { return i > 3; } public static boolean isLessThan11(int i) { return i < 11; }
(
::
)是Java 8的方法引用(即把这个方法作为值),如上面的FunctionalInterfaceTest::isOdd
,意思是将isOdd()
方法传递给filter()
方法。它是Lambda表达式i -> isOdd(i)
的简写形式。什么是谓词(Predicate)?
谓词指的是条件表达式的求值返回true或false的过程,它接受一个参数值并返回true或false。 -
在Stream API中使用,后续再详细介绍Stream API
Lambda表达式示例
() -> {} // No parameters; void result
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
// Complex block body with multiple returns
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
方法引用和构造方法引用示例
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
![关注微信公众号](/i/l/?n=22&i=blog/1288885/202210/1288885-20221024192833238-944128733.png)