导航
引例
Lambda表达式
格式
格式简化
函数式接口
@FunctionalInterface
四大核心函数式接口
Predicate
Consumer
Supplier
Function
改进
Lambda表达式与变量捕获
方法引用
格式
三类方法引用
静态方法引用
实例方法引用
构造方法引用
有这样一位农场主,他经营着一片苹果园。某天这位农场主突发奇想,他想找出果园里所有的绿苹果。这种简单的要求,我们可以很轻松的帮他实现:
public class Apple {
private String color; // 颜色
private int weight; // 重量
public Apple(String color, int weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public String toString() {
return "Apple [color=" + color + ", weight=" + weight + "]";
}
}
public class FindApple1 {
// 果园
public static List orchard = Arrays.asList(new Apple("green", 150),
new Apple("green", 200), new Apple("yellow", 150),new Apple("red", 170));
public static void main(String[] args) {
List basket = new ArrayList<>();
// 找到所有的绿苹果
for (Apple apple : orchard) {
if("green".equals(apple.getColor())) {
basket.add(apple);
}
}
System.out.println(basket);
}
}
然而这位农场主是一个善变的人,突然他改变了主意——找出果园里所有的红苹果而不是绿苹果。为了应对需求的变更,同时考虑到这位善变的农场主以后可能会想要找其他颜色的苹果,我们对程序做出相应的修改:
public class FindApple2 {
public static void main(String[] args) {
List basket = appleFilter(FindApple1.orchard, "red");
System.out.println(basket);
}
// 找到指定颜色的苹果
private static List appleFilter(List orchard, String color) {
List temp = new ArrayList<>();
for (Apple apple : orchard) {
if(color.equals(apple.getColor())) {
temp.add(apple);
}
}
return temp;
}
}
使用修改之后的程序,即使农场主再次改变主意——找出果园里所有的黄苹果,仍然可以应对需求的变更。然而农场主的确又改变主意了,只不过这一次他要找的并不是黄苹果,而是重量大于150g的苹果。这样一来,我们修改过的程序又不适用于新的需求了。为了一劳永逸,我们把程序调整成这样:
public interface AppleCheck {
boolean test(Apple apple);
}
public class FindApple3 {
public static void main(String[] args) {
List basket = appleFilter(FindApple1.orchard, new AppleCheck() {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
});
System.out.println(basket);
}
// 根据指定条件找苹果(条件可以随意变更)
private static List appleFilter(List orchard, AppleCheck appleCheck) {
List temp = new ArrayList<>();
for (Apple apple : orchard) {
if(appleCheck.test(apple)) {
temp.add(apple);
}
}
return temp;
}
}
使用最终版的程序,不管农场主再冒出什么新的想法,我们只需要修改找苹果的方式(AppleCheck的具体实现)就可以应对需求变更。
引例的最后,我们通过引入接口成功的解决了农场主不断变化需求的问题。但是使用匿名内部类实现接口的做法让我们的代码看起来很笨重,不够简洁。那么还有什么更好的做法吗?答案是肯定的,下面使用Lambda表达式来修改程序:
public class FindApple4 {
public static void main(String[] args) {
// 使用Lambda表达式代替匿名内部类
List basket = appleFilter(FindApple1.orchard, apple -> apple.getWeight() > 150);
System.out.println(basket);
}
private static List appleFilter(List orchard, AppleCheck appleCheck) {
List temp = new ArrayList<>();
for (Apple apple : orchard) {
if(appleCheck.test(apple)) {
temp.add(apple);
}
}
return temp;
}
}
使用Lambda表达式之后的代码看上去是不是很优雅,很简洁?我们只用一行代码就完成了匿名内部类的工作。 那么Lambda表达式应该如何使用呢?先不要着急,我们先了解一下Lambda表达式的格式。
在FindApple4中出现的Lambda表达式是这样的:
apple -> apple.getWeight() > 150 // 简化版Lambda表达式
上面的Lambda表达式是简化之后的样子,完整版是这样的:
(Apple apple) -> { return apple.getWeight() > 150;} // 完整版Lambda表达式
一个完整的Lambda表达式的格式如下所示:
(参数列表) -> { 方法实现 }
然而通常情况下我们不会书写完整的Lambda表达式,而是会进行适当的简化。
Lambda表达式可以根据传入参数自动推导该参数的类型(类型推导),所以参数列表中的的参数类型可以省略。参数列表简化规则如下:
下面通过几个例子演示上述规则:
(a, b) -> { System.out.println(a+b); } // 正确
a, b -> { System.out.println(a+b); } // 错误:不可以存在两个参数并省略括号
a -> { System.out.println(a); } // 正确
String a -> { System.out.println(a); } // 错误:不可以存在参数类型并省略括号() -> { System.out.println("no paramter"); } // 正确
-> { System.out.println("no paramter"); } // 错误:参数列表为空,不可以省略括号
方法实现简化规则如下:
规则演示:
(a, b) -> { System.out.println(a); System.out.println(b);} // 正确
(a, b) -> System.out.println(a); System.out.println(b); // 错误:方法实现有多条语句要用花括号包起来a -> System.out.println(a) // 正确
a -> System.out.println(a); // 错误:方法实现只有一条语句,省略花括号时分号必须一起省略a -> {return a;} // 正确
a -> a // 正确
a -> return a // 错误:方法实现只有一条包含return的语句,省略花括号时return必须一起省略
Tip:上面罗列的规则可能没有考虑到所有的情况,总之大家多尝试一下,不正确的格式是无法通过编译的。
当然,Lambda表达式是有使用条件的,能够使用Lambda表达式的前提是函数式接口——只声明一个抽象方法的接口。再去看我们创建的AppleCheck接口,你就会发现它是一个函数式接口。
我们可以将Lambda表达式作为函数式接口的一个具体实现,例如这样:
AppleCheck appleCheck = apple -> "green".equals(apple.getColor());
我们可以使用Lambda表达式表示函数式接口的一个具体实现,但是在实际开发中,别人可能并不知道某个接口是一个函数式接口,并向其中添加了新的抽象的方法,那么你之前的使用Lambda表达式作为该接口的具体实现的代码就会报错。因此为了表示某个接口是一个函数式接口,我们可以使用@FunctionalInterface注解该接口。
使用@FunctionalInterface注解的接口只能声明一个抽象方法。若接口声明两个抽象方法则无法通过编译。
使用@FunctionalInterface注解的接口一定是函数式接口,不使用@FunctionalInterface注解的接口也可以是函数式接口(只要能保证该接口中只存在一个抽象方法)。
下面将AppleCheck修改为函数式接口:
@FunctionalInterface // 注解为函数式接口
public interface AppleCheck {
boolean test(Apple apple);
// Apple getApple(); // 函数式接口中只允许存在一个抽象方法
boolean equals(Object obj); // Object类中public抽象方法
default void getAppleByDefault() { // default方法
System.out.println("getAppleByDefault");
}
static void getAppleByStatic() { // 静态方法
System.out.println("getAppleByStatic");
}
}
看到上面的代码你可能会觉得很奇怪,不是说函数式接口只能声明一个抽象方法吗?怎么我们定义的函数式接口存在这么多方法?确实函数式接口只允许声明一个抽象方法,但是除抽象方法之外函数式接口中还可以声明以下两种方法:
关于上述第二点,在FunctionalInterface的JavaDoc中有如下描述:
If an interface declares an abstract method overriding one of the public methods of {@code java.lang.Object}, that also does not count toward the interface's abstract method count.
即接口声明的抽象方法重写了Object类中public抽象方法,该抽象方法不计入抽象方法总数。
JDK8中可以定义为函数式接口的接口都加上了@FunctionalInterface注解,如Comparator接口:
@FunctionalInterface
public interface Comparator{
int compare(T o1, T o2);
...
}
如果仅仅为了使用Lambda表达式而特意定义一个函数式接口,未免得不偿失。其实JDK8已经为我们预先定义了大量的函数式接口,下面是四大核心函数式接口:
Predicate(断言)接口声明抽象方法test(),该方法接收一个泛型对象并返回一个布尔值。看到这个接口你可能会觉得似曾相识,没错,Predicate接口就是我们定义的AppleCheck接口的泛型版本。
@FunctionalInterface
public interface Predicate{
boolean test(T t);
default Predicateor(Predicate other) { ... }
default Predicateand(Predicate other) { ... }
default Predicatenegate() { ... }
...
}
除了抽象方法test()之外,Predicate还提供三个默认方法:and(),or()和negate()。这三个方法的返回值都是Predicate类型,通过这三个方法可以构建更为复杂的Predicate。看下面一个例子:
public class TestPredicate {
public static void main(String[] args) {
// 条件1
Predicate redApple = apple -> "red".equals(apple.getColor());
// 条件2
Predicate heavyApple = apple -> apple.getWeight() > 150;
// 条件3
Predicate greenApple = apple -> "green".equals(apple.getColor());
// 条件组合
Predicate complexPredicate = redApple.and(heavyApple).or(greenApple);
for (Apple apple : FindApple1.orchard) {
if(complexPredicate.test(apple)) System.out.println(apple);;
}
}
}
这三个方法的作用相当于逻辑运算符&&、||和!,complexPredicate的判断逻辑相当于这样:
(redApple && heavyApple) || greenApple
除了Predicate之外JDK8还提供特殊版本的Predicate接口:IntPredicate、LongPredicate、DoublePredicate等。
Consumer(消费者)接口声明抽象方法accept(),该方法接收一个泛型对象,没有返回值。
@FunctionalInterface
public interface Consumer{
void accept(T t);
default ConsumerandThen(Consumer after) { ... }
}
Consumer提供一个默认方法:andThen(),该方法的返回值为Consumer类型,可以组合多个Consumer,串联调用。
public class TestConsumer {
public static void main(String[] args) {
// 操作1
Consumer applePrinter1 =
apple -> System.out.print("apple color: " + apple.getColor());
// 操作2
Consumer applePrinter2 =
apple -> System.out.println(", apple weight: " + apple.getWeight());
// 组合操作
Consumer applePrinter = applePrinter1.andThen(applePrinter2);
for (Apple apple : FindApple1.orchard) {
applePrinter.accept(apple);
}
}
}
同Predicate一样,除了Consumer之外JDK8还提供IntConsumer、LongConsumer、DoubleConsumer、BiConsumer等接口。
Supplier(供应商)接口声明抽象方法get(),该方法返回一个泛型对象。
@FunctionalInterface
public interface Supplier{
T get();
}
public class TestSupplier {
public static void main(String[] args) {
// 提供200以下的随机数
Supplier weight = () -> new Random().nextInt(200);
// 提供重量在200一下的红苹果
Supplier appleCreator = () -> new Apple("red", weight.get());
System.out.println(appleCreator.get());
}
}
除了Supplier之外JDK8还提供IntSupplier、LongSupplier、DoubleSupplier、BooleanSupplier等接口。
Function(函数)接口声明抽象方法apply(),该方法接收一个泛型对象并返回一个泛型对象。
@FunctionalInterface
public interface Function{
R apply(T t);
defaultFunction compose(Function before) { ... }
defaultFunction andThen(Function after) { ... }
...
}
正如其名,调用Function接口类似数学中的函数:,给出入参t,经过f()运算之后,得到出参r。
Function接口提供两个默认方法:compose()和andThen(),它们的返回值都是Function类型。通过这两个方法可以将多个Function进行组合调用。
public class TestFunction {
public static void main(String[] args) {
// 函数f()
Function f = x -> x + 1;
// 函数g()
Function g = x -> x * 2;
// 先进行函数g()运算,再开始函数f()运算
Function compose1 = f.compose(g);
// 运算顺序和compose1相反
Function compose2 = f.andThen(g);
System.out.println(compose1.apply(3)); // f(g(3)) : 7
System.out.println(compose2.apply(3)); // g(f(3)) : 8
}
}
当然,JDK8也提供了IntFunction、LongToDoubleFunction、BiFunction等接口。
下面使用JDK8提供的函数式接口来修改FindApple4,这里提供两种思路:
public class FindApple5 {
public static void main(String[] args) {
List basket = appleFilter(FindApple1.orchard, apple -> apple.getWeight() > 150);
System.out.println(basket);
}
private static List appleFilter(List orchard, Predicate predicate) {
List temp = new ArrayList<>();
for (Apple apple : orchard) {
if(predicate.test(apple)) {
temp.add(apple);
}
}
return temp;
}
}
public class FindApple6 {
public static void main(String[] args) {
List basket = new ArrayList<>();
appleFilter(FindApple1.orchard, basket, (result, apple) -> {
if(apple.getWeight() > 150)
result.add(apple);
});
System.out.println(basket);
}
private static void appleFilter(List orchard, List basket,
BiConsumer, Apple> biConsumer) {
for (Apple apple : orchard) {
biConsumer.accept(basket ,apple);
}
}
}
其实说到这儿,好像也没明说Lambda表达式是个啥。不过你可能已经发现了,Lambda表达式本质上就是特定匿名内部类的简写形式。所以既然如此,匿名内部类存在的问题——匿名内部类中访问的局部变量需要修饰为final类型,Lambda表达式也一并继承了下来。
Java8之前,如果在匿名内部类中访问局部变量,需要显式的将此变量声明为final类型,Java8中则会隐式的将匿名内部类中访问的局部声明为final类型:在Lambda表达式中访问局部变量的操作,称之为变量捕获。一旦局部变量被Lambda表达式捕获,那么该变量会被隐式声明成final类型。见下面一个例子:
public class TestLocalVariable1 {
public static void main(String[] args) {
int num = 3;
IntConsumer consumer = (n) -> {
System.out.println(num + n); // 此时num已经被声明为 final int num = 3
};
consumer.accept(2);
}
}
被Lambda表达式捕获的变量,我们不能修改它的值——不论是Lambda表达式内还是外。
对于前者,由于被捕获的变量已经隐式声明为final类型,所以我们不能再去修改它的值,故而无法通过编译;对于后者,由于在Lambda表达式外该变量的值发生了变化,所以这个变量无法被隐式声明为final类型,因此报错。见下面一个例子:
public class TestLocalVariable2 {
public static void main(String[] args) {
int num = 3;
IntConsumer consumer = (n) -> {
// num++; // 无法修改final类型变量的值
System.out.println(num + n);
};
// num++; // 变量的值发生变化,无法被隐式声明为final类型
consumer.accept(2);
}
}
不过对于引用类型变量而言,我们可以Lambda表达式中修改该引用指向的对象。因为引用类型变量中存放的是地址值,用final修饰引用类型变量表示的含义是该引用不可以指向其他的对象。
public class TestLocalVariable3 {
public static void main(String[] args) {
Apple apple = new Apple("red",150);
Consumer consumer = color -> {
// apple = new Apple("green", 170); // 错误:不可修改引用指向的对象
apple.setColor(color);
};
consumer.accept("green");
System.out.println(apple);
}
}
当我们使用Lambda表达式去实现某个功能时,若恰巧存在某个方法可以实现这个功能,就可以用方法引用来表示这个方法。
从上面的定义不难看出来,方法引用的本质是特定Lambda表达式的表现形式。是一种语法糖。方法引用并未定义新的功能,只是Lambda表达式的一种更简洁的表达,具有更强的可读性。
从名字上来看,方法引用属于引用的一种。而我们知道引用类型数据代表的是对实际值的引用,其本身并不存放任何实际值,方法引用也是如此。方法引用表示对某个方法的引用,其本身并不具有该方法的功能实现。
方法引用的格式如下:
类名(对象名):: 方法名
::是域操作符,表示对方法的引用。方法名后面不需要括号。下面通过一个例子来演示方法引用:
public class Student {
private String name;
private int score;
public Student(String name, int score) {
super();
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return "Student [name=" + name + ", score=" + score + "]";
}
public static int compareByScore(Student s1, Student s2) {
return s1.getScore() - s2.getScore();
}
}
public class TestMethodReference1 {
public static List students = Arrays.asList(new Student("zhangsan", 64),
new Student("lisi", 85), new Student("wangwu", 71), new Student("zhaoliu", 82));
public static void main(String[] args) {
students.sort(Student::compareByScore);
System.out.println(students);
}
}
上面的例子中,我们想要调用list的sort()方法对集合中的元素排序,而sort()方法使用Comparator接口作为参数:
public interface List
extends Collection {
default void sort(Comparator c) { ... }
...
}
之前提到过:JDK8将Comparator注解为函数式接口,所以我们可以使用Lambda表达式表示Comparator接口的一个实现:
students.sort((Student s1, Student s2) -> s1.getScore() - s2.getScore());
而此时Student类中恰好存在compareScore()方法,可以实现上面Lambda表达式的功能,那么就可以用方法引用来引用该方法:
students.sort(Student::compareByScore);
方法引用可以分为三类:静态方法引用、实例方法引用和构造方法引用。
格式: 类名 :: 静态方法名
这类方法引用比较好理解——相当于把调用静态方法的.替换成::(注意,这里的用词是相当于,方法调用和方法引用之间没有任何关系,它们是两种完全不相同的东西),TestMethodReference1中使用的就是此类方法引用。
格式1: 对象名 :: 实例方法名
这类方法引用也很容易理解——相当于把调用对象实例方法的.替换成::。
JDK8中Iterable接口新增forEach()方法,该方法使用Consumer接口作为参数:
public interface Iterable
{
default void forEach(Consumer action) { ... }
...
}
下面我们来试着使用该方法来打印集合:
public class TestMethodReference2 {
public static void main(String[] args) {
TestMethodReference1.students.forEach(System.out::println);
}
}
看到上面的例子,你可能会觉得奇怪:这里并没有出现对象,为什么可以使用方法引用呢?其实out就是定义在System类中一个对象:
public final class System {
public final static PrintStream out = null;
...
}
而println()方法正是PrintStream类中的成员方法:
public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
public void println(Object x) { ... }
...
}
这里我们希望有一个方法可以实现打印对象的功能,而实例对象out正好可以提供println()方法,因此可以使用方法引用。
格式2: 类名 :: 实例方法名
这类方法引用是比较难理解的,我们通过一个例子讲解。向Student类中添加下面的方法:
public int compareByScore2(Student s1) {
return this.getScore() - s1.getScore();
}
public class TestMethodReference3 {
public static void main(String[] args) {
TestMethodReference1.students.sort(Student::compareByScore2);
TestMethodReference1.students.forEach(System.out::println);
}
}
通过TestMethodReference1的例子,我们知道sort()方法使用Comparator接口作为参数。然而Comparator接口的compare()方法有两个参数,而Student类的compareByScore2()方法却只有一个参数,这里为什么可以使用方法引用表示compareByScore2()方法呢?
这就是这类方法引用难以理解的地方。首先实例方法肯定需要通过对象来调用,那么这个对象是从哪儿来的呢?我们知道方法引用对应Lambda表达式,Lambda表达式的第一个参数就会成为调用实例方法的对象,其余参数则会作为该实例方法的参数传递。如下图所示:
下面再演示一个例子强化理解:
public class TestMethodReference4 {
public static void main(String[] args) {
List list = Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu");
list.sort(String::compareToIgnoreCase);
list.forEach(System.out::println);
}
}
String类的compareToIgnoreCase()方法定义如下:
public final class String implements java.io.Serializable, Comparable
, CharSequence {
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
...
}
格式1: 类名 :: new
这类方法引用只能用于构造方法。下面通过一个例子演示:
public class TestMethodReference5 {
public static void main(String[] args) {
// Supplier supplier = Student::new; // 报错: Student类中没有无参构造方法
BiFunction bf = Student::new;
Student student = bf.apply("zhangsan", 64);
System.out.println(student);
}
}
上面的例子中,试图通过方法引用表示Student类的无参构造方法,但是由于我们在Student类定义了有参构造方法,所以该类中不存在无参构造方法。因此这里使用方法引用表示Student类的无参构造方法会出现错误信息,方法引用只能表示指定类的无参构造方法。
格式2: 类名[] :: new
这类方法引用是数组专属的。
其实可能你都没发现,到现在为止你都没有见过数组的构造方法。在Java中并不存在数组这个类,它是一种即时创建的类型。数组的构造方法只有一个int类型参数,该参数表示数组的长度。下面的例子是数组构造方法引用:
public class TestMethodReference6 {
public static void main(String[] args) {
IntFunction fun = int[]::new;
int[] arr = fun.apply(5); // 创建长度为5的数组
System.out.println(arr.length);
}
}
到此为止,关于Lambda表达式、函数式接口和方法引用就全部介绍完了。不过这些只是所有Java8新特性的基础,下一篇文章将进入Java8新特性最重要部分的学习——流。
参考:
https://blog.csdn.net/yangyifei2014/article/details/80068265
https://blog.csdn.net/sun_promise/article/details/51190256
https://segmentfault.com/a/1190000012269548