百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

为什么要使用lambda表达式?原来如此,涨知识了

yuyutoo 2025-02-26 14:26 9 浏览 0 评论

为什么要使用Lambda表达式

先看几段Java8以前经常会遇到的代码:

创建线程并启动

// 创建线程
public class Worker implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            doWork();
        }
    }
}
// 启动线程
Worker w = new Worker();
new Thread(w).start();

比较数组

// 定义一个比较器
public class LengthComparator implements Comparator {
 @Override
 public int compare(String first, String second) {
     return Integer.compare(first.length(), second.length());
 }
}
//对字符数组进行比较
Arrays.sort(words, new LengthComparator());

给按钮添加单击事件

public void onClick(Button button) {
  button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
          System.out.println("button clicked.");
      }
  });
}

对于这三段代码,我们已经司空见惯了。

但他们的问题也很突出:就是噪声太多!想实现一个数组的比较功能,至少要写5行代码,但其中只有一行代码才是我们真正关注的!

Java复杂冗余的代码实现一直被程序员所诟病,好在随着JVM平台语言Scala的兴起以及函数式编程风格的风靡,让Oracle在Java的第8个系列版本中进行了革命性的变化,推出了一系列函数式编程风格的语法特性,比如Lambda表达式以及Stream。

如果采用Lambda表达式,上面三段代码的实现将会变得极为简洁。

创建线程并启动(采用Lambda版本)

new Thread(() -> {
for (int i = 0; i < 100; i++) {
    doWork();
}
}).start();

比较数组(采用Lambda版本)

