Lamdba表达式详解(一篇解决lamdba表达式)

文章目录

  • Lamdba表达式
    • 前言
    • 一、Lamdba表达式的标准格式
    • 二、Lamdba表达式的三种写法
      • 1、单个参数的
      • 2、多个参数
      • 3、要写参数类型的
    • 三、Lamdba栗子:
      • 1、方式1:定义一个类MyRunnable实现Runnable接口,重写run()方法
      • 2、方式2:匿名内部类的方式改进
      • 3、方式3:Lamdba表达式的方式改进
      • 4、 三种方式实现需求的优缺点
  • 函数接口
    • 一、基本应用
    • 二、其他函数接口
      • 1、UnaryOperator
      • 2、Predicate
      • 3、Consumer
      • 4、Supplier
  • 方法引用
    • 一、什么是方法引用
      • 1、静态方法引用
      • 2、实例方法引用
      • 3、构造方法引用
      • 4、数组构造方法引用
    • 二、变量引用
    • 三、类型推断
    • 四、级联表达式
    • 参考资料:

Lamdba表达式

前言

虽然 JDK8 发布距今已经七八年了,但是相信还是有相当多小伙伴用着 JDK8,写着 JDK6 的代码。所以我们有必要回顾一下 JDK8,其中JDK8很重要的新特性就是Lamdba表达式。

JDK8 中引入了 Lamdba,这个大家都知道,虽然现在 JDK 都出到 16 了,但是老实说,项目中的Lamdba 表达式似乎还是很少有人用。有的团队技术风格激进,可能会见到很多Lamdba,但是大部分技术团队还是比较保守的。

好啦,开整吧!

一、Lamdba表达式的标准格式

组成Lamdba表达式的三要素:形式参数,箭头,代码块

(形式参数)->{
    代码块
}
  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果只有一个参数可以不要(),如果没有参数留空即可
  • ->:由英文中画线和大于符号组成,固定写法。代表指向动作
  • 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容,如果方法体不止一行,需要用上 {} ,如果方法体只有一行,则不需要 {} ;

二、Lamdba表达式的三种写法

先来说说,如果要用 Lamdba,必须是只有一个需要强制实现方法的接口,我们可以使用@FunctionalInterface注解去标记该接口:

@FunctionalInterface
public interface Calculator {
    int square(int i);
}

此时如果该接口中有多个空方法,编译期间就会报错。

现在我们建议尽量将一个接口设计的小一些,这样也满足单一职责原则。

不过JDK8 中引入了default方法,就是自带默认实现的那种,自带默认实现的方法可以有多个,这个并不影响Lamdba,并且@FunctionalInterface注解也不会去检查默认方法的数量。

1、单个参数的

如果只是一个参数,那么直接写参数即可,例如如下代码:

@FunctionalInterface
public interface Calculator {
    int square(int i);
}

public class LamdbaDemo01{
    public static void main(String[] args) {
        Calculator ic = i ->i*i;
        int square = ic.square(5);
        System.out.println("square+" + square);
        //结果:square+25
    }
}

当函数只有一个参数的时候,直接写即可,不需要添加()。

2、多个参数

多个参数的话,就需要写上 () 了,以 Spring Security 中登录成功的回调为例:

.defaultLogoutSuccessHandlerFor((req,resp,auth)->{
    resp.setContentType("application/json;charset=utf-8");
    Map<String, Object> result = new HashMap<>();
    result.put("status", 200);
    result.put("msg", "使用 logout1 注销成功!");
    ObjectMapper om = new ObjectMapper();
    String s = om.writeValueAsString(result);
    resp.getWriter().write(s);
},new AntPathRequestMatcher("/logout1","GET"))
.defaultLogoutSuccessHandlerFor((req,resp,auth)->{
    resp.setContentType("application/json;charset=utf-8");
    Map<String, Object> result = new HashMap<>();
    result.put("status", 200);
    result.put("msg", "使用 logout2 注销成功!");
    ObjectMapper om = new ObjectMapper();
    String s = om.writeValueAsString(result);
    resp.getWriter().write(s);
},new AntPathRequestMatcher("/logout2","POST"))
.and()
.csrf().disable();

这种情况,方法有多个参数,此时使用 Lamdba表达式就需要加上 () 。

3、要写参数类型的

正常来说用 Lamdba时候不需要写上参数类型,但是如果你需要写,就要加上 () ,还是上面那个例
子,如下:

