在本文中,我们提供了全面的Lambda Expressions Java教程。
1. Lambda Expressions Java教程–简介
Lambda表达式被认为是Java 8中引入的最好的功能之一。Lambda表达式被认为是Java进入函数式编程世界的第一步 。 可以将其视为无需类即可创建的函数。 它也可以像参数一样传递,并且可以在需要时和根据需要执行。 Java Lambda表达式是匿名函数的简洁表示,可以将其传递。 具有单个功能的匿名类通过额外的语法呈现出笨拙的外观。 这些表述旨在消除这种混乱。
如前所述,Java Lambda表达式是无名函数 ,可以作为常量值进行传递。 这意味着它们可以存在于可能存在任何其他常数值的任何位置,但是通常作为参数写入某些其他函数。 考虑一个典型的例子,我们可以将比较函数传递给泛型排序函数,而不是麻烦地定义一个整个过程(并引起词法不连续和名称空间污染)来描述这种比较,我们只需传递一个lambda表达式描述了比较。 让我们看一下Lambda表达式的一些属性 :
- 匿名:它仍然可以称为匿名,因为它没有明确的名称。
- 简洁:正如前面提到的匿名类的情况,与匿名类相比,我们用Lambdas编写的代码要少得多。
- 函数:Lambda更像是函数而不是方法。 这是因为方法属于类,而Lambda不属于。 但是就像方法一样,Lambda接受参数列表,具有主体并且还可以引发异常。
- 可以传递:Lambda可以传递给其他函数,就像普通参数一样。
为了消除由于我们上面提到的观点而引起的任何误解,lambda不会添加引入之前的更多功能。 它只是改善了我们编写代码的方式,并减少了很多样板代码。 该样板代码甚至与我们用来通过基础操作系统的多核性质进行代码识别的系统级编程有关。 让我们看一下这种简单的语法糖如何使我们的工作在并行性,代码简洁性和紧凑性方面更容易。
2.编写Lambda表达式
在本节中,我们将看到Java Lambda表达式如何减少执行一些简单操作所需编写的代码行。 例如,我们将比较代码行数以构成一个Comparator函数。 为了进行比较,我们将在此处创建一个简单的POJO
类,即Student
类,其中包含Student ID(作为Long
和name作为String
参数:
学生.java
public class Student { private Long id; private String name; // standard setters and getters }
比较我们在应用程序中定义的POJO
对象是一种非常通用的编程实践。 如果我们要比较两个Student
类对象,则可以使Comparator像这样:
匿名类的比较器
Comparator<Student> byId = new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { return s1.getId().compareTo(s2.getId()); } };
这是一个作为Anonymous类的简单Comparator实现,但是当使用Lambda完成时,我们会发现相同的实现非常精确和干净。 让我们在这里看到使用Lambda表达式完成的相同任务:
pom.xml
Comparator<Student> byId = (s1, s2) -> s1.getId().compareTo(s2.getId());
Lambda表达式上方也可以称为块Lambda表达式,因为它由>符号右侧的单个代码块组成。 它变得更加简洁小巧,这听起来很神奇,请参见以下代码片段:
Lambda的简洁实现
Comparator<Student> byId = Comparator.comparing(Student::getId);
这是建立比较器的好方法,而且也很简单。 对于上面我们进行的Block Lambda表达式,让我们将其分为几部分以更好地理解:
- Lambda Expression以在此情况下传递给函数Comparator的参数列表开头
- 箭头符号将Lambda Expression参数与Lambda主体分开
- 主体清楚地将两个学生对象及其
id
进行比较,该表达式定义了Lambda返回值
需要注意的是,编译后的代码(即匿名类版本和Lambda表达式版本的字节码)将完全相同,因为Lambda表达式仅仅是使代码清晰的语法。 尽管使用Lambda表达式有时可能会使代码的可读性降低。
3. Lambda表达式与匿名类
我们使用Lambda表达式编写的代码也可以使用Anonymous类编写,其实现方式与Lambda Expressions完全相同。 区别在于Lambda代码的简洁性。
作为比较示例,让我们构造一个类和一个将Runnable
作为输入的方法:
可运行类
public class RunnableInstance { public static void doSomething(Runnable runnable){ runnable.run(); } }
当我们使用Anonymous类制作Runnable时,其外观如下所示:
可通过匿名类运行
Runnable runnable = new Runnable() { @Override public void run() { System.out.print( "Anonymous class implementation." ); } }; doSomething(runnable);
让我们尝试将上面的代码转换为Lambda表达式,看看如何得到干净的东西:
可与Lambda一起运行
Runnable runnable = () -> System.out.print( "Lambda Expression." ); doSomething(runnable);
如果我们不想多次使用可运行的实现,我们甚至可以避免进行引用:
简洁的Lambda Runnable
doSomething(() -> System.out.print( "Lambda Expression." ));
4.使用Lambda表达式进行并行编程
每当我们谈论线程时,我们大多数人都会退后一步,考虑是否真的需要在我们的应用程序中实现线程以支持并行性,因为并行性本质上微不足道且难以管理。 当我们有一个项目集合时,我们实现了一个lambda,如:
并行编程
collection.map { // my lambda }
在这里,集合本身能够与提供的Lambda实现并行性,而不必自己实现线程。 这意味着,在多核环境中,Lambda可以在集合上进行流式传输时利用多个核。 就像我们考虑一个简单的例子一样:
Lambda与并行流
List<String> names = students.stream() .map(s -> s.getName().toUpperCase()) .collect(Collectors.toList());
map函数可以在多核环境中并行运行,以一次处理多个对象,而无需我们做任何事情。 为此,仅需要执行此程序的操作系统必须是多核。 一旦满足此条件,我们可以确保可以在给定语句中并行执行的任何操作都将自动完成。
5.收藏和流
Collections框架是Java中最常用的Framework API之一。 集合允许我们将相似的对象收集到可以针对特定目的进行优化的数据结构中。 前面的所有示例都需要对象的集合,因此,假设我们有一个Student类型的对象的集合,就像我们前面定义的那样:
学生集合
List students = getStudentObjectCollection();
我们从添加到Collection接口的新方法stream()
开始。 由于所有集合都“扩展”集合,因此所有Java集合都继承了此方法:
学生流
List students = getStudentObjectCollection(); Stream stream = students.stream(); // a stream of student objects
尽管看起来很像,但Stream接口不是另一种常规的集合类型。 我们可以将Stream视为“数据流”抽象,它使我们能够转换或操纵其包含的数据。 与我们在Java中研究过的其他集合不同,Stream不允许我们直接访问其包含的元素。 尽管如果您想访问元素,我们总是可以将流转换为Java中的集合之一并实现我们的目的。
出于演示目的,我们将看到如果我们必须计算students
集合中有多少个奇数ID对象,我们的代码将是什么样子。 首先,让我们看看如何在不使用流的情况下完成此操作:
计数奇数
long count = 0 ; List students = getStudentObjectCollection(); for (Student s : students) { if (s.getId() % 2 == 1 ) { count++; } }
使用for
循环,我们创建了一个计数器,每次在学生列表中遇到奇数ID时,该计数器都会递增。 对于一个非常简单的任务,我们已经数百次编写了这种类型的代码,它跨越多行。
我们也可以在一行中使用Stream编写完全相同的代码:
使用流
List students = getStudentObjectCollection(); long count = students.stream().filter(student -> student.getId() % 2 == 1 ).count();
这看起来比以前的for
循环方法干净整洁吗? 一切都始于调用stream()
方法,该方法将给定的集合转换为Stream,其他所有调用都链接在一起,因为Stream接口中的大多数方法都是在考虑Builder模式的情况下设计的 。 对于那些不习惯使用这种方法进行链接的用户,可能更容易这样可视化:
可视化流
List students = getStudentObjectCollection(); Stream stream = students.stream(); stream = stream.filter(student -> student.getId() % 2 == 1 ); long count = stream.count();
让我们将注意力集中在我们使用的Stream的两种方法中, filter()
和count()
。
filter()
方法采用要过滤集合的条件,该条件由带一个参数并返回布尔值的lambda表达式表示:
Lambda条件
student -> student.getId() % 2 == 1
并非偶然,用于表示该表达式的功能接口filter()
方法的参数filter()
是谓词接口。 它只有一个抽象方法boolean test(T t)
:
功能介面
@FunctionalInterface public interface Predicate { boolean test(T t); // non-abstract methods here }
参数化类型T
表示流中元素的类型,即Student对象。 过滤之后,剩下的就是调用count()
方法。 没什么大不了的,它只是计算过滤发生后我们流中还剩下多少个对象(除了过滤之外,我们还可以有更多的东西)。 count()
方法被视为“终端操作”,在调用该方法后,该流被称为“已消耗”且无法再使用。
6. Lambda表达式的缺点
尽管带有Lambda Expressions的代码看起来非常简洁,但是Lambdas也有一些缺点。 让我们在这里研究其中的一些:
- 无法处理检查的异常 :引发检查的异常的任何代码都应包装在try-catch语句中。 但是,即使我们这样做,也不总是总是清楚抛出的异常发生了什么。
- 性能问题 :由于JIT不能始终将
forEach()
+ lambda优化到与普通循环相同的程度,因此Lambda可以在很小程度上影响性能。 - 调试挑战 :显然,使用Lambdas时,代码并不总是那么简洁。 这使得在代码和可读性方面发生的异常的堆栈跟踪变得有些困难。
尽管Lambda有一些缺点,但是当您编写简洁的代码时,它们仍然是一个很好的伴侣。
7.结论
Java Lambda表达式在所有LISP,Perl,Python以及最新版本的C ++,Objective C,C#和Java 8中都出现(具有不同的语法),但值得注意的是,即使它可以处理传递的函数(或一些借口)作为参数。 它们是具有特定语义的语法元素,并且这些语义对运行时的要求比C所设计的要高。
在本课中 ,我们可以阅读有关Lambda表达式的更多信息,它与功能接口有很深的联系,还演示了将并行流与Lambda表达式配合使用的性能比较,并加深了对Lambda表达式如何与功能接口一起使用并可以在简单语句中使用的理解。利用多核操作系统提供的并行性,而无需了解幕后工作的API。
上次更新时间为2020年2月17日
翻译自: https://www.javacodegeeks.com/lambda-expressions-java-tutorial.html