Java 8 Functional Interface 函数式接口
java一直以来都是作为一个面向对象的语言。这意味着java里的一切都是围绕着对象的,在java里面没有单独存在的函数,它们都是类的一部分,我们需要通过类或者对象来调用它们。
像C++或者JavaScript这些语言,他们被称为函数式编程语言因为在这些语言里面我们可以编写我们需要的函数,这些语言当中有一些支持既面向对象编程也支持函数式编程。
当然不是说面向对象不好,但是它会给编程带来大量的冗余。比如说,创建一个Runnable的实例。一般来说我们可以使用匿名类的方法。
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
如果你仔细阅读上面的代码,你会发现真正有用的是run方法内部的代码,剩下的则是由于java编程所带来的语法结构。
java 8 函数式接口和lambda表示可以帮助我们编写体积更小的代码
一个只含有一个抽象方法的接口被称为函数式接口(FunctionalInterface),这个接口也会被@FunctionalInterface注解,用此标注该接口为函数式接口这个注解不是强制性的,但是使用上这个注解能够避免当你想把这个接口作为函数式接口又不小心给它还写了其他的抽象的方法这样的情况,因为注解@FunctionalInterface会让编译器在编译期间检查是否只含有一个抽象方法,否则就会抛出编译错误。
java 8函数式接口的主要好处在于,我们可以通过它使用lambda表达式,来实例化这些接口,以避免让匿名类的实现变得很臃肿。
java.lang.Runnable接口是一个典型的函数式接口,因为它刚好只有一个抽象方法run();
下面代码展示了一些函数式接口的例子。
interface Foo { boolean equals(Object obj); }// 不是函数式 因为 equal()已经是object的一个成员
interface Comparator<T> {
boolean equals(Object obj);
int compare(T o1, T o2);
}//是函数式因为Comparator只有这一个不属于Object 的抽象方法
interface Foo {
int m();
Object clone();
}// 不是函数式因为 Object.clone 不是public的
interface X { int m(Iterable<String> arg); }
interface Y { int m(Iterable<String> arg); }
interface Z extends X, Y {}
// 是函数式: 两个方法,但是有相同的方法签名
interface X { Iterable m(Iterable<String> arg); }
interface Y { Iterable<String> m(Iterable arg); }
interface Z extends X, Y {}
// 是函数式,因为Y.m 是一个子方法签名并且返回类型可以替代
interface X { int m(Iterable<String> arg); }
interface Y { int m(Iterable<Integer> arg); }
interface Z extends X, Y {}
// 不是函数式:没有一个方法拥有其他所有的抽象方法的子签名
interface X { int m(Iterable<String> arg, Class c); }
interface Y { int m(Iterable arg, Class<?> c); }
interface Z extends X, Y {}
// 不是函数式:没有一个方法拥有其他所有的抽象方法的子签名
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// 编译错误: 没有方法的签名是可替代的
interface Foo<T> { void m(T arg); }
interface Bar<T> { void m(T arg); }
interface FooBar<X, Y> extends Foo<X>, Bar<Y> {}
// 编译错误,不同的签名
Lambda 表达式
在面向对象的Java世界里,lambda表达式使Java实现了函数式编程,对象是Java的基础,正是因为这个原因我们永远也无法编写出一个不属于任何对象的函数。这就是为什么Java仅仅只为函数式接口提供lambda表达式支持的原因。
因为在函数式接口当中刚好只有一个抽象的方法,这样使用lambda表达式就不会出现表意不明的情况,也就是说不知道是调用那个方法的情况。Lambda表达式的语法是(函数参数)->(函数体)。现在看看如何使用Lamabda表达式编写一个Runnable的匿名类
Runnable r1 = () -> System.out.println("My Runnable");
让我们搞清楚上面的代码究竟产生了哪些作用:
- Runnable是一个函数式接口,所以我们可以使用lambda表达式创建一个它的实例
- 因为run()函数没有任何参数,所以这个lambda表达式也没有任何参数
- 就像if else的语法规则一样,假设我们只有一行代码那我们可以在函数体内不写花括号{};但是如过果有多行代码,那我们就必须要使用花括号。
为什么需要lambda表达式
- 减少代码一个很明显的例子就是lambda表达式可以帮助我们的代码更加简洁。
- 列化和并行支持使用lambda的另外一个好处是我们可以使用Java的Stream API的串行和并行操作。
串行和并行支持使用lambda的另外一个好处是我们可以使用Java的Stream API的串行和并行操作。
//传统的写法
private static boolean isPrime(int number) {
if(number < 2) return false;
for(int i=2; i<number; i++){
if(number % i == 0) return false;
}
return true;
}
上面这段代码,是串行执行的,如果数字非常大那将会耗费巨大的时间。另外一个问题在于这个段代码有很多退出点而且可读性也不是很高。下面我们用lambda表达式 和Stream来解决这个问题。
//声明式写法
private static boolean isPrime(int number) {
return number > 1
&& IntStream.range(2, number).noneMatch(
index -> number % index == 0);
}
IntStream包含了很多nt型序列的串行和并行操作,他是使用StreamAPI的int型操作的实现。为了让代码的可读性更高,我们也可以写成下面的样子
private static boolean isPrime(int number) {
IntPredicate isDivisible = index -> number % index == 0;
return number > 1
&& IntStream.range(2, number).noneMatch(
isDivisible);
}
可能会有人不太了解IntStream 的range()方法,它返回一个从range方法第一个参数开始到第二个参数的顺序序列,间隔为1。
noneMatch()返回序列中是不是没有元素满足声明好的条件。这个跟方法跟&&操作一样,如果已经确定返回值,则不会继续判断所有的元素是否满足声明好的条件。
传递方法行为
我们通过一个简单的例子看看如何使用lambda表达式传递一个方法行为比如说,我们需要可根据给出的标准来求出一个数字List当中所有满足某个条件的数的和,我们可以使用Predicate。
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) {
return numbers.parallelStream()
.filter(predicate)
.mapToInt(i -> i)
.sum();
}
使用:
//所有数的和
sumWithCondition(numbers, n -> true)
//偶数的和
sumWithCondition(numbers, i -> i%2==0)
//大于5的数的和
sumWithCondition(numbers, i -> i>5)
惰性带来的高效性
另外一个lambda的优点是,例如我们需要写一个方法找到3-11之间最大的奇数,并且返回它的平方。一般来说我们会这样写。
private static int findSquareOfMaxOdd(List<Integer> numbers) {
int max = 0;
for (int i : numbers) {
if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
max = i;
}
}
return max * max;
}
上面这段代码会按顺序的一个一个循环判断,但是我们可以使用StreamAPI的惰性搜索来达到同样的效果。让我们看看如何通过函数式编程并且使用lambda和Stream 来达到上面的效果。
public static int findSquareOfMaxOdd(List<Integer> numbers) {
return numbers.stream()
.filter(NumberTest::isOdd) //Predicate 是一个函数式接口
.filter(NumberTest::isGreaterThan3) // 我们使用lambda表达式来实例化它
.filter(NumberTest::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新引入的方法引用Java的编译器会使传入的参数去调用这个方法,它是lambda表达式i ->isGreaterThan3(i) 或者 i->NumberTest.isGreaterThan3(i)的缩减版
lambda表达式例子
() -> {} // 没有参数没有返回值
() -> 42 // 没有参数,有表达式体
() -> null //没有参数,有表达式体
() -> { return 42; } // 没有参数,有返回值表达式体
() -> { System.gc(); } // 没有参数,没有返回值表达式体
// 复杂代码块和多种返回值
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // 单一类型声明参数
(int x) -> { return x+1; } //同上
(x) -> x+1 // 单一类型推理参数
x -> x+1 // 单一的类型推理参数,括号可选
(String s) -> s.length() // 单一类型声明参数
(Thread t) -> { t.start(); } // 单一类型声明参数
s -> s.length() // 单一类型推理参数
t -> { t.start(); } // 单一类型推理参数
(int x, int y) -> x+y // 多个声明类型参数
(x,y) -> x+y // 多个推理类型参数
(x, final y) -> x+y // 非法,不能修改推理类型参数
(x, int y) -> x+y // 非法:不能包含推理类型和声明类型参数
方法和构造函数引用
一个方法引用是用来引用这个函数而不是直接调用它,一个构造函数引用只是用来引用这个方法,而不是创建一个新的实例。
例如下面
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
译者注:本文翻译自journaldev
因本人水平有限,如果翻译上有什么纰漏,或者错误,请大家不吝赐教。