interface ICalculator{
    int square(int i);
}
public class LamdbaDemo01 {
    public static void main(String[] args) {
        ICalculator ic = (int i) -> i * i;
        int square = ic.square(5);
        System.out.println("square = " + square);
    }
}

三、Lamdba栗子:

**需求:**启动一个线程,在控制台输出一句话:多线程程序启动了

1、方式1:定义一个类MyRunnable实现Runnable接口,重写run()方法

//创建MyRunnable类的对象
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("多线程程序启动了...");
    }
}

public class LamdbaDemo {
    public static void main(String[] args) {
        //实现类的方式实现需求
        MyRunnable my = new MyRunnable();
        //创建Thread类的对象,把MyRunnable的对象作为构造参数传递
        Thread t = new Thread(my);
        //启动线程
        t.start();
    }
}

2、方式2:匿名内部类的方式改进

public class LamdbaDemo {
    public static void main(String[] args) {
        //匿名内部类的方式改进
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多线程程序启动了...");
            }
        }).start();
    }
}

3、方式3:Lamdba表达式的方式改进

public class LamdbaDemo {
    public static void main(String[] args) {
        //Lamdba表达式改进
        new Thread(() -> {
            System.out.println("多线程程序启动了...");
        }).start();
    }
}

4、 三种方式实现需求的优缺点

  • 综合上述三个方法可以看出,当我们使用实现类的方式实现需求的时候,我们可以看到我们需要先创建一个实体类,然后将实现类的对象传进去才可以使用。
  • 当我们使用你们匿名内部类的方法实现需求时,我们可以发现需要重写run方法(当我们调用其他方法,忘记去重写什么方法时会比较懵逼),相比较也是比较麻烦的。
  • 当我们使用Lamdba表达式来实现需求时,我们可以看到我们不用关心创建了什么实体类、重写了什么方法。我们只需要关心它最终要做的事情是System.out.println("多线程程序启动了...");

函数接口

JDK8 中自带了函数式接口,使用起来也非常方便。

一、基本应用

我们先来看一个简单的例子。

假设我有一个打招呼的接口 SayHello,SayHello 接口中只有一个 sayHello 方法,然后在 User 类中调
用该接口对应的方法,最终用法如下:

@FunctionalInterface
interface SayHello {
    String sayHello(String name);
}

class User {
    private String username;
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String say(SayHello sayHello) {
        return sayHello.sayHello(this.username);
    }
}

public class LamdbaDemo02 {
    public static void main(String[] args) {
        User user = new User();
        user.setUsername("javaboy");
        String say = user.say((username) -> "hello " + username);
        System.out.println("say = " + say);
    }
}

分析 main 方法中的调用过程之后,我们发现,在调用时最核心的是如下一行代码:

(username) -> "hello " + username

在这段代码中,我们只关心方法的输入和输出,其他的都不是我所考虑的,为了一个简单的输入输出,
我还要额外定义一个接口,这显然不太划算。

JDK8 中提供了函数接口,可以帮助我们简化上面的接口定义。如下:

class User2 {
    private String username;
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String say(Function<String,String> sayHello) {
        return sayHello.apply(this.username);
    }
}

public class LamdbaDemo03 {
    public static void main(String[] args) {
        User2 user2 = new User2();
        user2.setUsername("javaboy");
        String say = user2.say((username) -> "hello " + username);
        System.out.println("say = " + say);
    }
}

可以用 Function 代替我们前面的接口定义,这里有两个泛型,第一个泛型表示接
口输入的参数类型,第二个泛型表示接口输出的参数类型,而且大家注意,我们最终 main 方法中的调
用方式是不变的。有了 Function 函数之后,以后我们就不需要定义一些简单的接口了。

而且 Function 函数还支持链式操作,如下:

public class LamdbaDemo03 {
    public static void main(String[] args) {
        User2 user2 = new User2();
        user2.setUsername("javaboy");
        Function<String, String> func = (username) -> "hello " + username;
        String say = user2.say(func.andThen(s -> "你好 " + s));
        System.out.println("say = " + say);
    }
}

二、其他函数接口

接口 输入参数 返回类型 说明
UnaryOperator T T 一元函数,输入输出类型相同
Predicate T boolean 断言
Consumer T T / 消费一个数据,只有输入没有输出
Function T T R 输入 T 返回 R,有输入也有输出
Supplier / T 提供一个数据,没有输入只有输出
BiFunction (T,U) R 两个输入参数
BiPredicate (L,R) boolean 两个输入参数
BiConsumer (T,U) void 两个输入参数
BinaryOperator (T,T) T 二元函数,输入输出类型相同

