java之Lambda表达式

写在前面

lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。

函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。

相对而言,面向对象过分强调“必须通过对象的形式来做事情”。

函数式思想则尽量忽略面向对象的复杂语法——强调做什么而不是以什么形式做

面向对象的思想:

  • 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.

函数式编程思想:

  • 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

举例

以一个简单的实现Runnable接口的线程类为例:

面向对象方式

/**
 * 标准方式(面向对象)创建线程
 * @author layman
 */
public class Demo01  {
    public static void main(String[] args) {
        //标准方式创建线程
        Demo01Runnable run = new Demo01Runnable();
        Thread t = new Thread(run);
        t.start();
    }
}
class Demo01Runnable implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--线程任务开启");
    }
}

匿名内部类方式

/**
 * 匿名内部类方式创建线程
 * @author layman
 */
public class Demo01  {
    public static void main(String[] args) {
        //匿名内部类方式创建线程
        Runnable run = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"--线程任务开启");
            }
        };
        new Thread(run ).start();
    }
}

可以看到,通过匿名内部类,可以大大的简化代码。

可以看到这个匿名内部类仅被使用了一次,因此无需使用参数接收这个匿名内部类,而是直接把它作为参数传递。

本着这样的思考,我们可以继续精简代码,最终如下:

/**
 * 匿名内部类方式创建线程(精简)
 * @author layman
 */
public class Demo01  {
    public static void main(String[] args) {
        //匿名内部类方式创建线程(精简)
        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"--线程任务开启");
            }
        }).start();
    }
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

匿名内部类的好处与弊端

一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法——确实太复杂了!

代码分析

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,只有其中的抽象run方法是核心。
  • 为了指定run的方法体,必须指定Runnable接口的实现类。
  • 为了省去定义一个Runnable接口实现类的麻烦,不得不使用匿名内部类
  • 必须覆盖重写抽象run方法。

然鹅,实际上,似乎只有方法体才是关键

编程思想转换

做什么,而不是怎么做

  • 我们真的希望创建一个匿名内部类对象吗?
  • 不。我们只是为了做这件事情而不得不创建一个对象。
  • 我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递代码——这才是我们真正的目的。

创建对象只是受限于面向对象语法而不得不采取的一种手段方式。

那,有没有更加简单的办法?

如果我们将关注点从怎么做回归到做什么的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

开启新知识的大门(Lambda表达式)

借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

/**
 * @author layman
 */
public class Demo01Lambda {
    public static void main(String[] args) {
        //Lambda表达式
        new Thread(() -> System.out.println(Thread.currentThread().getName()+"--线程任务开启")).start(); 
    }
}

这段代码和刚才的执行效果是完全一样的,可以在jdk1.8或更高的编译级别下通过。

代码语义分析

() -> System.out.println(Thread.currentThread().getName()+"--线程任务开启")
  • 前面的一对小括号()是即run方法的参数,上面的表达式表示没有参数。
  • 中间的一个箭头->代表将前面的参数传递给后面的代码;
  • 后面的输出语句是业务逻辑代码。

从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有不得不创建接口对象的束缚,不再有抽象方法覆盖重写的负担,就是这么简单!

lambda 表达式形式

参数箭头(->) 以及一个表达式

(参数类型 参数名称) -> { 代码语句 }

如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在 {}中,并包含显式的 return语句。例如:

(String first, String second) -> {
	if (first.length() < second.length()) return -1;
	else if (first.length() > second.length()) return 1;
	else return 0; 
}

即使 lambda 表达式没有参数, 仍然要提供空括号,就像无参数方法一样:

() -> { for (int i = 0;i > 100;i++ ) System.out.println(i); }

如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型。例如:

// 等同于(String first, String second)
Comparator<String> comp= (first, second) -> first.length() - second.length();

如果方法只有一 参数, 而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:

// 不是(event) -> . . . 或者 (ActionEvent event) -> .
ActionListener listener = event -> System.out.println("The time is " + new Date()"); 

无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得
出。例如,下面的表达式

(String first, String second) -> first.length() - second.length()

可以在需要 int 类型结果的上下文中使用。

注意:

如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值,
这是不合法的。例如:

int x)-> { if(x >= 0) return 1; } 

函数式接口

对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达
式。这种接口称为函数式接口 ( functional interface )。

在这里插入图片描述

简单代码示例

下面给出几个简单的代码示例

