Lambda表达式,匿名函数
一、标准格式
Lambda省去面向对象的条条框框,格式由3个部分组成 :
- 一些参数
- 一个箭头
- 一段代码
Lambda表达式的标准格式为︰
(参数类型 参数名称)->{代码语句}
格式说明∶
- 小括号内的语法与传统方法参数列表一致︰无参数则留空;多个参数则用逗号分隔。
- -> 是新引入的语法格式,代表指向动作。
- 大括号内的语法与传统方法体要求基本一致。
例子:不使用Lambda表达式
//使用匿名内部类的方式,实现多线程
new Thread(
new Runnable(){
@override
public void run(){
System.out.println( Thread.currentThread().getName()+”新线程创建了");
}
}
).start();
例子:使用Lambda表达式
//使用Lambda表达式,实现多线程
new Thread(
()->{
system.out.println(Thread.currentThread().getName()+”新线程创建了");
}
).start();
Lambda表达式 : 是可推导,可以省略
凡是根据上下文推导出来的内容,都可以省略书写可以省略的内容:
- (参数列表):括号中参数列表的数据类型,可以省略不写
- (参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
- {一些代码} : 如果{ }中的代码只有一行,无论是否有返回值,都可以省略({ }, return,分号)
注意:要省略{ },return,分号必须一起省略
例子:使用优化省略Lambda表达式
//优化省略Lambda
new Thread(
()->System.out.println(Thread.currentThread( ).getName()+”新线程创建了")
).start()
二、 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
-
使用lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的 Runnable、comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
-
使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为"函数式接口”。
三、函数式接口
1、概念
函数式接口在Java中是指: 有且仅有一个抽象方法的接口
。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
备注∶"语法糖"是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是"语法糖"。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
2、格式
只要确保接口中有且仅有一个抽象方法即可,当然接口中可以包含其他的方法(默认,静态,私有)。且接口当中抽象方法的 public abstract 是可以省略的。
@FunctionalInterface注解 作用 : 可以检测接口是否是一个函数式接口
是 : 编译成功 否 : 编译失败
修饰符 interface 接口名称{
public abstract 返回值类型 方法名称(可选参数信息);
//其他非抽象方法内容
}
扩展∶实际上使用内部类也可以达到同样的效果,只是将代码操作延迟
到了另外一个对象当中通过调用方法来完成。而是否调用其所在方法是在条件判断之后才执行的。
3、使用Lambda作为参数和返回值
如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。
例如java.lang.Runnable 接口就是一个函数式接口,假设有一个startThread方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和 Thread类的构造方法参数为Runnable没有本质区别。
public class Demo04Runnable {
private static void startThread( Runnable task) {
new Thread(task). start( );
}
public static void main(String[] args) {
startThread( () -> system.out.println("线程任务执行!"));
}
}
类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取一个java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取。
public class Demo06Comparator {
private static comparator<String> newComparator() {
return (a, b) -> b.length() - a.length();
}
public static void main(string[ ] args) {
String[]array = { "abc", "ab", "abcd”};
system.out.println(Arrays.tostring( array ) );
Arrays.sort( array,newComparator());
system.out.println(Arrays.tostring(array));
}
}
4、常用函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。下面是最简单的几个接口及使用示例。
泛型、枚举、反射
lambda表达式、链式编程、函数式接口、Stream流式计算
超级多 FunctionalInterface
作用:简化编程模型,在新版本的框架底层大量应用!
foreach(消费者类的函数式接口)
4.1 Supplier接口 --供给者接口
没有输入,只有返回值,返回一个 泛型指定的数据类型。
**java.util.function.Supplier接口仅包含一个无参的方法 : T get( )。用来 获取(生成)
一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。
4.2 Consumer接口 --消费者接口
只有输入,没有返回值,使用泛型的数据类型。
==java.util.function.Consumer==接口则正好与Supplier接口相反,它不是生产一个数据,而是消费(使用)
一个数据,其数据类型由泛型决定。
-
抽象方法 void accept( T t )
-
意为消费一个指定泛型的数据。
-
默认方法 : andThen( consumer con )
- 需要两个consumer接口,可以把两个Consumer接口组合到一起,再对数据进行消费。
consumer<String> con1; consumer<String> con2; String s = "hetlo"; con1.accept(s); con2.accept(s); //等价于 连接两个consumer接口再进行消费 con1.andThen( con2 ).accept(s);
- 谁写前边谁先消费。
4.3 Predicate接口 --断定型接口
有一个输入参数,返回值为布尔值。
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用==java.util.function.Predicate==接口。
- 抽象方法 : boolean test( T t )
- 用于条件判断的场景
- 默认方法 : and
- 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用"与"逻辑连接起来实现
与
的效果。
- 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用"与"逻辑连接起来实现
- 默认方法 : or
- 方法 or 实现逻辑关系中的
或
。
- 方法 or 实现逻辑关系中的
- 默认方法 : negate
- 方法 negate 实现逻辑关系中的
非
。
- 方法 negate 实现逻辑关系中的
4.4 Function接口 --改造型接口
传入 T数据类型,返回 R 数据类型
java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据
,前者称为前置条件,后者称为后置条件。
- 抽象方法 : R apply( T t )
- 根据类型T的参数获取类型R的结果。
使用的场景例如∶将String类型转换为Integer类型
。
- 根据类型T的参数获取类型R的结果。
- 默认方法 : andThen( Function fun)
- 用来进行组合操作
四、方法引用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案∶拿什么参数做什么操作。那么考虑一种情况 : 如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑 ?
4.1冗余的Lambda场景
来看一个简单的函数式接口以应用Lambda表达式︰
@FunctionalInterface
public interface Printable {
void print(string str);
}
在 Printable接口 当中唯一的 抽象方法 prin 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单∶
public class Demo01Printsimple {
private static void printstring(Printable data) {
data.print("Hello,world!");
}
public static void main(String[] args) {
printstring(s -> system.out.println(s));
}
}
其中 printString 方法只管调用 Printable 接口的 print( )方法,而并不管 print( ) 方法的具体实现逻辑会将字符串打印到什么地方去。而main方法通过Lambda表达式指定了函数式接口 Printable的具体操作方案为 : 拿到 String(类型可推导,所以可省略)数据后,在控制台中输出它。
4.2问题分析
这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是.System .out对象中的 println(String)方法。既然Lambda希望做的事情就是调用println (String)方法,那何必自己手动调用呢?
4.3、用方法引用改进代码
能.否省去 Lambda的语法格式(尽管它已经相当简洁)呢 ? 只要“引用”过去就好了︰
public class Demo02PrintRef {
private static void printstring(Printable data) {
data.print( "Hello, world! ");
}
public static void main( String[] args) {
printstring(system.out :: println);
}
}
请注意其中的双冒号
::
写法,这被称为方法引用
,而双冒号是一种新的语法。
4.4万法引用符
双冒号 :: 为 引用运算符
,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
语义分析
例如上例中,System.out对象中有一个重载的==println(String)==方法恰好就是我们所需要的。那么对于printstring方法的函数式接口参数,对比下面两种写法,完全等效︰
- Lambda表达式写法 : s -> system.out.println(s);
- 方法引用写法: System.out : : println
第一种语义是指 : 拿到参数之后经Lambda之手,继而传递给 System.out.println方法去处理。
第二种等效写法的语义是指∶直接让system .out 中的 println( ) 方法来取代Lambda。
两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。
注 : Lambda中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常
推导与省略
如果使用Lambda,那么根据 "可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式――它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。