一、Lambda表达式
1.1 相关背景
Lambda表达式是Java 8 引入的一个重要特性,它是函数式编程在Java中的一种体现。
在Java之前的版本中,Java主要采用面向对象的编程风格,而Lambda表达式的引入使得具备了函数编程的能力。
1.2 函数式编程
函数式编程是一种编程范式,它将计算过程视为函数应用的连续组合。函数式编程强调使用纯函数(Pure Function),避免使用可变状态和副作用,倡导将计算过程抽象为函数,便于代码的理解、测试和并行化。
Lambda表达式允许将函数作为方法的参数或者将代码块作为数据进行传递。它的引入主要解决以下几个问题:
- 匿名内部类的冗余代码:在Java 8之前版本中,为了实现函数的传递,常常需要使用匿名内部类来定义一个函数接口的实现,这导致代码冗长、可读性差。Lambda表达式的引入简化了匿名内部类的语法,让代码更加简洁明了;
- 函数式编程的支持:函数式编程强调将函数作为第一类对象进行传递和操作。Lambda表达式提供了一种便捷的语法形式,使得函数可以作为参数传递给方法或者作为返回值返回;
- 并发编程的支持:函数式编程中的纯函数天然具备无副作用的特性,这使得在并行编程中更容易实现可靠的多线程和并行处理。
Lambda表达式的引入使得Java在并行编程方面具备了更好的支持(结合Stream流)。
1.3 匿名内部类和Lambda表达式
匿名内部类和Lambda表达式都是在Java中用于实现函数式编程的机制,它们可以用来传递行为(函数)作为参数或返回值。
匿名内部类:
匿名内部类是在Java早期引入的一种机制,用于创建一个没有命名的、实现了某个接口或抽象类的类的实例。通过匿名内部类,可以在使用某个接口或抽象类的地方直接定义一个实现接口或者抽象类的实例。匿名内部类通常通过创建一个子类来实现,并且在实例化的同时定义实现的方法。
例如,以下是使用匿名内部类实现Runnable
接口的例子:
Thread thread = new Thread(new Runnable() {public void run() {System.out.println("Hello from anonymous inner class");}
});
thread.start();
Lambda表达式:
Lambda表达式是Java 8中引入的一种更简洁、更直观的方式来表示函数式接口的实例,使得可以以更紧凑的方式传递行为。使用Lambda表达式,可以直接定义一个函数式接口的实例,而无需创建匿名内部类。
以下是使用Lambda表达式实现相同功能的例子:
Thread thread = new Thread(() -> {System.out.println("Hello from Lambda expression");
});
thread.start();
Lambda表达式的引入让Java代码更具表达力和简洁性,提高了代码的可读性和可维护性。
思考:既然Lambda表达式这么好,那么可以直接替代匿名内部类吗?
当然不行,因为Lambda表达式只能用于实现只有一个抽象方法的接口(即函数式接口),如果一个接口或抽象类有多个抽象方法,那么它就不是一个函数式接口,因此不能直接使用Lambda表达式来实现,需要使用传统的匿名类或具体的类来实现。
小结:
匿名内部类和Lambda表达式都可以用来实现函数式编程,传递行为作为参数或返回值。匿名内部类是一种更早引入的机制,Lambda表达式是Java 8引入的更简洁、更直观的方式。根据具体的场景和需求,可以选择使用匿名内部类或Lambda表达式来实现相应的功能。
Lambda表达式在Java中又称为闭包或匿名函数。
二、Lambda表达式的使用
2.1 基本语法
Lambda表达式使用箭头->
将参数和函数体分隔开,参数可以有零个或多个,函数体可以是一个表达式或一段代码块。如果主体是一个表达式,它将直接返回该表达式的结果,如果主体是一个代码块,它将按照常规的Java语法执行,并且您可能需要使用return语句来返回值。
下面是一些Lambda表达式的示例:
- 无参数的Lambda表达式:
() -> System.out.println("Hello, Lambda!");
- 带有参数的Lambda表达式:
(x, y) -> System.out.println(x + y);
- 带有多行代码的Lambda表达式:
(x, y) -> {int sum = x + y;System.out.println("Sum: " + sum);return sum;
}
2.2 使用限制与说明
1、Lambda表达式仅能放入如下代码:预定义使用了@Functional
注释的函数式接口,自带一个抽象函数的方法或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为Lambda表达式的目标类型,可以用作返回类型或Lambda目标代码的参数。
例如:若一个方法接收Runnable
、Comparable
或者Callable
接(都有单个抽象方法),可以传入Lambda表达式。类似的,如果一个方法接受声明于java.util.function
包内的接口,例如Predicate
、Function
、Consumer
或 Supplier
,那么可以向其传Lambda表达式。
在Lambda表达式中,根据上下文的要求可以自动匹配函数式接口的抽象方法,并创建接口的实例。
2、Lambda表达式可以使用方法引用,仅当该方法不能修改Lambda表达式提供的参数。
本例中的Lambda表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用:
list.forEach(n -> System.out.println(n));
list.forEach(System.out::println); // 使用方法引用
然而,若对参数有任何修改,则不能使用方法引用,则需键入完整的Lambda表达式,如下示例:
list.forEach((String s) -> System.out.println("*" + s + "*"));
事实上可以省略这里的Lambda参数类型的声明,编译器可以从列表的类属性推测出来。
3、Lambda表达式内部可以使用静态、非静态和局部变量,这称为Lambda内部的变量捕获。
4、Lambda方法在编译器内部被翻译成私有方法,并派发invokedynamic
字节码指令来进行调用。可以使用JDK中的javap
工具来反编译class文件。使用java -p
或javap -c -v
命令来看Lambda表达式生成的字节码。大致应该长这样:
private static java.lang.Object lambda$0(java.lang.String);
5、Lambda表达式有个限制,那就是只能引用final
或final
局部变量,这就是说不能在Lambda内部修改定义在域外的变量。
List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { factor++; });
Compile time error : “local variables referenced from a lambda expression must be final or effectively final” 另外,只是访问它而不作修改是可以的,如下所示:
List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); });
2.3 变量捕获
匿名内部类的变量捕获:
在Java中,匿名内部类可以捕获外部变量,即在匿名内部类中引用并访问外部作用于的变量。这种行为称为变量捕获(Variable Capturing)。
在匿名内部类中,可以捕获以下类型的变量:
- 实例变量(Instance Variables):如果匿名内部类位于一个实例方法中,它可以捕获并访问该实例的实例变量。
- 静态变量(Static Variables):匿名内部类可以捕获并访问包含它的类的静态变量。
- 方法参数(Method Parameters):匿名内部类可以捕获并访问包含它的方法的参数。
- 本地变量(Local Variables):匿名内部类可以捕获并访问声明为
final
的本地变量。从Java 8开始,final
关键字可以省了,但该变量实际上必须是最终的(不可修改的)。
当匿名内部类捕获变量时,它们实际上是在生成的字节码中创建了一个对该变量的副本。这意味着即使在外部作用域中的变量发生改变,匿名内部类中捕获的变量仍然保持其最初值。
以下示例展示了匿名内部类捕获外部变量的用法:
public class OuterClass {private int instanceVariable = 10;private static int staticVariable = 20;public void method() {final int localVar = 30; // 或者直接使用 Java 8+ 的隐式 finalRunnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("Instance variable: " + instanceVariable);System.out.println("Static variable: " + staticVariable);System.out.println("Local variable: " + localVar);}};runnable.run();}
}
在上面示例中,method方法中创建了一个匿名内部类的实例,并在该类的run方法中访问了外部的实例变量、静态变量和本地变量localVar
,这些变量都被匿名内部类捕获并访问。
需要注意的是,如果在匿名内部类中捕获非final
或非最终的变量,或在Java 8之前的版本中捕获非final
修饰的变量,编译器将报错。从Java 8开始,可以捕获对final
变量的隐式引用,无需显示地将其声明为final
。
Lambda表达式的变量捕获:
在Lambda表达式中,同样可以捕获外部作用域的变量,Lambda表达式可以捕获以下类型的变量:
- 实例变量(Instance Variables):Lambda表达式可以捕获并访问包含它的实例的实例变量。
- 静态变量(Static Variables):Lambda表达式可以捕获并访问包含它的类的静态变量。
- 方法参数(Method Parameters):Lambda表达式可以捕获并访问包含它的方法的参数。
- 本地变量(Local Variables):Lambda表达式可以捕获并访问声明为
final
的本地变量。从Java 8开始,final
关键字可以省略,但该变量实际上必须是最终的(即不可修改)。
与匿名内部类不同,Lambda表达式不会创建对变量的副本,而是直接访问变量本身。这意味着在Lambda表达式中捕获的变量在外部作用域中发生的改变也会在Lambda表达式中反映出来。
以下是一个示例,展示了Lambda表达式捕获外部变量的用法:
public class LambdaVariableCapture {private int instanceVariable = 10;private static int staticVariable = 20;public void method() {int localVar = 30;// Lambda表达式捕获外部变量Runnable runnable = () -> {System.out.println("Instance variable: " + instanceVariable);System.out.println("Static variable: " + staticVariable);System.out.println("Local variable: " + localVar);};runnable.run();}
}
在上面示例中,method方法中创建了一个Lambda表达式,捕获了外部的实例变量、静态变量和本地变量localVar
,Lambda表达式中直接使用这些变量,无需声明。
需要注意的是,与匿名内部类一样,如果在Lambda表达式中捕获非final
或非最终的变量,或者在Java8之前的版本中捕获非final
修饰的变量,编译器将报错。从Java 8开始,可以隐式捕获对final
变量的引用,无需显示地将其声明为final
。
另外,Lambda表达式还有一个特点是它们可以访问外部作用域的变量,但不能修改它们的值。这是因为Lambda表达式中的变量是相当于隐式声明的fianl
变量,一旦捕获,就不允许修改。
2.4 Lambda表达式与Stream流
Lambda表达式和Stream API是Java 8引入的两个重要特性,它们之间有着密切的关系,特别是在简化代码和提升代码可读性方面。
- Lambda表达式提供了一种简洁的语法来定义匿名函数(即没有名称的函数),它可以被传递作为参数或者用于简化代码,特别是在需要实现接口的地方(如
Runnable
、Comparator
等)。 - Stream API提供了一种高效且易于使用的方式来处理数据集合。它支持一系列的操作(如过滤、映射、规约等)来处理数据。Stream API的操作可以分为两类:中间操作(如
filter
、map
)和终端操作(如forEach
、collect
)。
二者的关系:
Lambda表达式经常与Stream API一起使用,因为Stream中的许多操作(如filter
、map
等)都需要函数式接口作为参数,而Lambda表达式正好可以简洁地实现这些接口。通过使用Lambda表达式,Stream API操作变得更加简洁和直观。
例如:下面的例子中在Stream流API也使用了Lambda表达式:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> filteredNames = names.stream().filter(name -> name.startsWith("A")) // Lambda表达式.map(String::toUpperCase) // 方法引用(也是Lambda的一种形式).collect(Collectors.toList());
总的来说,Lambda表达式和Stream API结合使用,使得Java代码在处理集合数据时变得更加简洁、易读和高效。
二者的区别:
疑问1:Java中Iterable、Map等接口中的forEach等方法是Stream流的API吗?
Iterable、Map等接口中的forEach、removeIf等方法并不是Stream API的一部分,它们是Java 8在这些接口中引入的默认方法(default methods),这些方法直接在集合上操作,不需要创建流,虽然这些方法和Stream API有一些相似之处,但它们是独立的。
虽然Stream API也有forEach方法,但它是流的终端操作之一,Stream API提供了一组独立的操作方法,通过创建流来处理集合数据,支持更复杂和灵活的数据处理。
疑问2:Stream流与Collection、Map接口的API在使用场景上有哪些区分,分别适用哪些场景?
Stream API使用场景:
- 复杂的数据处理管道:Stream API提供了一种声明式的方式来定义处理逻辑,可以对数据进行多步处理(如过滤、映射、排序、聚合等);
- 延迟执行:Stream API的中间操作(如filter、map)是延迟执行的,只有在终端操作(如collect、forEach)被调用时才会执行。这种特性可以优化性能和资源使用。
- 并行处理:Stream API支持并行流处理,可以轻松实现并行计算,提高性能。
Iterable、Collection、Map接口的forEach等API使用场景:
- 简单迭代:当只需要对集合中的每个元素执行一个操作,而不需要进行复杂的数据处理时,可以使用
forEach
方法。 - 条件删除:removeIf方法可以根据给定的条件删除集合中的元素,非常直观和简洁。
- 遍历元素:可以方便地遍历元素。
总结:
- Stream API:适用于复杂的数据处理管道、延迟执行和并行处理。
- Iterable、Map的forEach等API:适用于简单的迭代操作、条件删除和遍历键值对。
2.5 Lambda表达式在集合框架中的应用
Java 8 引入了Lambda表达式和许多新的默认方法,这些方法极大地简化了集合的操作。以下是Java 8以后新增的、支持Lambda表达式的集合API默认方法:
Iterable接口
- forEach:迭代集合元素
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(element -> System.out.println(element));
Collection接口(extends Iterable)
- removeIf:筛选集合元素
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
list.removeIf(element -> element.startsWith("b"));
System.out.println(list); // 输出: [apple, cherry]
List接口(extends Collection)
- replaceAll:对List中的每个元素进行替换操作
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
list.replaceAll(element -> element.toUpperCase());
System.out.println(list); // 输出: [APPLE, BANANA, CHERRY]
- sort:对List中的元素进行排序
List<String> list = new ArrayList<>(Arrays.asList("cherry", "banana", "apple"));
list.sort((a, b) -> a.compareTo(b));
System.out.println(list); // 输出: [apple, banana, cherry]
Map接口
- forEach: 对Map中的每个键值对执行指定的操作
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.forEach((key, value) -> System.out.println(key + ": " + value));
- computeIfAbsent:如果键不存在,则计算并添加键值对
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.computeIfAbsent("banana", key -> 2);
System.out.println(map); // 输出: {apple=1, banana=2}
- computeIfPresent:如果键存在,则计算新值并更新键值对
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.computeIfPresent("apple", (key, value) -> value + 1);
System.out.println(map); // 输出: {apple=2}
- merge:根据指定的条件合并值
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.merge("banana", 2, (oldValue, newValue) -> oldValue + newValue);
System.out.println(map); // 输出: {apple=1, banana=2}
- putIfAbsent:如果键不存在则添加键值对
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.putIfAbsent("banana", 2);
System.out.println(map); // 输出: {apple=1, banana=2}
- replaceAll:对Map中的每个键值对进行替换操作
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.replaceAll((key, value) -> value * 2);
System.out.println(map); // 输出: {apple=2, banana=4}
这些方法都是Java 8新增的,通过这些方法,你可以更方便地操作集合,提高代码的可读性和可维护性,并且支持使用Lambda表达式来简化代码。
除了上述API还可以通过Stream流API处理集合框架,本文仅介绍Lambda表达式相关的API。
三、Lambda表达式的优缺点
Lambda表达式在Java中引入了函数式编程的概念,具有许多优点和一些限制。下面是Lambda表达式的主要优点和缺点:
优点:
- 简洁性:相比匿名内部类的繁琐语法,Lambda表达式提供了一种更简洁、更紧凑的语法,可以减少冗余的代码和样板代码,使代码更容易于理解,提高了编码效率。
- 代码可读性:Lambda表达式使得代码更加容易解释和易读,可以直接将逻辑集中在一起,提高代码的可读性和维护性。
- 便于并行处理:Lambda表达式与Java 8引入的Stream API结合使用,可以方便地进行集合的并行处理,充分发挥多核处理器的优势,提高代码执行效率。
缺点:
- 只能用于函数式接口:Lambda表达式只能用于函数式接口(只有一个抽象方法的接口),这限制了它的使用范围。如果需要使用非函数式接口,仍然需要使用传统的方式,如匿名内部类。
- 可读性的折衷:尽管Lambda表达式可以提高代码的可读性,但在某些复杂的情况下,Lambda表达式可能变得难以理解和阅读,特别是当表达式变得过于复杂时。
- 变量捕获的限制:Lambda表达式对捕获的变量有一些限制。它们只能引用final或实际上的最终变量,这可能对某些情况下的代码编写和调试带来一些困扰。
- 学习曲线:对于习惯于传统Java编程风格的开发者来说,Lambda表达式是一项新的概念,需要一定的学习和适应过程。