接下来我们来看看这些函数接口。

1、UnaryOperator

当输入输出类型相同时,可以使用 UnaryOperator 函数接口,例如我们上面的代码,修改之后如下:

class User2 {
    private String username;
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String say(UnaryOperator<String> sayHello) {
        return sayHello.apply(this.username);
    }
}

public class LamdbaDemo03 {
    public static void main(String[] args) {
        User2 user2 = new User2();
        user2.setUsername("javaboy");
        UnaryOperator<String> func = (username) -> "helloo " + username;
        String say = user2.say(func);
        System.out.println("say = " + say);
    }
}

2、Predicate

Predicate 输入一个 T 类型的参数,输出一个 boolean 类型的值。

举一个简单的例子,例如如下代码,我们定义一个 List 集合中存放着用户姓名,现在要过滤出所有姓张
的用户,代码如下:

public class LamdbaDemo04 {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("张三", "里斯", "张五");
        List<String> list = names.stream().filter(s ->
        s.startsWith("张")).collect(Collectors.toList());
        for (String s : list) {
            System.out.println("s = " + s);
        }
    }
}

filter 中传入的就是一个 Predicate 函数接口,这个接口接收 String 类型的数据,返回一个 boolean。

注意:

一些常用类型的函数接口,JDK 中直接提供了相关的类供我们使用,例如 Predicate 可以
IntPredicate代替; Consumer 可以用 IntConsumer 代替。

3、Consumer

看名字就知道,这个是消费数据,只有输入没有输出。

例如集合的遍历就可以使用 Consumer 函数接口。

public class LamdbaDemo04 {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("张三", "里斯", "张五");
        names.stream().forEach(s -> System.out.println(s));
    }
}

4、Supplier

Supplier 刚好和 Consumer 相反,它只有输出没有输入。有的时候我们的工厂方法没有输入只有输
出,这个时候就可以考虑使用 Supplier(如果有输入参数,则可以考虑使用 Function 函数接口)。