Arrays.sort(words, (first, second) -> Integer.compare(first.length(), second.length())

给按钮添加单击事件(采用Lambda版本)

button.addActionListener((event) -> System.out.println("button clicked."));

怎么样?通过Lambda表达式,代码已经变得足够简洁,让你把关注点全部都放在业务代码上。

Lambda表达式的语法

格式:(参数) -> 表达式

其中:

  1. 参数可以为0-n个。如果有多个参数,以逗号(,)分割。如果有一个参数,括号()可以省去;如果没有参数,括号()也不能省去。[这就有点不够纯粹了,比scala还是差了点!],参数前可以加类型名,但由于自动类型推导功能,可以省去。
  2. 表达式可以是一行表达式,也可以是多条语句。如果是多条语句,需要包裹在大括号{}中。
  3. 表达式不需要显示执行返回结果,它会从上下文中自动推导。 以下是一些例子:

一个参数

event -> System.out.println("button clicked.")

多个参数

(first, second) -> Integer.compare(first.length(), second.length()
复制代码

0个参数

() -> System.out.println("what are you nongshalei?")

表达式块

() -> {for (int i = 0; i < 100; i++) {    doWork();}}

函数式接口

在Java8中新增加了一个注解: [@FunctionalInterface],函数式接口。

什么是函数式接口呢?它包含了以下特征:

  • 接口中仅有一个抽象方法,但允许存在默认方法和静态方法。
  • [@FunctionalInterface]注解不是必须的,但建议最好加上,这样可以通过编译器来检查接口中是否仅存在一个抽象方法。

Lambda表达式的本质就是函数式接口的匿名实现。只是把原有的接口实现方式用一种更像函数式编程的语法表示出来。

Java8的java.util.function包已经内置了大量的函数式接口,如下所示:

函数式接口

参数类型

返回类型

方法名

描述

Supplier

T

get

产生一个类型为T的数据

Consumer

T

void

accept

消费一个类型为T的数据

BiConsumer

T,U

void

accept

消费类型为T和类型为U的数据

Function

T

R

apply

把参数类型为T的数据经过函数处理转换成类型为R的数据

BiFunction

T,U

R

apply

把参数类型为T和U的数据经过函数处理转换成类型为R的数据

UnaryOperator

T

T

apply

对类型T进行了一元操作,仍返回类型T

BinaryOperator

T,T

T

apply

对类型T进行了二元操作,仍返回类型T

Predicate

T

void

test

对类型T进行函数处理,返回布尔值

BiPredicate

T,U

void

test

对类型T和U进行函数处理,返回布尔值

从中可以看出:

  • 内置的函数式接口主要分四类:Supplier, Consumer, Function,Predicate。Operator是Function的一种特例。
  • 除了Supplier没有提供二元参数以外(这和java不支持多个返回值有关),其他三类都提供了二元入参。

以下是一个综合的例子:

public class FunctionalCase {
    public static void main(String[] args) {
        String words = "Hello, World";
        String lowerWords = changeWords(words, String::toLowerCase);
        System.out.println(lowerWords);

        String upperWords = changeWords(words, String::toUpperCase);
        System.out.println(upperWords);

        int count = wordsToInt(words, String::length);
        System.out.println(count);

        isSatisfy(words, w -> w.contains("hello"));
        String otherWords = appendWords(words, ()->{
            List allWords = Arrays.asList("+abc", "->efg");
            return allWords.get(new Random().nextInt(2));
        });
        System.out.println(otherWords);
        consumeWords(words, w -> System.out.println(w.split(",")[0]));
    }
    public static String changeWords(String words, UnaryOperator func) {
        return func.apply(words);
    }
    public static int wordsToInt(String words, Function func) {
        return func.apply(words);
    }
    public static void isSatisfy(String words, Predicate func) {
        if (func.test(words)) {
            System.out.println("test pass");
        } else {
            System.out.println("test failed.");
        }
    }
    public static String appendWords(String words, Supplier func) {
        return words + func.get();
    }
    public static void consumeWords(String words, Consumer func) {
        func.accept(words);
    }
}

如果觉得这些内置函数式接口还不够用的话,还可以自定义自己的函数式接口,以满足更多的需求。

方法引用

如果Lambda表达式已经有实现的方法了,则可以用方法引用进行简化。 方法引用的语法如下:

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法

这样前面提到的Lambda表达式:

event -> System.out.println(event)

则可以替换为:

System.out::println

另一个例子:

(x,y)->x.compareToIgnoreCase(y)

可以替换为:

String::compareToIgnoreCase

注意:方法名后面是不能带参数的! 可以写成System.out::println,但不能写成System.out::println(“hello”)

如果能获取到本实例的this参数,则可以直接用this::实例方法进行访问,对于父类指定方法,用super::实例方法进行访问。

下面是一个例子:

public class Greeter {
    public void greet() {
        String lowcaseStr = changeWords("Hello,World", this::lowercase);
        System.out.println(lowcaseStr);
    }
    public String lowercase(String word) {
        return word.toLowerCase();
    }
    public String changeWords(String words, UnaryOperator func) {
        return func.apply(words);
    }
}
class ConcurrentGreeter extends Greeter {
    public void greet() {
        Thread thread = new Thread(super::greet);
        thread.start();
    }
    public static void main(String[] args) {
        new ConcurrentGreeter().greet();
    }
}

构造器引用

构造器引用和方法引用类似,只不过函数接口返回实例对象或者数组。 构造器引用的语法如下:

  • 类::new
  • 数组::new

举个例子:

List labels = Arrays.asList("button1", "button2");
Stream

其中的labels.stream().map(Button::new)相当于 labels.stream().map(label->new Button(label))

再看个数组类型的构造器引用的例子:

Button[] buttons = stream.toArray(Button[]::new);

把Stream直接转成了数组类型,这里用Button[]::new来标示数组类型。

变量作用域

先看一段代码:

public void repeatMsg(String text, int count) {
    Runnable r = () -> {
        for (int i = 0; i < count; i++) {
            System.out.println(text);
            Thread.yield();
        }
    };
}

一个lambda表达式一般由以下三部分组成:

  • 参数
  • 表达式
  • 自由变量

参数和表达式好理解。那自由变量是什么呢? 它就是在lambda表达式中引用的外部变量,比如上例中的text和count变量。

如果熟悉函数式编程的同学会发现,Lambda表达式其实就是”闭包”(closure)。只是Java8并未叫这个名字。 对于自由变量,如果Lambda表达式需要引用,是不允许发生修改的。

其实在Java的匿名内部类中,如果要引用外部变量,变量是需要声明为final的,虽然Lambda表达式的自由变量不用强制声明成final,但同样也是不允许修改的。

比如下面的代码:

public void repeatMsg(String text, int count) {
    Runnable r = () -> {
        while (count > 0) {
            count--;  // 错误,不能修改外部变量的值
            System.out.println(text);
        }
    };
}

另外,Lambda表达式中不允许声明一个和局部变量同名的参数或者局部变量。 比如下面的代码:

Path first = Paths.get("/usr/bin");
Comparator comp = (first, second) -> Integer.compare(first.length(), second.length());
// 错误,变量first已经被定义

接口中的默认方法

先说说为什么要在Java8接口中新增默认方法吧。

比如Collection接口的设计人员针对集合的遍历新增加了一个forEach()方法,用它可以更简洁的遍历集合。 比如:

list.forEach(System.out::println());

但如果在接口中新增方法,按照传统的方法,Collection接口的自定义实现类都要实现forEach()方法,这对广大已有实现来说是无法接受的。

于是Java8的设计人员就想出了这个办法:在接口中新增加一个方法类型,叫默认方法,可以提供默认的方法实现,这样实现类如果不实现方法的话,可以默认使用默认方法中的实现。

一个使用例子:

public interface Person {
    long getId();

    default String getName() {
        return "jack";
    }
}

默认方法的加入,可以替代之前经典的接口和抽象类的设计方式,统一把抽象方法和默认实现都放在一个接口中定义。这估计也是从Scala的Trait偷师来的技能吧。

接口中的静态方法

除了默认方法,Java8还支持在接口中定义静态方法以及实现。

比如Java8之前,对于Path接口,一般都会定义一个Paths的工具类,通过静态方法实现接口的辅助方法。

接口中有了静态方法就好办了, 统一在一个接口中搞定!虽然这看上去破坏了接口原有的设计思想。

public interface Path{
  public static Path get(String first, String... more) {
    return FileSystem.getDefault().getPath(first, more);
  }
}

这样Paths类就没什么意义了~

小结

使用Lambda表达式后可以大幅减少冗余的模板式代码,使把更多注意力放在业务逻辑上,而不是复制一堆重复代码, 除非你在一个用代码行数来衡量工作量的公司,你觉得呢?

相关推荐

当 Linux 根分区 (/) 已满时如何释放空间?

根分区(/)是Linux文件系统的核心,包含操作系统核心文件、配置文件、日志文件、缓存和用户数据等。当根分区满载时,系统可能出现无法写入新文件、应用程序崩溃甚至无法启动的情况。常见原因包括:...

玩转 Linux 之:磁盘分区、挂载知多少?

今天来聊聊linux下磁盘分区、挂载的问题,篇幅所限,不会聊的太底层,纯当科普!!1、Linux分区简介1.1主分区vs扩展分区硬盘分区表中最多能存储四个分区,但我们实际使用时一般只分为两...

Linux 文件搜索神器 find 实战详解,建议收藏

在Linux系统使用中,作为一个管理员,我希望能查找系统中所有的大小超过200M文件,查看近7天系统中哪些文件被修改过,找出所有子目录中的可执行文件,这些任务需求...

Linux 操作系统磁盘操作(linux 磁盘命令)

一、文档介绍本文档描述Linux操作系统下多种场景下的磁盘操作情况。二、名词解释...

Win10新版19603推送:一键清理磁盘空间、首次集成Linux文件管理器

继上周四的Build19592后,微软今晨面向快速通道的Insider会员推送Windows10新预览版,操作系统版本号Build19603。除了一些常规修复,本次更新还带了不少新功能,一起来了...

Android 16允许Linux终端使用手机全部存储空间

IT之家4月20日消息,谷歌Pixel手机正朝着成为强大便携式计算设备的目标迈进。2025年3月的更新中,Linux终端应用的推出为这一转变奠定了重要基础。该应用允许兼容的安卓设备...

Linux 系统管理大容量磁盘(2TB+)操作指南

对于容量超过2TB的磁盘,传统MBR分区表的32位寻址机制存在限制(最大支持2.2TB)。需采用GPT(GUIDPartitionTable)分区方案,其支持64位寻址,理论上限为9.4ZB(9....

Linux 服务器上查看磁盘类型的方法

方法1:使用lsblk命令lsblk输出说明:TYPE列显示设备类型,如disk(物理磁盘)、part(分区)、rom(只读存储)等。...

ESXI7虚机上的Ubuntu Linux 22.04 LVM空间扩容操作记录

本人在实际的使用中经常遇到Vmware上安装的Linux虚机的LVM扩容情况,最终实现lv的扩容,大多数情况因为虚机都是有备用或者可停机的情况,一般情况下通过添加一块物理盘再加入vg,然后扩容lv来实...

5.4K Star很容易!Windows读取Linux磁盘格式工具

[开源日记],分享10k+Star的优质开源项目...

Linux 文件系统监控:用脚本自动化磁盘空间管理

在Linux系统中,文件系统监控是一项非常重要的任务,它可以帮助我们及时发现磁盘空间不足的问题,避免因磁盘满而导致的系统服务不可用。通过编写脚本自动化磁盘空间管理,我们可以更加高效地处理这一问题。下面...

Linux磁盘管理LVM实战(linux实验磁盘管理)

LVM(逻辑卷管理器,LogicalVolumeManager)是一种在Linux系统中用于灵活管理磁盘空间的技术,通过将物理磁盘抽象为逻辑卷,实现动态调整存储容量、跨磁盘扩展等功能。本章节...

Linux查看文件大小:`ls`和`du`为何结果不同?一文讲透原理!

Linux查看文件大小:ls和du为何结果不同?一文讲透原理!在Linux运维中,查看文件大小是日常高频操作。但你是否遇到过以下困惑?...

使用 df 命令检查服务器磁盘满了,但用 du 命令发现实际小于磁盘容量

在Linux系统中,管理员或开发者经常会遇到一个令人困惑的问题:使用...

Linux磁盘爆满紧急救援指南:5步清理释放50GB+小白也能轻松搞定

“服务器卡死?网站崩溃?当Linux系统弹出‘Nospaceleft’的红色警报,别慌!本文手把手教你从‘删库到跑路’进阶为‘磁盘清理大师’,5个关键步骤+30条救命命令,快速释放磁盘空间,拯救你...

取消回复欢迎 发表评论: