Lambda表达式(为简化代码而生)
什么是Lambda表达式呢?
有人把它称之为“闭包的替代品”,也有人把它称之为匿名函数,它究竟是何方神圣,它在Java中到底充当了什么样的角色?一起关注今晚8点“程序员有话说”,我们不见不散!
哈哈,开个玩笑。它本质上就是一个代码块。
那这个Lambda表达式它具体表现为什么样的结构呢?
它的基础语法其实就是引入了一个操作符:(->),因此也有人把它称之为箭头函数。
这个箭头把一个代码分为左侧和右侧两部分,它的左侧就代表这个表达式的参数列表,而右侧就代表这个表达式的功能,也叫Lambda体。
了解并使用lambda表达式以后可以大大降低我们的代码量,不会增加我们的理解难度,并且让我们的逻辑看起来更加清晰。
首先我们先看这样一个案例:
List<String> list = Arrays.asList("小明","小刚","小艾");
需求:打印该列表中每一个同学的姓名,名字之间用“,”隔开
我们很自然地想到,利用循环来遍历这个列表,依次打印列表中每一个值:
for(String s:list){
System.out.print(s+",");
}
//执行结果:小明,小刚,小艾,
IntelliJ IDEA小技巧,写遍历语句时,可以直接输入以下内容(即可自动生成代码):
- itar:普通for遍历数组
- itli:普通for遍历列表
- iter:增强for遍历列表
但是这样一来我们发现,它打印的结果是:小明,小刚,小艾,
最后一个人后面也有逗号,很显然不合理。
那么如何解决这个问题呢?
程序中一个问题的解决方案可能是千千万万的,那么怎么做才更合理,更优雅一些呢?
我们可以通过字符串的截取操作,把最后一个逗号截取掉,当然我们也可以通过另外一种遍历形式得到:
for(int i = 0;i<list.size()-1;i++){
System.out.print(list.get(i)+",");
}
if(list.size()>0){
System.out.println(list.get(list.size()-1));
}
//执行结果:小明,小刚,小艾
但是这样的代码看起来显得冗余,一点也不优雅。
这里我们给出优雅的解决方案:
//Java8之后的String里的join方法
System.out.println(String.join(",",list));//小明,小刚,小艾
这一行代码即可实现我们以前用多行循环语句加一次if语句才可以做到的事情。
它虽然跟Lambda没有啥关系,但它体现了代码简化的必要性和优雅性,支持这种方式实际上也是未来的一种趋势,就是写代码越来越简单了。
实质上Java8对于代码的优化不止于此
本文中所介绍的 Lambda表达式就是Java8对于代码优化的一种方式
现在我们改变需求,还是刚刚那个列表,现在只需要遍历每一项进行打印即可。
List<String> list = Arrays.asList("小明","小刚","小艾");
//快捷-->list遍历:itli(增强for:iter) 数组遍历;itar
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
for (String s : list) {
System.out.println(s);
}
增强for显然比普通for看起来更加简洁。
但是增强for还可不可以更加简洁呢?
我们可以直接调用这个list的forEach()方法,而这个forEach()方法中,它是可以支持函数式接口的(参数是Consumer,后面再详细介绍它):
//list的forEach方法,它支持Lambda表达式
//lambda的复杂方式
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
通过这种方式,我们仍然可以遍历list,但是这种方式看起来貌似并没有比上面的增强for简洁多少,甚至还要复杂一些。这里我们需要注意,这是Lambda的复杂方式。
这种Lambda的实现方式实际上是可以逐步简化的:
//lambda的简易方式
list.forEach((String s) -> System.out.println(s) );
这种方式甚至可以更加简化:
//lambda的最简易方式
list.forEach(System.out::println);//它可以直接打印出list中每一项的值
不只是集合可以用,数组也可以用,很多其它地方也可以使用Lambda表达式,这里仅仅是使用列表的遍历来举例说明。
和匿名内部类的渊源
Lambda确实是简化代码了,方便了,简洁了,但是它为什么这么方便,它是如何使用,并且是如何做到的呢?
我们先来看看以下的案例:
Lambda的使用其实最早是和匿名内部类相关的。
匿名内部类其实也是一种内部类,只不过没有名字,所以它是一次性的,即只能使用一次。
匿名内部类也是用来简化代码的,但是它需要有一个前提条件:继承父类或实现一个接口:
现在我们来写一个不使用匿名内部类的方式:
package testLambda;
/**
* 不使用匿名内部类的方式
*/
abstract class Person {
public abstract void eat();
}
class Child extends Person{
@Override
public void eat() {
System.out.println("小孩吃零食");
}
}
public class Demo1 {
public static void main(String[] args) {
Person person = new Child();
person.eat();
}
}
我们再写一个使用匿名内部类的方式:
package testLambda2;
/**
* 使用匿名内部类的方式
*/
abstract class Person {
public abstract void eat();
}
public class Demo2 {
public static void main(String[] args) {
//使用匿名内部类,相当于我们只使用一次,不需要给它创建额外的类了
Person person = new Person() {
@Override
public void eat() {
System.out.println("小孩吃零食");
}
};
person.eat();
}
}
如果我们需要使用到多线程,那么我们如何使用它的匿名内部类呢?
package testLambda2;
/**
* 匿名内部类+多线程
*/
public class Demo3 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("threading run");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
如果使用Lambda表达式来声明一个Runnable,可以这样做:
//无参数,无返回值
Runnable runnable1 = () -> System.out.println("threading1 run");
new Thread(runnable1).start();
这是比较简单的Lambda表达式。
它左侧是一个空的(),表示:无参
右侧有且仅有一句打印输出的代码,表示:无返回值
它的基础语法其实就是引入了一个操作符:->
左侧:参数列表
右侧:执行的功能代码(lambda体)
它实际上就是一种的语法糖,只是看起来简单了,但编译器还是要老老实实得把它推导编译成常规的代码。
接下来我们看看这样一组案例:
String [] strArr = {"LiHua","Tom","HongHong"};
需求:我们希望通过字符串的长度,进行排序
不使用Lambda表达式,我们是这样实现的:
package testLambda2;
import java.util.Arrays;
import java.util.Comparator;
public class Demo4 {
public static void main(String[] args) {
String [] strArr = {"LiHua","Tom","HongHong"};
//我们希望通过字符串的长度,进行排序
//Arrays.sort()默认是根据字符串首字母来进行排序的
//因此我们希望自定义Arrays.sort()的排序规则
//首先自己生成一个比较器
class LengthComparator implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
//借用Integer的compare方法为我们比较
/*如果o1.length()<o2.length() 返回-1
相等:返回0
o1.length()>o2.length() 返回1
*/
return Integer.compare(o1.length(),o2.length());
}
}
//复用了Arrays的排序方法
Arrays.sort(strArr,new LengthComparator());
System.out.println(Arrays.toString(strArr));//[Tom, LiHua, HongHong]
}
}
当然上面的方法显得很麻烦,我们还得自己定义一个类,实现Comparator接口,虽然我们复用了Integer的compare方法为我们比较,但代码还是有点冗余。
优化方案:
实质上Arrays.sort()是可以直接支持Lambda表达式的,它的第一个参数仍然是数组本身,第二个参数就可以是Lambda表达式。
public static void main(String[] args) {
String [] strArr = {"LiHua","Tom","HongHong"};
//有参数,有返回值
Arrays.sort(strArr,(String o1,String o2) ->
Integer.compare(o1.length(),o2.length()) );
System.out.println(Arrays.toString(strArr));//[Tom, LiHua, HongHong]
}
以上是我们使用Integer的比较方法,Lambda体一行即可搞定。
但是如果我们自己定义compare比较规则,Lambda体就不止一行了,那么多行的Lambda该怎么写呢?
把多行代码用大括号括起来,代替上面一行的Lambda体:
Arrays.sort(strArr,(String o1,String o2) -> {
if (o1.length()<o2.length()) return -1;
else if(o1.length()>o2.length()) return 1;
else return 0;
});
注意:这种形式的Lambda要求必须覆盖 if 语句的所有可能情况,漏掉一种都不行。
即必须确保它一定有返回值。
我们现在再来看上面的那种形式的Lambda表达式:
Arrays.sort(strArr,(String o1,String o2) ->
Integer.compare(o1.length(),o2.length()) );
对于这种有参数有返回值的Lambda,它有两个参数,o1和o2分别都是String类型的。而且我们要处理的是String数组里的字符串。也就是说,我们很明确自己要处理的就是String类型,这个时候我们就可以把参数的类型省略:
Arrays.sort(strArr,(o1,o2) ->
Integer.compare(o1.length(),o2.length()) );
以上代码中,我们把参数类型省略,编译器仍然能够推算出参数的类型,并且能够正确执行。
这说明,如果参数的类型可以推断出来,那么类型可以省略。
当然,如果参数添加了类型,那么该参数也是可以添加一些修饰符的,例如 final等,还可以添加注解。
Arrays.sort(strArr,(final String o1,final String o2) ->
Integer.compare(o1.length(),o2.length()) );
这里我们再举一些例子,看看其它的Lambda表达式是怎样写的:
-
无参并返回一个数值5:
() -> 5
-
单一参数的 lambda:
(int x) -> 2*x
-
单一参数,且可以直接推断出其类型,就可省略小括号:
x -> 2*x
以上就是Lambda表达式最基础的使用以及它的简化方式。
有时Lambda还可以更加简化,连箭头 -> 都可以省略。
在什么情况下可以省略箭头呢?这里就不得不提到另一种使用方式了,叫做方法引用。
先看以下代码:
//这里还是使用Java8给我们提供的函数式接口Consumer,声明Lambda
Consumer<String> consumer = (x) -> System.out.println(x);
//相当于调用Lambda
consumer.accept("Studying Lambda");
以上的Lambda中,前面的x是对参数的接收,后面lambda体中也仅仅只是对参数x的使用。
像这种表达式的参数和方法的参数完全相同的情况,参数和箭头就可以隐藏:
Consumer<String> consumer = System.out::println;
consumer.accept("Studying Lambda");
以上是更加简单的Lambda表达式的使用方式,而这种方式我们称之为方法引用。而且它仅仅是方法引用其中的一种。
方法引用
方法引用的方式是使得调用者和两个方法之间使用两个冒号(::)进行分隔,总的来说它大致分为3种情况。
- 对象调用它里面的方法:object :: instanceMethod
- 类调用它的静态方法:class :: staticMethod
- 类调用它的实例方法:class :: instanceMethod
说到调用方法,在类的内部,有两个很重要的关键字:this和super
它们在Lambda表达式中也是可以使用方法引用的。
举个小例子:
class Student{
public void print(){
System.out.println("I am a Student");
}
}
class CollegeStudent extends Student{
@Override
public void print() {
//通过线程调用父类的方法
Thread thread = new Thread(super::print);//this同理
thread.start();
}
}
函数式接口
首先思考这样一个问题:为什么我们需要Lambda表达式呢?
从Java语言诞生起它的变化其实是不大的,而且Java中有个很重要的特征是面向对象的编程思想,它和函数式语言JavaScript是截然不同的,那么Java如何去强调它面向对象的本质呢?就是因为在java的世界里函数是没有办法独立存在的,如果说在函数式语言中这个函数是“头等公民”,那么在Java中就不提供这种支持,那它怎么能够去更好得支持函数在其中的作用呢?所以Java的设计者就冥思苦想,想出了这种Lambda表达式的方式,比较类似于 js 里的闭包,但它们还是有一些差别的,但是它相当于闭包的一种替代方案。
Lambda表达式,首先它其实是一种函数,而他这个函数也要是一种对象啊,在Java中万物皆对象,那么这个表达式它是一种什么样的对象类型呢?它是一种函数式接口(functional interface)。
函数式接口,首先它必须是一个接口,并且它里面只能够有一个抽象方法,那么这种接口我们称之为:SAM,它是Single Abstract Method Interfaces的简称。
我们能够使用Lambda表达式,本质上是那个方法它支持了Lambda表达式,包括Integer的sort(),包括Consumer<>等。这是Java8给我们提供的对于原方法的升级,它并没有改变Java面向对象的本质。
那么如果我们希望自己创建的类也可以使用Lambda表达式,该怎么做呢?
我们可以自己声明:(自定义函数式接口)
//首先它必须是一个接口,这是我们自定义的。
//为了更好的满足编译级错误检查,Java给我们提供了这样一个注解:@FunctionalInterface(函数式接口注解)
@FunctionalInterface
interface MyService{
//其次它必须有且仅有一个抽象方法
void print(String message);
}
public class TestSAM {
public static void main(String[] args) {
MyService service = msg -> System.out.println(msg);
service.print("Hello SAM");
//因为就一个参数,所以可隐藏参数以及箭头
MyService service1 = System.out::println;
service1.print("Hello SAM 1");
}
}
为了更好的满足 编译级错误检查,Java给我们提供了这样一个注解:函数式接口注解(@FunctionalInterface)
当我们写的接口不符合函数式接口的定义时,它会报错:
以上是我们自定义的函数式接口,它非常得简单。
Java8给我们提供了一些设计完备的函数式接口,它们可以更好得支持Lambda表达式。
我们刚刚使用的Consumer就是其中之一,它是消费型接口。
四大函数式接口
- Consumer 消费型接口
T是接收的参数类型
(只提供处理参数的类型)
支持那种单一参数且无返回值的形式
通过accept()方法调用
Consumer前面代码涉及得比较多,这里不再展示。
- Function<T,R> 函数型接口
接收T类型的参数,返回R类型的结果
通过apply()方法调用,apply()也叫做应用方法
public class TestSAM {
public static String subStr(String str, Function<String,String> function){
return function.apply(str);
}
public static void main(String[] args) {
String result = subStr("月亮不睡你不睡",str -> str.substring(0,4));
System.out.println(result);//月亮不睡
}
}
- Supplier 供应型接口
T是返回结果的类型
(只提供返回结果的类型)
通过get()方法调用
public class TestSAM {
public static String generateUUID(Supplier<String> supplier){
return supplier.get();
}
public static void main(String[] args) {
String result = generateUUID( () -> UUID.randomUUID().toString());
System.out.println(result);
}
}
- Predicate 断言型接口
T是接收参数的类型
返回类型只能是boolean
通过test方法调用
public class TestSAM {
public static boolean startWith(String str, Predicate<String> predicate){
return predicate.test(str);
}
public static void main(String[] args) {
boolean result = startWith("LiHua",str -> str.startsWith("Li"));
System.out.println(result);
}
}