Supplier<Connection> supplier = ()->{
    Connection con = null;
    try {
        con = DriverManager.getConnection("", "", "");
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return con;
};
Connection connection = supplier.get();

方法引用

到今天,方法引用估计很多小伙伴可能多多少少都见过,即使自己没写过,可能也看别人写过。用过的
小伙伴可能会感觉这个用着真爽,cool!没用过的小伙伴可能就要吐槽这什么鬼代码。

不管怎么样,我们今天还是来看看方法引用;

一、什么是方法引用

什么是方法引用?

简单说,方法引用就是一个 Lamdba 表达式,操作符就是 :: ,有的小伙伴们可能会觉得所谓的
Lamdba 就是 -> 代替匿名内部类,其实不然!Lamdba 中包含的东西还是蛮多的,方法引用就算是其
中之一。

有的时候,我们使用 Lamdba,需要自己写方法的实现,但是有的时候,我们可能不需要自己写方法的
实现,就是单纯的调用一下方法,这种时候,通过方法名称来调用,会更加清晰,可读性更高,也更加
简洁易懂。

方法引用不仅可以用来访问类或者实例中已经存在的方法,也可以用来访问构造方法。

1、静态方法引用

例如我定义一个 Lamdba,该 Lamdba 中有一个方法可以完成对数字格式的转换,如下:

public class LamdbaDemo05 {
    public static void main(String[] args) {
        Function<Integer, String> func = a -> String.valueOf(a);
        String s = func.apply(99);
        System.out.println("s = " + s);
    }
}

在上面的这个 Function 中,我们将一个 Integer 类型数字转为了一个字符串,由于在 Lamdba 中并没
有其他代码,就是一个简单的类型转换,因为我们可以将之简写成如下方式:

public class LamdbaDemo05 {
    public static void main(String[] args) {
        Function<Integer, String> func = String::valueOf;
        String s = func.apply(99);
        System.out.println("s = " + s);
    }
}

类似的,比如我们有一个 Consumer,如下:

Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("javaboy");

Consumer 消费一个字符串,消费的方式就是控制台打印,这种时候我们可以简写成如下方式:

Consumer<String> consumer = System.out::println;
consumer.accept("javaboy");

这就是静态方法引用。

再举个例子,用 Lamdba 写一个给参数求次幂的函数,如下:

BiFunction<Integer, Integer, Double> func = (a, b) -> Math.pow(a, b);
Double result = func.apply(3, 4);
System.out.println("result = " + result);

传入两个参数类型都是 Integer,返回的数据类型是 Double,调用 Math.pow 计算次幂。

上面这段代码我们也可以通过静态方法引用简化:

BiFunction<Integer, Integer, Double> func = Math::pow;
Double result = func.apply(3, 4);
System.out.println("result = " + result);

2、实例方法引用

方法引用也可以用在实例方法上。

Random random = new Random();
IntUnaryOperator func = i -> random.nextInt(i);
Integer r = func.applyAsInt(10);
System.out.println("r = " + r);

这段代码也可以使用方法引用,方式如下:

Random random = new Random();
IntUnaryOperator func = random::nextInt;
Integer r = func.applyAsInt(10);
System.out.println("r = " + r);

就是把类换成实例而已,其他都是一样的。

不过需要注意的是,字符串的实例稍微特殊一些,如下一个字符串排序方法:

String[] stringArray = {"Barbara", "Mary", "James"};
Arrays.sort(stringArray,String.CASE_INSENSITIVE_ORDER);
System.out.println(Arrays.toString(stringArray));

如果使用方法引用,方式如下:

String[] stringArray = {"Barbara", "Mary", "James"};
Arrays.sort(stringArray, String::compareToIgnoreCase);
System.out.println(Arrays.toString(stringArray));

这个感觉有点像静态方法引用,其实不是的,Lamdba 的第一个参数会成为调用实例方法的对象。
在实例方法引用中,如果需要指定泛型,泛型放在 :: 后面。

3、构造方法引用

例如如下方法提供一个 Cat 实例:

Supplier<Cat> supplier = () -> new Cat();
Cat cat = supplier.get();

通过方法引用,可以简写成如下形式:

Supplier<Cat> supplier = Cat::new;
Cat cat = supplier.get();

4、数组构造方法引用

例如创建一个长度为 10 的数组,如下:

IntFunction<int[]> func = (i) -> new int[i];
int[] arr = func.apply(10);
System.out.println("arr.length = " + arr.length);

使用构造方法引用,可以简写成如下方式:

IntFunction<int[]> func = int[]::new;
int[] arr = func.apply(10);
System.out.println("arr.length = " + arr.length);

二、变量引用

内部类中使用外部定义的变量,需要这个变量是一个 final 类型的,如果用了 Lamdba 表达式,这个规
则依然适用。

如下:

String s = "javaboy";
Consumer<String> consumer = s1 -> System.out.println(s1 + s);
consumer.accept("hello ");

此时虽然不用给 s 变量添加 final 标记,但是它实际上已经是 final 类型的了,如果强行修改,就会报错:

在这里插入图片描述

三、类型推断

大部分情况下,Lamdba 表达式都是可以推断出自己的类型的,个别情况下可能推断不出,比如出现方
法重载的时候,这个时候可能就需要我们类型强转了,例如如下代码:

@FunctionalInterface
interface ICalculator2{
    int add(int a, int b);
}
@FunctionalInterface
interface ICalculator3{
    int multiply(int a, int b);
}
public class LamdbaDemo06 {
    public static void main(String[] args) {
        calculator((ICalculator2) (a, b) -> a + b);
    }
    public static void calculator(ICalculator2 iCalculator) {
    }
    public static void calculator(ICalculator3 iCalculator) {
    }
}

上面的代码中定义了两个计算器 ICalculator2 和 ICalculator3,然后有一个重载的方法分别用到了
ICalculator2 和 ICalculator3,这就导致在使用 Lamdba 表达式时无法推断出到底使用哪个对象,此时
我们就需要显式的进行类型强转。

四、级联表达式

Lamdba 表达式也可以写成 N 多层,具体则看需求。

例如三个数相加,可以写成如下形式:

Function<Integer, Function<Integer, IntFunction<Integer>>> func = x -> y -> z ->
x + y + z;
Integer i = func.apply(3).apply(4).apply(5);
System.out.println("i = " + i);

这个表达式从右往左看可能容易理解。

z->x+y+z 对应的是IntFunction

z->x+y+z 整体作为返回,y 作为输入,对应的是Function>

y -> z -> x + y + z 整体作为返回,x 作为输入,对应的就是 Function,

Function>>

that's all

参考资料:

同时JDK8还有一个很重要的新特性Stream流,推荐云深i不知处的文章:
Stream流

你可能感兴趣的:(java学习,java,开发语言)