知识不回顾是会被遗忘的!
网上看了一些相关文章,这里记录一下,仅供参考
Java语言从JDK1.8开始引入了函数式编程。
函数式编程的核心特点是,函数作为一段功能代码,可以像变量一样进行引用和传递,以便在有需要的时候进行调用。
Java对函数式编程的支持,本质是通过接口机制来实现的。首先定义一个仅声明一个方法的接口,然后对接口冠以@FunctionalInterface注解,那么这个接口就可以作为“函数类型”,可以接收一段以Lambda表达式,或者方法引用予以承载的逻辑代码。例如:
@FunctionalInterface
interface IntAdder {
int add(int x, int y);
}
IntAdder adder = (x, y) -> x + y;
IntAdder 就可以看成是一个“函数类型”。Lambda表达式和方法引用的介绍见后文。
概念如此,需要思考的有几点:
为什么必须是只声明一个方法的接口?
显然这个方法就是用来代表“函数类型”所能执行的功能,一个函数一旦定义好,它能执行的功能是确定的,就是调用和不调用的区别。接口中声明的方法就是和函数体定义一一对应的。
事实上,@FunctionalInterface下只能声明一个方法,多一个、少一个都不能编译通过 。覆写Object中toString/equals的方法不受此个数限制。
比如Comparator接口就声明了2个方法:
// Comparator.java
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
//...
}
严格地说,@FunctionalInterface下只能声明一个未实现的方法,default方法和static方法因为带有实现体,所有不受此限制。
@FunctionalInterface
public interface IAdd {
R add(T t1, T t2);
default R test1(T t1, T t2) {//可以额外定义default方法
return null;
}
static R test2(T t1, T t2) {//可以额外定义static方法
return null;
}
}
关于interface中声明default/static方法有疑虑的话,可以查阅博主另一篇文章:java接口里面可以有成员变量么?
@FunctionalInterface注解不是必须的,不加这个注解的接口(前提是只包含一个方法)一样可以作为函数类型。不过,显而易见的是,加了这个注解表意更明确、更直观,是更被推荐的做法。
要定义清楚一个函数类型,除了函数名称,必须明确规定函数的参数个数和类型、返回值类型,这些信息都是包含于接口中声明的方法。
java.util.function包下预定义了常用的函数类型,包括:
@FunctionalInterface
public interface Consumer {
void accept(T t); //接收一个类型为T(泛型)的参数,无返回值;所以叫消费者
}
@FunctionalInterface
public interface BiConsumer {
void accept(T t, U u);//接收2个参数,无返回值
}
@FunctionalInterface
public interface Supplier {
T get();//无参数,有返回值(所以叫提供者)
}
//注意没有BiSupplier,因为返回值只能有1个,不会有2个
@FunctionalInterface
public interface Function {
R apply(T t);//一个输入(参数),一个输出(返回值)
}
@FunctionalInterface
public interface BiFunction {
R apply(T t, U u);//两个输入T和U,一个输出R
}
@FunctionalInterface
public interface UnaryOperator extends Function {
static UnaryOperator identity() {//一元操作,输入原样返回给输出
return t -> t;
}
}
@FunctionalInterface
public interface BinaryOperator extends BiFunction {//二元操作,输入输出类型相同
public static BinaryOperator minBy(Comparator super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;//传入比较器,返回较小者
}
public static BinaryOperator maxBy(Comparator super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;//传入比较器,返回较大者
}
}
这些个定义,都是在参数个数(0,1,2)和有无返回值上做文章。另外还有一些将泛型类型具体化的衍生接口,比如Predicate、LongSupplier等等。
@FunctionalInterface
public interface Predicate {
boolean test(T t);//输入1个参数,返回boolean,就好比是预言家,预言你这个输入是真还是假
}
@FunctionalInterface
public interface LongSupplier {
long getAsLong();//没有输入,输出long类型(long类型的提供者)
}
上面弄清楚了函数类型@FunctionalInterface,那么函数类型能接收怎么样的函数实现体呢?怎么接收呢?该Lambda出场了。
Lambda表达式能赋值给一个变量,也就能当作参数传给函数。这个Lambda形式的变量/参数的类型是它所实现的那个接口,所包含的方法体便是这个接口抽象方法的实现。以后看到调用方法的参数是一个SAM类型接口的时候就可以考虑使用Lambda表达式替换匿名内部类来写。
作用:
任务逻辑传递(传递一段运算逻辑给执行者)
回调逻辑传递(简化接口回调的时候 new匿名类后实现抽象方法的模版代码)
将一个方法写成Lambda表达式,只需要关注参数列表和方法体。
语法组成:
(参数类型 参数名) -> {
方法体;
return 返回值;
}
简写:
Java8 之前创建接口实现类总会有很多冗余的模版代码,接口中定义的抽象方法越多,每次实现的模版代码就越多,而很多时候这个接口实现类只需要用到一次。
变量作用域 :
Lambda表达式用来定义函数实现体。有很多种写法(都是为了简化书写),但核心是通过->连接参数和实现代码:
(入参)->{实现代码}
//无返回值的时候
(int x)->{System.out.println(x);}
(x)->{System.out.println(x);}//参数类型自动推断
x->{System.out.println(x);}//只有一个参数的时候,可以省略小括号
x->System.out.println(x);//实现体只有一个表达式可以省略大括号,System.out.println本身无返回值
//有返回值的情况
(int x)->{return x*x;}
(x)->{return x*x;}
//x->return x*x; //错误,不能这么写!!
x->x*x;
说了这么多,来实操一把:
IntConsumer ic = x->System.out.println(x);
IntFunction
IntFunction
ic.accept(100);//100
System.out.println(ifi1.apply(5));//25
System.out.println(ifi2.apply(5));//25
好了,函数类型–>Lambda表达式说明白了,再来看看方法引用是怎么回事。
文章开头说过了,函数类型可以接收一段Lambda表达式,或者对方法的引用。方法引用就是对一个类中已经存在的方法加以引用,分4中类型:(以Test类为例)
第4种比较难以说清楚,看看下面的例子吧:
public class Test {
private String name = "";
public Test() {
System.out.println("构造方法:无参数");
}
public Test(String name) {
this.name = name;
System.out.println("构造方法:参数="+name);
}
public static void staticMethod(String str) {
System.out.println("static method: input=" + str);
}
public void instanceMethod(String str) {
System.out.println("instance method: input=" + str);
}
public static void main(String[] args) {
Supplier s1 = Test::new;//对无参构造器的引用,无参构造器其实就是一个对象的Supplier(提供者)
s1.get();//调用构造方法:无参数
Function f1 = Test::new;//引用有一个String参数的构造器
f1.apply("Test");//调用构造方法:参数=Test
Consumer c1 = Test::staticMethod;//对静态方法引用
c1.accept("1");//static method: input=1
Consumer c2 = new Test()::instanceMethod;//对实例方法的引用
c2.accept("2");//instance method: input=2
//第4种
BiConsumer bc1 = Test::instanceMethod;
bc1.accept(new Test(), "3");//instance method: input=3
}
}
第4中方法引用,本质上是对实例方法的引用,只不过是在调用的时候才传入那个实例对象。
JDK中很多函数类型,都实现了default的andThen方法,可以将多个函数体(Lambda表达式、方法引用)串起来,方便进行链式调用。
调用链上的任何一个抛出异常,整个调用链会提前结束,异常由调用者处理。
/**
* 通过andThen()进行链式操作
*/
@Test
public void testLinkConsumer() {
IntConsumer action = x-> System.out.print(x);
action = action.andThen(x->System.out.print("--tail1"))
.andThen(x->System.out.print("--tail2"));
//100--tail1--tail2
action.accept(100);
}
小插曲:Callable和Runnable到底什么区别?
//java.util.concurrent.Callable
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
//java.lang.Runnable
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
这两者也都是JDK预定义的函数接口,两者都不接收参数,主要用于多线程编程。
Runnable无返回值,一般用于new一个新线程的时候,在新线程中执行代码。
Callable一样一般用于在新线程中执行,只不过执行成功后有返回值,如果执行失败还会抛异常。
最后,一起分析:
Callable
Callable
c1引用了一个Lambda表达式;
c2引用了一个新的Lambda表达式,表示式的实现代码中调用了c1提供的call()方法,并将call()方法的返回值返回。
Callable c1 = ()->1;
Callable c2 = ()->c1.call();
Callable c3 = ()->{
System.out.println("c3 call c1");
return c1.call();
};
try {
System.out.println(c1.call());//1
System.out.println(c2.call());//1
System.out.println(c3.call());//c3 call c1
//1
} catch (Exception e) {
e.printStackTrace();
}
注意,c3和c2本质上是一样的,只不过方法实现上,多加了一行打印代码。
Lambda 表达式是 Java 8 中添加的功能。引入 Lambda 表达式的主要目的是为了让 Java 支持函数式编程。 Lambda 表达式是一个可以在不属于任何类的情况下创建的函数,并且可以像对象一样被传递和执行。
Java lambda 表达式用于实现简单的单方法接口,与 Java Streams API 配合进行函数式编程。
在前几篇关于 List、Set 和 Map 的文章中,我们已经看到了这几个 Java 容器很多操作都是通过 Stream 完成的,比如过滤出对象 List 中符合条件的子集时,会使用类似下面的 Stream 操作。
List list = aList.filter(a -> a.getId() > 10).collect(Colletors.toList);
其中filter
方法里用到的a -> a.getId() > 10
就是一个 Lambda 表达式,前面对用到 Lambda 的地方知识简单的说了一下,如果你对各种 Stream 操作有疑问,可以先把本篇 Lambda 相关的内容学完,接下来再仔细梳理 Stream 时就会好理解很多了。
上面说了 lambda 表达式便于实现只拥有单一方法的接口,同样在 Java 里匿名类也用于快速实现接口,只不过 lambda 相较于匿名类更方便些,在书写的时候连创建类的步骤也免去了,更适合用在函数式编程。
举个例子来说,函数式编程经常用在实现事件 Listener 的时候 。 在 Java 中的事件侦听器通常被定义为具有单个方法的 Java 接口。下面是一个 Listener 接口示例:
public interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
上面这个 Java 接口定义了一个只要被监听对象的状态发生变化,就会调用的 onStateChange 方法(这里不用管监听的是什么,举例而已)。 在 Java 8 版本以前,监听事件变更的程序必须实现此接口才能侦听状态更改。
比如说,有一个名为 StateOwner 的类,它可以注册状态的事件侦听器。
public class StateOwner {
public void addStateListener(StateChangeListener listener) { ... }
}
我们可以使用匿名类实现 StateChangeListener 接口,然后为 StateOwner 实例添加侦听器。
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
System.out.println("State changed")
}
});
在 Java 8 引入Lambda 表达式后,我们可以用 Lambda 表达式实现 StateChangeListener 接口会更加方便。
现在,把上面例子接口的匿名类实现改为 Lambda 实现,程序会变成这样:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
在这里,我们使用的 Lambda 表达式是:
(oldState, newState) -> System.out.println("State changed")
这个 lambda 表达式与 StateChangeListener 接口的 onStateChange() 方法的参数列表和返回值类型相匹配。如果一个 lambda 表达式匹配单方法接口中方法的参数列表和返回值(比如本例中的 StateChangeListener 接口的 onStateChange 方法),则 lambda 表达式将转换为拥有相同方法签名的接口实现。 这句话听着有点绕,下面详细解释一下 Lambda 表达式和接口匹配的详细规则。
上面例子里使用的 StateChangeListener 接口有一个特点,其只有一个未实现的抽象方法,在 Java 里这样的接口也叫做函数式接口 (Functional Interface)。将 Java lambda 表达式与接口匹配需要满足一下三个规则:
如果能满足这三个条件,那么给定的 lambda 表达式就能与接口成功匹配类型。
只有一个抽象方法的接口被称为函数是式接口,从 Java 8 开始,Java 接口中可以包含默认方法和静态方法。默认方法和静态方法都有直接在接口声明中定义的实现。这意味着,Java lambda 表达式可以实现拥有多个方法的接口——只要接口中只有一个未实现的抽象方法就行。
所以在文章一开头我说lambda 用于实现单方法接口,是为了让大家更好的理解,真实的情况是只要接口中只存在一个抽象方法,那么这个接口就能用 lambda 实现。
换句话说,即使接口包含默认方法和静态方法,只要接口只包含一个未实现的抽象方法,它就是函数式接口。比如下面这个接口:
import java.io.IOException;
import java.io.OutputStream;
public interface MyInterface {
void printIt(String text);
default public void printUtf8To(String text, OutputStream outputStream){
try {
outputStream.write(text.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e);
}
}
static void printItToSystemOut(String text){
System.out.println(text);
}
}
即使这个接口包含 3 个方法,它也可以通过 lambda 表达式实现,因为接口中只有一个抽象方法 printIt没有被实现。
MyInterface myInterface = (String text) -> {
System.out.print(text);
};
尽管 lambda 表达式和匿名类看起来差不多,但还是有一些值得注意的差异。 主要区别在于,匿名类可以有自己的内部状态--即成员变量,而 lambda 表达式则不能。
public interface MyEventConsumer {
public void consume(Object event);
}
比如上面这个接口,通过匿名类实现
MyEventConsumer consumer = new MyEventConsumer() {
public void consume(Object event){
System.out.println(event.toString() + " consumed");
}
};
MyEventConsumer 接口的匿名类可以有自己的内部状态。
MyEventConsumer myEventConsumer = new MyEventConsumer() {
private int eventCount = 0;
public void consume(Object event) {
System.out.println(event.toString() + " consumed " + this.eventCount++ + " times.");
}
};
我们给匿名类,加了一个名为 eventCount 的整型成员变量,用来记录匿名类 consume 方法被执行的次数。Lambda 表达式则不能像匿名类一样添加成员变量,所以也成 Lambda 表达式是无状态的。
使用匿名类实现函数式接口的时候,必须在 new 关键字后指明实现的是哪个接口。比如上面使用过的匿名类例子
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
}
});
但是 lambda 表达式,通常可以从上下文中推断出类型。例如,可以从 addStateListener() 方法声明中参数的类型 StateChangeListener 推断出来,Lambda 表达式要实现的是 StateChangeListener 接口。
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
通常 lambda 表达式参数的类型也可以推断出来。在上面的示例中,编译器可以从StateChangeListener 接口的抽象方法 onStateChange() 的方法声明中推断出参数 oldState 和 newState 的类型。
由于 lambda 表达式实际上只是个方法,因此 lambda 表达式可以像方法一样接受参数。Lambda 表达式参数根据参数数量以及是否需要添加类型会有下面几个形式。
如果表达式的方法不带参数,那么可以像下面这样编写 Lambda 表达式:
() -> System.out.println("Zero parameter lambda");
如果表达式的方法接受一个参数,则可以像下面这样编写 Lambda 表达式:
(param) -> System.out.println("One parameter: " + param);
当 Lambda 表达式只接收单个参数时,参数列表外的小括号也可以省略掉。
param -> System.out.println("One parameter: " + param);
当 Lambda 表达式接收多个参数时,参数列表的括号就没法省略了。
如果编译器无法从 Lambda 匹配的函数式接口的方法声明推断出参数类型(出现这种情况时,编译器会提示),则有时可能需要为 Lambda 表达式的参数指定类型。
(Car car) -> System.out.println("The car is: " + car.getName());
lambda 表达式的方法的方法体,在 Lambda 声明中的 -> 右侧指定:
(oldState, newState) -> System.out.println("State changed")
如果 Lambda 表达式的方法体需要由多行组成,则需要把多行代码写在用{ }括起来的代码块内。
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
可以从 Lambda 表达式返回值,就像从方法中返回值一样。只需在 Lambda 的方法体中添加一个 return 语句即可:
(param) -> {
System.out.println("param: " + param);
return "return value";
}
如果 Lambda 表达式所做的只是计算返回值并返回它,我们甚至可以省略 return 语句。
(a1, a2) -> { return a1 > a2; }
// 上面的可以简写成,不需要return 语句的
(a1, a2) -> { a1 > a2; }
Lambda 表达式本质上是一个对象,跟其他任何我们使用过的对象一样, 我们可以将 Lambda 表达式赋值给变量并进行传递和使用。
public interface MyComparator {
public boolean compare(int a1, int a2);
}
---
MyComparator myComparator = (a1, a2) -> a1 > a2;
boolean result = myComparator.compare(2, 5);
上面的这个例子展示 Lambda 表达式的定义,以及如何将 Lambda 表达式赋值给给变量,最后通过调用它实现的接口方法来调用 Lambda 表达式。
在某些情况下,Lambda 表达式能够访问在 Lambda 函数体之外声明的变量。 Lambda 可以访问以下类型的变量:
Lambda 内访问局部变量,Lambda 可以访问在 Lambda 方法体之外声明的局部变量的值
public interface MyFactory {
public String create(char[] chars);
}
String myString = "Test";
MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};
Lambda 访问实例变量,Lambda 表达式还可以访问创建了 Lambda 的对象中的实例变量。
public class EventConsumerImpl {
private String name = "MyConsumer";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(this.name);
});
}
}
这里实际上也是 Lambda 与匿名类的差别之一。匿名类因为可以有自己的实例变量,这些变量通过 this 引用来引用。但是,Lambda 不能有自己的实例变量,因此 this 始终指向外面包裹 Lambda 的对象。
Lambda 访问静态变量,Lambda 表达式也可以访问静态变量。这也不奇怪,因为静态变量可以从 Java 应用程序中的任何地方访问,只要静态变量是公共的。
public class EventConsumerImpl {
private static String someStaticVar = "Some text";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(someStaticVar);
});
}
}
如过编写的 lambda 表达式所做的只是使用传递给 Lambda 的参数调用另一个方法,那么 Java里为 Lambda 实现提供了一种更简短的形式来表达方法调用。比如说,下面是一个函数式数接口:
public interface MyPrinter{
public void print(String s);
}
接下来我们用 Lambda 表达式实现这个 MyPrinter 接口
MyPrinter myPrinter = (s) -> { System.out.println(s); };
因为 Lambda 的参数只有一个,方法体也只包含一行,所以可以简写成
MyPrinter myPrinter = s -> System.out.println(s);
又因为 Lambda 方法体内所做的只是将字符串参数转发给 System.out.println() 方法,因此我们可以将上面的 Lambda 声明替换为方法引用。
MyPrinter myPrinter = System.out::println;
注意双冒号 :: 向 Java 的编译器指明这是一个方法的引用。引用的方法是双冒号之后的方法。而拥有引用方法的类或对象则位于双冒号之前。
我们可以引用以下类型的方法:
最容易引用的方法是静态方法,比如有这么一个函数式接口和类
public interface Finder {
public int find(String s1, String s2);
}
public class MyClass{
public static int doFind(String s1, String s2){
return s1.lastIndexOf(s2);
}
}
如果我们创建 Lambda 去调用 MyClass 的静态方法 doFind
Finder finder = (s1, s2) -> MyClass.doFind(s1, s2);
所以我们可以使用 Lambda 直接引用 Myclass 的 doFind 方法。
Finder finder = MyClass::doFind;
接下来,如果我们在 Lambda 直接转发调用的方法是来自参数的方法
public interface Finder {
public int find(String s1, String s2);
}
Finder finder = (s1, s2) -> s1.indexOf(s2);
依然可以通过 Lambda 直接引用
Finder finder = String::indexOf;
这个与上面完全形态的 Lambda 在功能上完全一样,不过要注意简版 Lambda 是如何引用单个方法的。 Java 编译器会尝试将引用的方法与第一个参数的类型匹配,使用第二个参数类型作为引用方法的参数。
我们还也可以从 Lambda 定义中引用实例方法。首先,设想有如下接口
public interface Deserializer {
public int deserialize(String v1);
}
该接口表示一个能够将字符串“反序列化”为 int 的组件。现在有一个 StringConvert 类
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
StringConvert 类 的 convertToInt() 方法与 Deserializer 接口的 deserialize() 方法具有相同的签名。因此,我们可以创建 StringConverter 的实例并从 Lambda 表达式中引用其 convertToInt() 方法,如下所示:
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
// 等同于 Deserializer des = (value) -> stringConverter.convertToInt(value)
上面第二行代码创建的 Lambda 表达式引用了在第一行创建的 StringConverter 实例的 convertToInt 方法。
最后如果 Lambda 的作用是调用一个类的构造方法,那么可以通过 Lambda 直接引用类的构造方法。在 Lambda 引用类构造方法的形式如下:
ClassName::new
那么如何将构造方法用作 lambda 表达式呢,假设我们有这样一个函数式接口
public interface Factory {
public String create(char[] val);
}
Factory 接口的 create() 方法与 String 类中的其中一个构造方法的签名相匹配(String 类有多个重载版本的构造方法)。因此,String类的该构造方法也可以用作 Lambda 表达式。
Factory factory = String::new;
// 等同于 Factory factory (chars) -> String.new(chars);
今天这篇文章把 Lambda 表达式的知识梳理的了一遍,相信看完了这里的内容,再看到 Lambda 表达式的各种形态就不觉得迷惑了,虽然今天的文章看起来有点枯燥,不过是接下来 咱们系统学习 Stream 操作的基础,以及后面介绍 Java 中提供的几个函数式编程 interface 也会用到 Lambda 里的知识,后面的内容可以继续期待一下。
Java 8 (2/6篇) - Lambda表达式 & 函数式接口(FunctionalInterface Lib)_java8 lambda函数式接口_Jomurphys的博客-CSDN博客
彻底弄懂@FunctionalInterface、Lambda表达式和方法引用_@interface 表达式-CSDN博客
Java Lambda 表达式的各种形态和使用场景,看这篇就够了 - 知乎