JDK8新特性 ----- Lambda表达式
另外一个最值得学习的JDK8新特性之一,往着跳👉:
从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!
一. Lambda表达式
1.1 Lambda 的引入
JDK8 发布至今已经8年,且马上就要9年了。而我们今天要聊的 Lambda 就是当年 JDK8 推出时伴随引入而来的新特性之一,也是个人认为,JDK8 新特性中最值得去学习之一
相信你肯定或多或少都会听说过或者看到过甚至是使用过 Lambda表达式 的这一种写法,也或许有的小伙伴是第一次听说这个。无所谓,这些都不重要,即使以前不曾听闻,相信你也能在看完这篇文章之后,能够对 Lambda 有一个了解,甚至是在你的代码上渐渐使用起来~
那么在我们正式开始之前,先来简单的聊一聊这个 Lambda 到底是一个什么来的?
其实 Lambda 是一种计算机的编程语言 ,而我们实际上要去用的,Lambda表达式 却是一个匿名函数 。这两者之间是不同的,一个是语言;一个是一种方式,一种代码风格,可别搞混了。
那么或许有人会问了,我们为什么要去使用 Lambda表达式 呢?Lambda表达式 将函数式编程的概念引入了 Java,而函数式编程的好处在于可以帮助我们大大的节省代码量;可以对接口进行一个非常简洁的实现。
或者换句话说,可以让你的代码,变得十分简洁,变得十分优雅~
1.2 Lambda表达式 初体验
1.2.1 Comparator 经典的函数式接口
-
在 Java 中就有一个非常经典的函数式接口 ----- Comparator ,这是一个用于比较的接口。比较经典的例子就是 TreeMap 通过实现该接口中的方法去定义排序规则来实现排序
-
我们就用 TreeMap 的这个列子,来小试牛刀一把。首先,先把这个例子搭起来,通过重写 Comparator 中的 compare 方法来实现排序:
@Test public void test09(){ TreeMap<Integer, Object> map = new TreeMap<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); map.put(1, "天龙八部"); map.put(3, "神雕侠侣"); map.put(5, "射雕英雄传"); map.put(9, "侠客行"); map.put(8, "鹿鼎记"); map.put(6, "笑傲江湖"); map.forEach((k,v) -> System.out.println(k + " -- " + v)); }
单元测试跑起来后,我们可以看到 TreeMap 根据我们定义的规则去排序了。感兴趣的可以试一下没有排序是什么样子的,然后去对比一下
-
可以看到,在上面的列子中用的是匿名内部类这种写法去重写 compare 定义排序规则。这是没有使用 Lambda表达式 的,那么使用了 Lambda表达式 又会变成什么样呢?如下:
TreeMap<Integer, Object> map = new TreeMap<>((o1, o2) -> o2 - o1);
-
是的,你没看错,原来的那么一大段,使用 Lambda表达式 之后就是一句话的事。可能有些刚接触的朋友不是很能够理解,为什么这么一大坨东西能够浓缩成这么简短的一句。想要知道这个,那么就需要对 Lambda 的基础语法去学习了
1.2.2 Lambda 对接口的要求
-
虽然我们可以使用 Lambda表达式 对接口进行某些极为简洁的操作,但不是所有的接口都能够使用 Lambda表达式 的,Lambda表达式 对于接口的要求:接口中定义的必须要实现的方法只能是一样
-
这个是时候就可以引入一个 Java.lang 包下的一个注解:@FunctionalInterface ,这个注解用于修饰函数式的接口,即用了该注解在接口上之后,会自动去检查有且仅有一个抽象方法
-
如果有看了 Comparator接口 源码的朋友就会发现,在 Comparator 的源码中加了 @FunctionalInterface 这个注解,但它有两个抽象方法,为什么呢?
@FunctionalInterface 注解里面给出了解释:大概的意思就是如果这个抽象方法是 Object 的公共方法之一,那么就不计入接口抽象方法计数中;且 JDK8 中加入了默认方法的特性,可以有一个或多个默认方法。
感兴趣的可以去了解一下,总而言之看到这个注解,那么就意味着可以去使用 Lambda表达式 去进行书写了
1.2.3 Lambda 语法
首先我们先来看看 Lambda表达式 的基础语法:
- () ----- 表示参数列表,即入参那个括号的东西,基本一个意思
- {} ----- 表示方法体,在里面可以去处理你的逻辑
- -> ----- 这个是 Lambda表达式 的运算符,叫 goes to,也可以叫 箭头符号,这是分割参数列表与方法体之间的一个符号
对,就这三个,玩来玩去也就是玩这几个了符号了,虽然似乎Lambda 还有一些别的语法,但基础语法就这三个,玩明白这三个,就差不多能够玩懂 Lambda 了
1.2.4 基础实操
废话不多说,直接开始
(1) 无返回、无入参
那么要写 Lambda,就肯定少不了函数式接口了,所以我们自己去整一个:
@FunctionalInterface
public interface TsWithoutReturnAndParam {
// 没有返回值、没有入参
void test();
}
按照我们以往正常的写法,就是实现接口,重写接口的方法,然后再去执行对应的 test()
方法;或者类似像上面 Comparator 例子中用匿名函数重写里面的方法去实现。这两种方式都是没有使用到 Lambda表达式 的,那么用 Lambda表达式 怎么写呢?拭目以待
-
这是没用 Lambda表达式 之前的写法:
@Test public void test10(){ TsWithoutReturnAndParam lambda = new TsWithoutReturnAndParam() { @Override public void test() { System.out.println("飞雪连天射白鹿,笑书神侠倚碧鸳"); } }; lambda.test(); }
-
用了 Lambda表达式 之后的写法:
@Test public void test10(){ TsWithoutReturnAndParam lambda = () -> { System.out.println("飞雪连天射白鹿,笑书神侠倚碧鸳"); }; lambda.test(); }
-
**注意!当我们的方法体,也就是 {} 里面的代码只有一行时,{} 可以把他给干掉,最终就变成这样: **
@Test public void test10(){ TsWithoutReturnAndParam lambda = () -> System.out.println("飞雪连天射白鹿,笑书神侠倚碧鸳"); lambda.test(); }
(2) 无返回、单个入参
继续整一个新接口
@FunctionalInterface
public interface TsNoReturnSingleParam {
// 没有返回值、单个入参
void test(int param);
}
-
以前的写法,我这就不再演示了,都是一样的,举一反三即可。那么我们就直接使用 Lambda表达式 来演示了:
@Test public void test11(){ TsNoReturnSingleParam lambda = (r) -> { System.out.println("你的入参为:" + r); }; lambda.test(8); }
-
刚说了,当方法体中只有一行代码时,{} 可以去掉 ,所以可以优化为:
@Test public void test11(){ TsNoReturnSingleParam lambda = (r) -> System.out.println("你的入参为:" + r); lambda.test(8); }
-
注意!当我们的参数列表只有一个参数时,即 () 入参只有一个,那么连 () 都可以给它干掉 ,所以上面那段继续优化:
@Test public void test11(){ TsNoReturnSingleParam lambda = r -> System.out.println("你的入参为:" + r); lambda.test(8); }
-
这个就是最终的版本了。
(3) 无返回、多个入参
继续整:
@FunctionalInterface
public interface TsNoReturnMultiParam {
/*
// 多参,多个入参嘛~ 本来应该是这个的,但这与我想要演示的不一致,所以后面再演示这种吧,这个先不看
void test(int... params);
*/
void test(int param1, int param2);
}
多参嘛~两个也算是多参对吧!但其实我注释掉的才更严谨些,没关系,就当多看了一种演示了。因为我想要演示的,是不能去掉括号的情况,就与我 Comparator 那个例子中遍历 map 集合那样,括号中有两个参数
-
因为有了前面的基础,我也就不一步一步的优化过来了,直接跳过前奏一步到位了:
@Test public void test12(){ TsNoReturnMultiParam lambda = (p1, p2) -> System.out.println("第一个参数:" + p1 + " " + "第二个参数:" + p2); lambda.test(6, 7); }
-
在多个参数的时候,或者说是两个、及两个以上的时候,(),这个表示参数列表的符号还是需要保留的
-
好了,继续看有返回的
(4) 有返回、单个入参
或许有人会问,有返回、无入参去哪了?其实这个我原本也写出来了,不过感觉没啥营养,就把它干掉了。直接从单个入参开始吧,感兴趣的可以自己去试试
继续整:
@FunctionalInterface
public interface TsSingleReturnSingleParam {
int test(int param);
}
-
当我们的函数式接口有返回值的时候,会稍微有些不一样:
@Test public void test13(){ TsSingleReturnSingleParam lambda = r -> { System.out.println("你输入的参数是:" + r); return 88; }; int result = lambda.test(1); System.out.println("返回值 = " + result); }
-
可以看到,在方法体中有两个代码,所以 {} 就不能去掉了;那么如果只有一行代码的时候呢?按照前面的理解,是不是就应该变成:
TsSingleReturnSingleParam lambda = r -> return 88
,其实这个是不行的,这样就会报错,编译不通过了 -
在我们方法体中,如果唯一的一条语句是一条返回语句,那么在省略大括号的同时,还需要省略 return ,所以,应该变成这样:
TsSingleReturnSingleParam lambda = r -> 88
-
注意…我上面的返回值是直接写死了 “88” 的,可能会造成一个误导。其实有返回、无入参,便是这样的一个形式,写死一个数在返回那里,你写什么就返回什么。所以这就没有什么意义了,所以这一段可以写成这样:
@Test public void test13(){ TsSingleReturnSingleParam lambda = r -> r; int result = lambda.test(5566); System.out.println("返回值 = " + result); }
(5) 有返回、多个入参
继续看:
@FunctionalInterface
public interface TsSingleReturnMultiParam {
String test(int... params);
}
在上面的 无参、多个入参 看了两个入参的演示,那么现在来瞅瞅真正的多个入参是什么样子的
int...
像类似这种写法,可以经常在源码中看到。这是 JDK1.5 新增的语法,表示动态参数的意思,其实本质上也还是一个数组。所以可以把它当成一个数组处理即可
-
代码如下:
@Test public void test14(){ TsSingleReturnMultiParam lambda = params -> Arrays.toString(params); String result = lambda.test(1, 3, 5, 7, 9); System.out.println("返回值 = " + result); }
-
因为就像上面提到的 params 是一个数组,所以转成字符串放回出去就可以看到结果了。
-
所以到这里,想必你也大概能理解他的优化过程了。那么上面提到的 Comparator 就应该知道它是怎样一下子变成了那样吧?小伙伴可以尝试一下把优化的过程一步步写出来~
@Test public void test12(){ /* // 最开始的样子,没有使用Lambda表达式 TreeMap<Integer, Object> map = new TreeMap<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); */ /* // 使用Lambda表达式 TreeMap<Integer, Object> map = new TreeMap<>((Integer o1, Integer o2) -> { return o2 - o1; }); */ /* // 优化1:方法体中只有一行,{} 干掉 TreeMap<Integer, Object> map = new TreeMap<>((Integer o1, Integer o2) -> o2 - o1); */ // 优化2:参数中的基本数据类型是可以省略的,基本数据类型干掉 TreeMap<Integer, Object> map = new TreeMap<>((o1, o2) -> o2 - o1); }
1.2.5 小结
到这里,想必对 Lambda表达式 也已经有了一个大致的了解了。其实很简单的,就是围绕着 ()
、->
、{}
这三个而衍射出来的不同玩法,大大的减少了代码量,从而变得简洁、美观。但个人认为,这些都不重要,最重要的是,用了 Lambda表达式 之后,代码变得十分优雅~
毕竟优雅永不过时~
好了,这里就对上面的一些用法做一个总结吧:
- 当参数列表,即
()
为单参时,可以忽略()
- 当方法体,即
{}
里面的代码只有一行时,也可以忽略{}
- 但,如果方法体中有返回值,在忽略
{}
时,return 也需去掉 - 即不管
()
、{}
这两个存在还是忽略,->
这个箭头符号还是必须有的
1.3 Lambda表达式 进阶
看到这里了,相必你对 Lambda表达式 的使用也有了一定的了解了。那么接下来,我们在来看一下 Lambda表达式 的一些进阶玩法。
1.3.1 方法的引用
直接来看语法:方法的隶属者 :: 方法名
那么什么叫 方法的隶属者 呢?划重点:如果一个方法是静态方法,那么隶属者就是类了;而如果不是静态方法,那么隶属者就是当前的对象了
用到的一个符号就是
::
两个冒号了,就类似是一个指向的意思了。可以快速的将一个 Lambda表达式 的实现指向一个已经实现的方法,这种写法在使用 MyBatis-Plus 操作单表时用的极多,是的,极多!
简而言之的说呢…就是…我们来看一个例子:
-
创建一个类,类里面写一个静态方法。那么我们要实现一个简单的效果:返回的是你入参的两倍
public class Quote { public static int operation(int param) { System.out.println("入参为:" + param); return param * 2; } }
-
我们以往是通过
类名.方法名
然后传参从而得到一个 int 类型的返回值@Test public void test19(){ int result = Quote.operation(8); System.out.println(result); }
-
在前面我们定义的一个接口
TsSingleReturnSingleParam
与当前的定义的静态方法operation
返回值一样,所以可以进行一个引用。@Test public void test19(){ // 使用Lambda表达式 TsSingleReturnSingleParam lambda = r -> operation(r); System.out.println("lambda = " + lambda.test(8)); }
注意:被引用的方法的参数数量以及类型需与接口中定义的抽象方法一致;且返回值类型也需要和接口中定义的一致
-
使用引用的方式,所以最终又变成了这样:
@Test public void test19(){ // Lambda表达式进阶 TsSingleReturnSingleParam lambda = Quote::operation; System.out.println("lambda = " + lambda.test(8)); }
语法 ---- 方法的隶属者 :: 方法名,方法的隶属者:Quote;方法名:operation;
注意,不是一定要静态方法,普通的方法也是可以的。就像前面说的,如果一个方法是静态方法,那么隶属者就是类了;而如果不是静态方法,那么隶属者就是当前的对象了。而只要满足语法的前提下,都是可行的。
1.3.2 构造方法的引用
这个也是一样的,归根结底还是方法的引用,毕竟构造方法也还是方法嘛。
-
先把例子所需要的类都给创建好:
(1) 创建一个 Book 类
public class Book { private String bookName; private String author; public Book() { System.out.println("无参构造被调用..."); } public Book(String bookName, String author) { System.out.println("有参构造被调用..."); this.bookName = bookName; this.author = author; } // getter、setter等常规方法省略 }
(2) 创建两个接口,一个用于调用无参构造的;一个用于调用有参构造的
@FunctionalInterface public interface BookCreateNoParam { Book getBook(); }
@FunctionalInterface public interface BookCreateWithParam { Book getBook(String bookName, String author); }
-
对于这两个函数式接口,如果不考虑引用,要怎样写呢?很简单,一个无参,一个有参,与上面的是一样的:
@Test public void test20(){ // 无参构造 BookCreateNoParam lambda1 = () -> new Book(); System.out.println("Book = " + lambda1.getBook()); // 有参构造 BookCreateWithParam lambda2 = (name, author) -> new Book(name, author); System.out.println("Book = " + lambda2.getBook("笑傲江湖", "金庸")); }
-
在某些情况下,我们返回的是一个对象,如上面,就可以使用以下的这种方式来去写:使用关键字
new
来表示构造函数,即:@Test public void test20(){ // 无参构造 BookCreateNoParam lambda1 = Book::new; System.out.println("Book = " + lambda1.getBook()); // 有参构造 BookCreateWithParam lambda2 = Book::new; System.out.println("Book = " + lambda2.getBook("笑傲江湖", "金庸")); }
-
可以看到,无论引用的是无参构造还是有参构造,都是使用
Book::new
的形式去创建对象;而至于你是有参还是无参,在接口中便定义了,在调用接口中的抽象方法时传递参数
1.3.3 综合案例:在 MP 中的 Lambda表达式 使用
在开始之前,可能要引入两篇我前面的写的文章了:
化繁为简,MP 里面的增删改查:http://t.csdn.cn/XQ1GE
化繁为简,MP 中条件构造器的使用:http://t.csdn.cn/ELvDt
因为演示的实体类我就不重新去搭建了,直接使用之前两篇文章中的一些例子来改造吧
Lambda表达式 主要是在 MP 中的条件构造器中使用的比较多,
-
在 场景二 就可以写成这样了:
@Test public void test10() { QueryWrapper<User> qw = new QueryWrapper<>(); qw.lambda().eq(User::getAge, 18).eq(User::getName,"逍遥"); List<User> userList = userMapper.selectList(qw); userList.forEach(System.out::println); }
(1) 此时的隶属者就是:User 对象,而 getAge、getName 是里面的方法,所以可以直接去引用
(2) 只有下面的
forEach
那里,一般用于对集合遍历的,这个对象需要传一个 Comsumer 接口实现,而这个 Consumer又是一个函数式接口,源码如下:(3) 其中的抽象方法
accept
就相当于把集合中每一个对象都扔进去进行操作,至于你的操作是什么,自己定义。这个与我们前面定义的TsNoReturnSingleParam
类似,只是我们的返回值类型是 int 而这里是一个可变的 T(4)
System.out::println
这句就是 System 这类,里面有一个静态方法 out ,静态方法 out 里面又有一些方法,所以 :: 引用,和前面一个意思;且同时代码只有一行,然后那些可以有可以没有的都给干掉了,就剩下一句了(5) 就比如我们上面
User::getName
,那么我们写成 User.getName() 不一样可以。所以System.out.println()
与System.out::println
是一个意思。只是一个是以前的写法,一个是 JDK8 的写法 -
在来一个那里的例子吧,我看了下,其实都是差不多,就是对 User 中的方法去引用,换一种写法而已。场景六就可以写成:
@Test public void test12() { LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>(); LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>(); qw.like(User::getName, "月") .eq(User::getAge,18) .orderByDesc(User::getCreateTime); List<User> userList = userMapper.selectList(qw); userList.forEach(System.out::println); }
(1)
LambdaQueryWrapper
像之前文章介绍的,你也可以直接把这个实现个 new 出来,这种就是 Lambda表达式 的写法了。(2) 当然,你也可以像上面那里的写法,通过
QueryWrapper
的实现类去点lambda()
也是可以转换成 Lambda表达式 的写法,两种都可以,无关紧要(3) 这里的这段就是,模糊查询名字中包含 “月” 的,并且年龄为 18 岁的,然后根据创建时间排序
1.3.4 小结
-
当然,实际业务肯定是又长又臭的,如果没有使用 Lambda表达式 的方式的话代码量可能就会比较多了,一坨一坨的;而使用 Lambda表达式 则会简洁许多,稍微优雅一些
-
什么!你不信?我这里有一个小例子
(1) 传统的方式
@Test public void test15(){ User user1 = new User(); user1.setId(0); user1.setName(""); user1.setAge(0); user1.setMobile(""); user1.setEmail(""); user1.setCreateTime(LocalDateTime.now()); user1.setCreateBy(""); user1.setUpdateTime(LocalDateTime.now()); user1.setUpdateBy(""); user1.setIsDelete(0); }
(2) 简洁的方式(这种较为优雅)
@Test public void test15(){ User user2 = new User().setId(0) .setName("") .setAge(0) .setMobile("") .setEmail("") .setCreateTime(LocalDateTime.now()) .setCreateBy("") .setUpdateTime(LocalDateTime.now()) .setUpdateBy("") .setIsDelete(0); }
-
显然,第二种更为简洁些,少了很多冗余的代码。而且看着也极为优雅。
-
这个是 Lombok 中的
@Accessors(chain = true)
注解的使用,感兴趣的可以去搜一下相关的文章。
好了,Lambda 这一块也差不多了,相信看到这里,小伙伴们也都差不多能够熟悉了解 Lambda表达式 的使用了。
那么接下来就是 JDK8 中 Stream流 的使用,还是那句话:优化永不过时~让我们优雅到底吧
敬请期待~