从基础到精通,一遍文章读懂 JDK8 Lambda表达式 的使用!

4 篇文章 1 订阅
3 篇文章 0 订阅


另外一个最值得学习的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 经典的函数式接口
  1. 在 Java 中就有一个非常经典的函数式接口 ----- Comparator ,这是一个用于比较的接口。比较经典的例子就是 TreeMap 通过实现该接口中的方法去定义排序规则来实现排序

  2. 我们就用 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 根据我们定义的规则去排序了。感兴趣的可以试一下没有排序是什么样子的,然后去对比一下

    在这里插入图片描述

  3. 可以看到,在上面的列子中用的是匿名内部类这种写法去重写 compare 定义排序规则。这是没有使用 Lambda表达式 的,那么使用了 Lambda表达式 又会变成什么样呢?如下:

     TreeMap<Integer, Object> map = new TreeMap<>((o1, o2) -> o2 - o1);
    
  4. 是的,你没看错,原来的那么一大段,使用 Lambda表达式 之后就是一句话的事。可能有些刚接触的朋友不是很能够理解,为什么这么一大坨东西能够浓缩成这么简短的一句。想要知道这个,那么就需要对 Lambda 的基础语法去学习了

1.2.2 Lambda 对接口的要求
  1. 虽然我们可以使用 Lambda表达式 对接口进行某些极为简洁的操作,但不是所有的接口都能够使用 Lambda表达式 的,Lambda表达式 对于接口的要求:接口中定义的必须要实现的方法只能是一样

  2. 这个是时候就可以引入一个 Java.lang 包下的一个注解:@FunctionalInterface ,这个注解用于修饰函数式的接口,即用了该注解在接口上之后,会自动去检查有且仅有一个抽象方法

  3. 如果有看了 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表达式 怎么写呢?拭目以待

  1. 这是没用 Lambda表达式 之前的写法:

    @Test
    public void test10(){
        TsWithoutReturnAndParam lambda = new TsWithoutReturnAndParam() {
            @Override
            public void test() {
                System.out.println("飞雪连天射白鹿,笑书神侠倚碧鸳");
            }
        };
        lambda.test();
    }
    
  2. 用了 Lambda表达式 之后的写法:

        @Test
        public void test10(){
            TsWithoutReturnAndParam lambda = () -> {
                System.out.println("飞雪连天射白鹿,笑书神侠倚碧鸳");
            };
            lambda.test();
        }
    
  3. **注意!当我们的方法体,也就是 {} 里面的代码只有一行时,{} 可以把他给干掉,最终就变成这样: **

        @Test
        public void test10(){
            TsWithoutReturnAndParam lambda = () -> System.out.println("飞雪连天射白鹿,笑书神侠倚碧鸳");
            lambda.test();
        }
    
(2) 无返回、单个入参

继续整一个新接口

@FunctionalInterface
public interface TsNoReturnSingleParam {
    // 没有返回值、单个入参
    void test(int param);
}
  1. 以前的写法,我这就不再演示了,都是一样的,举一反三即可。那么我们就直接使用 Lambda表达式 来演示了:

        @Test
        public void test11(){
            TsNoReturnSingleParam lambda = (r) -> {
                System.out.println("你的入参为:" + r);
            };
            lambda.test(8);
        }
    
  2. 刚说了,当方法体中只有一行代码时,{} 可以去掉 ,所以可以优化为:

       @Test
        public void test11(){
            TsNoReturnSingleParam lambda = (r) -> System.out.println("你的入参为:" + r);
            lambda.test(8);
        }
    
  3. 注意!当我们的参数列表只有一个参数时,即 () 入参只有一个,那么连 () 都可以给它干掉 ,所以上面那段继续优化:

        @Test
        public void test11(){
            TsNoReturnSingleParam lambda = r -> System.out.println("你的入参为:" + r);
            lambda.test(8);
        }
    
  4. 这个就是最终的版本了。

(3) 无返回、多个入参

继续整:

@FunctionalInterface
public interface TsNoReturnMultiParam {
    /*
    // 多参,多个入参嘛~ 本来应该是这个的,但这与我想要演示的不一致,所以后面再演示这种吧,这个先不看
    void test(int... params);
    */

    void test(int param1, int param2);
}

多参嘛~两个也算是多参对吧!但其实我注释掉的才更严谨些,没关系,就当多看了一种演示了。因为我想要演示的,是不能去掉括号的情况,就与我 Comparator 那个例子中遍历 map 集合那样,括号中有两个参数

  1. 因为有了前面的基础,我也就不一步一步的优化过来了,直接跳过前奏一步到位了:

        @Test
        public void test12(){
            TsNoReturnMultiParam lambda = (p1, p2) -> System.out.println("第一个参数:" + p1 + "  " + "第二个参数:" + p2);
            lambda.test(6, 7);
        }
    
  2. 在多个参数的时候,或者说是两个、及两个以上的时候,(),这个表示参数列表的符号还是需要保留的

  3. 好了,继续看有返回的

(4) 有返回、单个入参

或许有人会问,有返回、无入参去哪了?其实这个我原本也写出来了,不过感觉没啥营养,就把它干掉了。直接从单个入参开始吧,感兴趣的可以自己去试试

继续整:

@FunctionalInterface
public interface TsSingleReturnSingleParam {
    int test(int param);
}
  1. 当我们的函数式接口有返回值的时候,会稍微有些不一样:

        @Test
        public void test13(){
            TsSingleReturnSingleParam lambda = r -> {
                System.out.println("你输入的参数是:" + r);
                return 88;
            };
            int result = lambda.test(1);
            System.out.println("返回值 = " + result);
        }
    
  2. 可以看到,在方法体中有两个代码,所以 {} 就不能去掉了;那么如果只有一行代码的时候呢?按照前面的理解,是不是就应该变成:TsSingleReturnSingleParam lambda = r -> return 88 ,其实这个是不行的,这样就会报错,编译不通过了

  3. 在我们方法体中,如果唯一的一条语句是一条返回语句,那么在省略大括号的同时,还需要省略 return ,所以,应该变成这样:TsSingleReturnSingleParam lambda = r -> 88

  4. 注意…我上面的返回值是直接写死了 “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 新增的语法,表示动态参数的意思,其实本质上也还是一个数组。所以可以把它当成一个数组处理即可

  1. 代码如下:

        @Test
        public void test14(){
            TsSingleReturnMultiParam lambda = params -> Arrays.toString(params);
            String result = lambda.test(1, 3, 5, 7, 9);
            System.out.println("返回值 = " + result);
        }
    
  2. 因为就像上面提到的 params 是一个数组,所以转成字符串放回出去就可以看到结果了。

  3. 所以到这里,想必你也大概能理解他的优化过程了。那么上面提到的 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 操作单表时用的极多,是的,极多!

简而言之的说呢…就是…我们来看一个例子:

  1. 创建一个类,类里面写一个静态方法。那么我们要实现一个简单的效果:返回的是你入参的两倍

    public class Quote {
        public static int operation(int param) {
            System.out.println("入参为:" + param);
            return param * 2;
        }
    }
    
  2. 我们以往是通过 类名.方法名 然后传参从而得到一个 int 类型的返回值

        @Test
        public void test19(){
            int result = Quote.operation(8);
            System.out.println(result);
        }
    
  3. 在前面我们定义的一个接口 TsSingleReturnSingleParam 与当前的定义的静态方法 operation 返回值一样,所以可以进行一个引用。

        @Test
        public void test19(){
            // 使用Lambda表达式
            TsSingleReturnSingleParam lambda = r -> operation(r);
            System.out.println("lambda = " + lambda.test(8));
        }
    

    注意:被引用的方法的参数数量以及类型需与接口中定义的抽象方法一致;且返回值类型也需要和接口中定义的一致

  4. 使用引用的方式,所以最终又变成了这样:

        @Test
        public void test19(){
            // Lambda表达式进阶
            TsSingleReturnSingleParam lambda = Quote::operation;
            System.out.println("lambda = " + lambda.test(8));
        }
    

    语法 ---- 方法的隶属者 :: 方法名,方法的隶属者:Quote;方法名:operation;

    注意,不是一定要静态方法,普通的方法也是可以的。就像前面说的,如果一个方法是静态方法,那么隶属者就是类了;而如果不是静态方法,那么隶属者就是当前的对象了。而只要满足语法的前提下,都是可行的。

1.3.2 构造方法的引用

这个也是一样的,归根结底还是方法的引用,毕竟构造方法也还是方法嘛。

  1. 先把例子所需要的类都给创建好:

    (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);
    }
    
  2. 对于这两个函数式接口,如果不考虑引用,要怎样写呢?很简单,一个无参,一个有参,与上面的是一样的:

        @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("笑傲江湖", "金庸"));
        }
    
  3. 在某些情况下,我们返回的是一个对象,如上面,就可以使用以下的这种方式来去写:使用关键字 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("笑傲江湖", "金庸"));
        }
    
  4. 可以看到,无论引用的是无参构造还是有参构造,都是使用 Book::new 的形式去创建对象;而至于你是有参还是无参,在接口中便定义了,在调用接口中的抽象方法时传递参数

1.3.3 综合案例:在 MP 中的 Lambda表达式 使用

在开始之前,可能要引入两篇我前面的写的文章了:

化繁为简,MP 里面的增删改查:http://t.csdn.cn/XQ1GE

化繁为简,MP 中条件构造器的使用:http://t.csdn.cn/ELvDt

因为演示的实体类我就不重新去搭建了,直接使用之前两篇文章中的一些例子来改造吧

Lambda表达式 主要是在 MP 中的条件构造器中使用的比较多,

  1. 在 场景二 就可以写成这样了:

        @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 的写法

  2. 在来一个那里的例子吧,我看了下,其实都是差不多,就是对 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 小结
  1. 当然,实际业务肯定是又长又臭的,如果没有使用 Lambda表达式 的方式的话代码量可能就会比较多了,一坨一坨的;而使用 Lambda表达式 则会简洁许多,稍微优雅一些

  2. 什么!你不信?我这里有一个小例子

    (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);
        }
    
  3. 显然,第二种更为简洁些,少了很多冗余的代码。而且看着也极为优雅。

  4. 这个是 Lombok 中的 @Accessors(chain = true) 注解的使用,感兴趣的可以去搜一下相关的文章。

好了,Lambda 这一块也差不多了,相信看到这里,小伙伴们也都差不多能够熟悉了解 Lambda表达式 的使用了。
那么接下来就是 JDK8 中 Stream流 的使用,还是那句话:优化永不过时~让我们优雅到底吧
敬请期待~

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天怎么不会塌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值