在 Java 8 引入 Lambda 表达式和 Stream API 后,成为了 Java 编程中不可或缺的功能之一,它们大大提升了代码的可读性、简洁性和可维护性。尤其是在处理集合数据时,Lambda 表达式和 Stream API 使得代码更加简洁且具有更好的表达力。本篇文章将带你从零开始,了解 Lambda 和 Stream 的基本概念、使用方法以及常见应用场景。
Lambda 表达式是一种匿名函数的表示方式,它通过简洁的语法来表达传递给方法的行为或操作。Lambda 可以被看作是函数式接口的实例,即实现了一个接口的单一抽象方法。Lambda 表达式通过明确、简洁的语法定义了一种可以传递的行为。
Lambda 表达式的基本语法:
(parameters) -> expression
其中:
parameters 是传递给 Lambda 表达式的参数,可以有多个参数,或者是没有参数。
expression 是 Lambda 表达式的实现,可以是一个简单的表达式或者代码块。
例如,一个接受两个整数并返回它们和的 Lambda 表达式:
(int a, int b) -> a + b;
Lambda 表达式的参数列表可以省略类型信息,编译器会根据上下文推导出类型。
(int a, int b) -> a + b; // 显式指定类型
(a, b) -> a + b; // 类型推导
->
箭头符号用于分隔输入参数和 Lambda 表达式的主体。箭头左侧是参数列表,右侧是 Lambda 的主体。
Lambda 的主体可以是一个单一表达式或者代码块:
单一表达式:返回值会自动被计算并返回。
(a, b) -> a + b; // 返回 a + b
代码块:如果有多个语句,需要使用大括号 {}
来包围,并且必须使用 return
来返回值。
(a, b) -> {
int sum = a + b;
return sum;
}
如果 Lambda 表达式没有参数,则可以省略参数列表并直接写空括号 ()
。
() -> System.out.println("Hello World");
Lambda 表达式必须实现一个函数式接口。函数式接口是只包含一个抽象方法的接口,通常会包含多个默认方法或静态方法。
Java 标准库中有几个常用的函数式接口:
接口 | 抽象方法 | 用途描述 | 示例代码片段 |
---|---|---|---|
Predicate |
boolean test(T t) |
条件判断(如过滤集合元素) | filter(x -> x > 10) |
Function |
R apply(T t) |
类型转换或计算 | map(s -> s.length()) |
Consumer |
void accept(T t) |
消费数据(如打印) | forEach(System.out::println) |
Supplier |
T get() |
数据生成(如工厂模式) | () -> new ArrayList<>() |
UnaryOperator |
T apply(T t) |
一元操作(如数值平方) | x -> x * x |
例如,无参无返回值(Runnable)示例:
new Thread(() -> System.out.println("执行任务")).start(); //
// 作为方法参数传递
public static void execute(Runnable task) { task.run(); }
execute(() -> System.out.println("延迟执行")); //
单参无返回值(Consumer)示例:
List list = Arrays.asList("a", "b");
list.forEach(s -> System.out.println(s)); //
除了使用标准库中的接口,我们也可以自定义函数式接口。例如:
@FunctionalInterface
public interface MyFunctionalInterface {
int add(int a, int b);
}
此时,可以使用 Lambda 表达式来实现该接口:默认首次实现,后面重复实现均无效
MyFunctionalInterface addOperation = (a, b) -> a + b;
System.out.println(addOperation.add(3, 4)); // 输出 7
在没有 Lambda 表达式之前,我们常常使用匿名内部类来实现接口或抽象类,但是这种方式有时显得冗长且难以维护。Lambda 表达式使得这种实现更加简洁。
传统方式(匿名内部类):
public class AnonymousClassExample {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello from the anonymous class!");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
使用 Lambda 表达式:
public class LambdaExample {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("Hello from the lambda expression!");
Thread thread = new Thread(runnable);
thread.start();
}
}
Lambda 表达式与 Java 8 的 Stream API 配合使用,可以使得集合的操作更加简洁且功能强大。例如,对 List 中的元素进行筛选、排序等操作。
List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println); // 输出 Alice
使用 Lambda 排序 List:
List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
names.stream()
.sorted((s1, s2) -> s1.length() - s2.length())
.forEach(System.out::println); // 输出按长度排序的名字
forEach
迭代forEach
是一个接受 Lambda 表达式的终端操作,它可以用来遍历集合中的元素。
List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
names.forEach(name -> System.out.println(name));
map
转换元素map
用于将集合中的每个元素都映射为另一个元素。
List numbers = Arrays.asList(1, 2, 3, 4, 5);
List squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squares); // 输出 [1, 4, 9, 16, 25]
虽然 Lambda 表达式非常方便,但在使用时仍需要注意一些问题:
性能:在性能要求极高的场合,过度使用 Lambda 表达式可能会带来额外的性能开销。尤其是对于内存频繁分配的场景,可能会影响性能。
调试困难:Lambda 表达式的调试相对困难,因为它是匿名的,栈跟踪信息中不会显示出具体的类名或方法名。
可读性:尽管 Lambda 提供了简洁的语法,但在复杂的逻辑中使用过多的 Lambda 表达式会影响代码的可读性,尤其是当 Lambda 表达式嵌套或链式调用时。
Stream
是一个从数据源(例如集合、数组、I/O 通道等)生成的元素序列,它允许以声明性(而非命令式)方式处理数据。可以将 Stream
理解为一条数据流,它通过管道进行一系列的转换操作,最终生成一个结果。
Stream的特性
惰性求值:Stream 中的中间操作通常是惰性求值的,意味着只有在执行终端操作时,才会真正开始计算。
无副作用:Stream 在处理数据时不修改源数据,而是返回一个新的 Stream 或结果。
可以并行执行:Stream 支持并行流处理,通过并行化提升数据处理的效率。
在 Stream
中,常见的操作分为 中间操作 和 终端操作,它们具有不同的特性和作用。
中间操作是处理数据的步骤,例如过滤、排序和映射等。它们通常返回一个新的 Stream
,并且是惰性执行的,即不会立即对数据进行计算。常见的中间操作有:
filter(Predicate
:过滤操作,返回符合条件的元素。
map(Function
:映射操作,对每个元素应用一个函数,返回一个新的元素。
sorted()
:排序操作,按自然顺序对元素进行排序。
终端操作是 Stream
流程中的最后一步,执行这些操作会触发数据的计算并生成结果。常见的终端操作有:
collect(Collector
:将 Stream
中的数据转换成集合、列表等。
forEach(Consumer
:对 Stream
中的每个元素执行指定的操作。
reduce()
:聚合操作,将 Stream
中的元素按某种方式合并成一个结果。
接下来,通过一些简单的代码示例来演示 Stream
的基本用法。
你可以通过以下方式创建 Stream
:
import java.util.*;
import java.util.stream.*;
public class StreamExample {
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream stream = numbers.stream(); // 从集合创建Stream
}
}
从数组中创建:
public class StreamExample {
public static void main(String[] args) {
String[] words = {"apple", "banana", "cherry"};
Stream stream = Arrays.stream(words); // 从数组创建Stream
}
}
从值创建:
public class StreamExample {
public static void main(String[] args) {
Stream stream = Stream.of(1, 2, 3, 4, 5); // 从值创建Stream
}
}
filter
过滤元素public class StreamExample {
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤偶数
.collect(Collectors.toList());
System.out.println(evenNumbers); // 输出:[2, 4, 6]
}
}
使用 map
映射元素
public class StreamExample {
public static void main(String[] args) {
List words = Arrays.asList("apple", "banana", "cherry");
List upperCaseWords = words.stream()
.map(String::toUpperCase) // 将每个单词转换为大写
.collect(Collectors.toList());
System.out.println(upperCaseWords); // 输出:[APPLE, BANANA, CHERRY]
}
}
使用 sorted
排序元素
public class StreamExample {
public static void main(String[] args) {
List numbers = Arrays.asList(5, 2, 8, 3, 7);
List sortedNumbers = numbers.stream()
.sorted() // 按自然顺序排序
.collect(Collectors.toList());
System.out.println(sortedNumbers); // 输出:[2, 3, 5, 7, 8]
}
}
collect
收集结果public class StreamExample {
public static void main(String[] args) {
List words = Arrays.asList("apple", "banana", "cherry");
Set wordSet = words.stream()
.collect(Collectors.toSet()); // 收集到一个Set中
System.out.println(wordSet); // 输出:[banana, cherry, apple]
}
}
使用 forEach
遍历元素
public class StreamExample {
public static void main(String[] args) {
List words = Arrays.asList("apple", "banana", "cherry");
words.stream()
.forEach(word -> System.out.println(word)); // 遍历并打印每个元素
}
}
使用 reduce
聚合结果
public class StreamExample {
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // 计算总和
System.out.println(sum); // 输出:15
}
}
Stream API 还支持并行流处理,允许我们以并行的方式处理数据,从而提高性能。并行流会自动把数据分成多个部分,分别在多个线程中处理。
示例:使用 parallelStream
实现并行计算
public class StreamExample {
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream()
.reduce(0, Integer::sum); // 使用并行流计算总和
System.out.println(sum); // 输出:55
}
}
Lambda 表达式和 Stream API 通常是一起使用的。Lambda 用来定义操作的行为,Stream 则提供了处理数据的流式接口。
假设我们有一个包含多个整数的列表,想要找出其中所有偶数的平方并求和。
import java.util.*;
public class LambdaStreamExample {
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sumOfSquares = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤出偶数
.map(n -> n * n) // 计算偶数的平方
.reduce(0, Integer::sum); // 计算平方的和
System.out.println("Sum of squares of even numbers: " + sumOfSquares);
}
}
在这个例子中,我们使用了 Lambda 表达式来定义过滤偶数和计算平方的操作,而 Stream API 则用来流式处理数据。
简洁性:减少了冗长的匿名类代码。
可读性:使代码更加简洁,便于理解。
可组合性:Lambda 表达式能够与其他函数式接口一起使用,方便链式调用。
声明式代码:通过流式操作,避免了冗长的迭代和条件判断。
惰性计算:Stream 采用惰性求值方式,只有在终端操作时才会进行计算,提高了性能。
并行操作:Stream 支持并行处理,能够更好地利用多核处理器。
集合操作:简化了对集合的操作,尤其是在处理复杂的集合过滤、转换和聚合时,Stream 提供了简洁的 API。
函数式编程:Lambda 表达式为 Java 引入了函数式编程的元素,可以更容易地实现函数式编程的思想,如高阶函数、不可变数据等。
通过 Lambda 和 Stream,Java 代码变得更加简洁、高效和可维护,特别是在处理集合数据时,它们为程序员提供了强大的支持。掌握这两个特性,将使你在日常开发中更得心应手。