简单练习A (无参无返回)

定义一个Hero类,定义一个抽象方法hitMonster(),进行调用。

代码如下:

/**
 * @author layman
 */
public class Demo02 {
    public static void main(String[] args) {
        //匿名内部类方法调用
        showTime(new Hero() {
            @Override
            public void hitMonster() {
                System.out.println("绿巨人在揍钢铁侠");
            }
        });
        
        //使用lambda表达式调用
        showTime(()->{
            System.out.println("葫芦娃大战奥特曼");
            System.out.println("孙悟空三打白骨精");
        });
    }

    
    public static void showTime(Hero hero){
        hero.hitMonster();
    }
}
//定义一个接口
interface Hero{
    // 定义一个抽象方法
    void hitMonster();
}
简单练习B (有参无返回)
需求:
    使用数组存储多个Monster对象
    对数组中的Monster对象使用Arrays的sort方法通过年龄进行降序排序

传统写法:

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;

/**
 * @author layman
 */
public class Demo03 {
    public static void main(String[] args) {
        //构建怪物数组
        Monster[] monsters = {
                new Monster("虚空怪物",300),
                new Monster("触手蟑螂",45),
                new Monster("铁头鱼",458)};

        System.out.println("排序前:" + Arrays.toString(monsters));
        //使用匿名内部类定义一个比较器
        Comparator comparator = new Comparator<Monster>(){
            @Override
            public int compare(Monster o1, Monster o2) {
                //降序
                return o2.getAge() - o1.getAge();
            }
        };
        //调用方法进行排序
        Arrays.sort(monsters,comparator);
        System.out.println("排序后:" + Arrays.toString(monsters));
    }
}
@Data
@AllArgsConstructor
class Monster{
    private String name;
    private Integer age;
}

这种做法在面向对象的思想中,似乎是理所当然的。其中Comparator接口的实例(使用了匿名内部类)实现了按照年龄从大到小的排序规则。

代码分析

下面我们来搞清楚上述代码真正要做什么事情。

  • Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键
  • 为了指定compare的方法体,不得不需要Comparator接口的实现类。
  • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类。
  • 必须覆盖重写抽象compare方法,方法名称、方法参数、方法返回值不得不再写一遍,且不能写错。

实际上,只有参数和方法体才是关键。


使用Lambda表达式优化代码:

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Arrays;

/**
 * @author layman
 */
public class Demo03 {
    public static void main(String[] args) {
        //构建怪物数组
        Monster[] monsters = {
                new Monster("虚空怪物",300),
                new Monster("触手蟑螂",45),
                new Monster("铁头鱼",458)};

        System.out.println("排序前:" + Arrays.toString(monsters));

        //使用Lambda表达式编写函数接口
        Arrays.sort(monsters,(o1,o2)->{
            return o2.getAge() - o1.getAge();
        });
        System.out.println("排序后:" + Arrays.toString(monsters));
    }
}
@Data
@AllArgsConstructor
class Monster{
    private String name;
    private Integer age;
}
简单练习C (有参有返回)

给定一个计算器Calculator接口,内含抽象方法getSum可以将两个int数字相加得到和:

代码如下:

/**
 * @author layman
 * @date 2021/3/2
 */
public class Demo04 {
    public static void main(String[] args) {
        //匿名内部类写法
        int sum = getSum(10, 20, new Calculator() {
            @Override
            public int getSum(int a, int b) {
                return a + b;
            }
        });
        System.out.println("求和sum为:" + sum);

        //标准lambda表达式写法
        int sum1 = getSum(30, 40, (int a, int b) -> {
                return a + b;
            }
        );
        System.out.println("求和sum1为:" + sum1);

        //lambda表达式省略写法
        int sum2 = getSum(50, 60, (a, b) ->
                     a + b
        );
        System.out.println("求和sum2为:" + sum2);
    }

    public static int getSum(int a,int b,Calculator calculator){
        int sum = calculator.getSum(a,b);
        return sum;
    }
}
interface Calculator {
    int getSum(int a,int b);
}

Lambda省略格式

原则:可推导即可省略

省略规则

在Lambda标准格式的基础上,使用省略写法的规则为:

  • 如果参数类型可以由上下文推导得出,那么小括号内的参数类型可以省略不写。
  • 如果小括号内有且仅有一个参数,那么小括号可以省略。
  • 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的Runnable、Comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值