Java集合和数据结构-Java三大核心特性:反射(Reflection)、枚举 (Enum)与Lambda表达式。
好的,谨遵您的嘱咐。我将严格按照您提供的图片大纲,为您呈现一份关于 Java 反射、枚举和 Lambda 表达式的万字详解。这份文档将深入剖析每个知识点的核心概念、使用方法、内部原理、优缺点,并提供所有必要代码的逐行注释和详尽解释。
第一部分:Java 反射 (Reflection)
3. 反射基本信息
什么是反射?
Java 的反射(Reflection)机制是指在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能被称为 Java 语言的反射机制。
简单来说,正常的 Java 代码是在编译时就已经确定了要调用的类和方法。而反射则是在运行时才去“看”(Reflect)一个类或对象的内部结构,然后再决定如何操作它。它赋予了 Java 语言一种“自省”的能力,让代码可以检查和修改自身在运行时的行为。
反射的意义(源于图片 image_665a1f.png)
反射机制是许多高级 Java 框架的基石。比如:
-
Spring 框架:通过 XML 或注解配置 Bean,Spring 在启动时会读取这些配置,通过反射动态地创建对象(IOC - 控制反转),并注入依赖(DI - 依赖注入)。
-
IDE(如 IntelliJ IDEA, Eclipse):当你在编写代码时,IDE 提供的代码自动补全、提示等功能,就是通过反射来查看类的成员。
-
动态代理:在运行时动态地创建一个代理类,实现对目标对象的行为增强,其核心就是反射。
反射打破了编译期和运行期的界限,极大地增强了程序的灵活性和扩展性。
4. 反射相关的类 (重要)
反射功能主要由 java.lang.reflect 包下的一些核心类来提供。而所有这些操作的起点,都源于一个特殊的类:java.lang.Class。
4.1 Class 类 (反射机制的起源)
在 Java 中,万物皆对象。一个类,比如我们写的 Person.java,在编译后会生成 Person.class 文件。当 JVM 加载这个 .class 文件时,它会在内存中创建一个 java.lang.Class 类型的对象。这个 Class 对象就完整地描述了 Person 这个类的全部信息,比如它有哪些字段、哪些方法、哪些构造器、实现了哪些接口等等。
可以理解为,Person 类是我们自己定义的“图纸”,而 JVM 根据这张图纸在内存中创建了一个专门描述这张图纸信息的 Class 对象。我们后续所有的反射操作,都是从获取这个 Class 对象开始的。
4.2.1 获得 Class 对象的三种方式
Java
// 我们先定义一个用于反射操作的示例类
class Person {
private String name;
public int age;
public Person() {}
private Person(String name) {
this.name = name;
}
public void publicMethod(String content) {
System.out.println("This is a public method. Content: " + content);
}
private String privateMethod(int number) {
return "This is a private method. Number: " + number;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
public class GetClassObjectExample {
public static void main(String[] args) throws ClassNotFoundException {
// 方式一:通过类名.class 获取
// 这种方式在编译时就已经确定,类型安全,性能较好。
// 通常在我们已经知道具体类名时使用。
Class<Person> clazz1 = Person.class;
System.out.println("方式一: " + clazz1);
// 方式二:通过对象实例的 getClass() 方法获取
// 这种方式需要先有一个该类的实例,是在运行时动态获取的。
Person person = new Person();
Class<?> clazz2 = person.getClass(); // 返回值的类型是 Class<?>
System.out.println("方式二: " + clazz2);
// 方式三:通过 Class.forName("全限定类名") 获取
// 这是最动态、最常用的一种方式,因为类名是以字符串形式传入的。
// 常用于框架开发中,根据配置文件中的字符串来加载类。
// 注意:此方法会抛出 ClassNotFoundException 异常。
Class<?> clazz3 = Class.forName("Person"); // 如果 Person 类在包内,需要写 "com.example.Person"
System.out.println("方式三: " + clazz3);
// 验证三种方式获取的是同一个 Class 对象
// 在一个 JVM 中,对于同一个类,只存在一个 Class 对象。
System.out.println("clazz1 == clazz2 ? " + (clazz1 == clazz2)); // true
System.out.println("clazz1 == clazz3 ? " + (clazz1 == clazz3)); // true
}
}
4.1.1 Class 类中的相关方法
获取 Class 对象后,我们就可以像查字典一样,获取这个类的一切信息。
| 方法分类 | 方法名 | 功能描述 |
| 获取构造器 | getConstructors() | 获取所有 public 的构造方法 |
getDeclaredConstructors() | 获取所有的构造方法(包括 private) | |
getConstructor(Class<?>... parameterTypes) | 获取指定的 public 构造方法 | |
getDeclaredConstructor(Class<?>... parameterTypes) | 获取指定的任意构造方法 | |
| 获取成员变量 | getFields() | 获取所有 public 的成员变量(包括父类的) |
getDeclaredFields() | 获取本类所有的成员变量(不包括父类的) | |
getField(String name) | 获取指定的 public 成员变量 | |
getDeclaredField(String name) | 获取指定的任意成员变量 | |
| 获取方法 | getMethods() | 获取所有 public 的方法(包括父类和接口的) |
getDeclaredMethods() | 获取本类所有的方法(不包括父类的) | |
getMethod(String name, Class<?>... parameterTypes) | 获取指定的 public 方法 | |
getDeclaredMethod(String name, Class<?>... parameterTypes) | 获取指定的任意方法 | |
| 其他信息 | getName() | 获取完整的类名(含包名) |
getSimpleName() | 获取不含包名的类名 | |
getSuperclass() | 获取父类的 Class 对象 | |
getInterfaces() | 获取该类实现的所有接口的 Class 对象数组 | |
newInstance() | (已过时) 调用无参构造方法创建实例 |
【扩展说明】getDeclared... vs get... 的区别
-
getDeclaredXxx()系列方法:获取的是在当前类中显式声明的所有Xxx(字段、方法、构造器),无论其访问修饰符是public,protected,default还是private。它不关心父类。 -
getXxx()系列方法:获取的是所有public的Xxx,这其中包含了从父类继承来的public成员。
4.2.2 反射的使用示例
下面的示例将完整演示如何通过反射来突破访问限制,创建私有构造器的实例,并调用其私有方法和修改私有字段。
Java
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionInAction {
public static void main(String[] args) throws Exception { // 简化异常处理,实际开发中应精确捕获
// 1. 获取 Person 类的 Class 对象
Class<Person> personClass = Person.class;
// --- 操作构造器 ---
System.out.println("--- 操作构造器 ---");
// 2. 获取私有的、带 String 参数的构造器
// 需要传入参数类型的 .class 对象来唯一确定一个构造器
Constructor<Person> privateConstructor = personClass.getDeclaredConstructor(String.class);
// 3. 突破访问限制
// 默认情况下,我们无法访问私有成员。必须将 accessible 设置为 true。
privateConstructor.setAccessible(true);
// 4. 使用私有构造器创建实例
// newInstance 的参数将传递给构造器
Person personInstance = privateConstructor.newInstance("张三");
System.out.println("通过私有构造器创建的实例: " + personInstance);
// --- 操作字段 ---
System.out.println("\n--- 操作字段 ---");
// 5. 获取私有的 name 字段
Field nameField = personClass.getDeclaredField("name");
// 6. 同样需要突破访问限制
nameField.setAccessible(true);
// 7. 修改私有字段的值
// set 方法需要两个参数:(要修改哪个对象, 新的值)
nameField.set(personInstance, "李四");
System.out.println("修改私有字段 name 后的实例: " + personInstance);
// 8. 获取字段的值
// get 方法需要一个参数:(要从哪个对象获取)
String nameValue = (String) nameField.get(personInstance);
System.out.println("获取到的私有字段 name 的值: " + nameValue);
// --- 操作方法 ---
System.out.println("\n--- 操作方法 ---");
// 9. 获取私有的、带 int 参数的 privateMethod 方法
Method privateMethod = personClass.getDeclaredMethod("privateMethod", int.class);
// 10. 突破访问限制
privateMethod.setAccessible(true);
// 11. 调用私有方法
// invoke 方法需要两个参数:(要在哪个对象上调用, 传递给方法的参数)
// 如果方法是静态的,第一个参数可以传 null
Object returnValue = privateMethod.invoke(personInstance, 123);
System.out.println("调用私有方法 privateMethod 的返回值: " + returnValue);
}
}
5. 反射优点和缺点
优点:
-
动态性与灵活性:这是反射最大的优点。它允许程序在运行时才加载、探知和使用编译期间完全未知的类。这使得编写高度灵活、可扩展的通用框架成为可能。
-
代码解耦:反射可以帮助我们最大限度地将代码的依赖关系从编译期推迟到运行期,降低模块间的耦合度。
缺点:
-
性能开销大:反射操作涉及到动态类型解析、方法查找、安全检查等一系列耗时步骤,其性能远低于直接的 Java 调用。对于性能敏感的应用,应避免在热点代码(如循环)中频繁使用反射。
-
破坏封装性:通过
setAccessible(true)可以访问和修改类的私有成员,这完全违背了面向对象的封装原则,可能导致代码逻辑混乱和维护困难。 -
类型不安全:编译器无法对反射代码进行类型检查。所有类型错误(如方法名写错、参数类型不匹配)都只能在运行时才会暴露,这增加了程序的不稳定性。
-
代码可读性差:大量使用反射的代码,其调用关系变得模糊不清,难以阅读和理解。
6. 重点总结
-
反射的意义:赋予程序在运行时动态地检查和操作类与对象的能力,是实现框架和动态功能的基石。
-
反射重要的几个类:
-
java.lang.Class: 反射的入口,代表一个类的元数据。 -
java.lang.reflect.Constructor: 代表类的构造器。 -
java.lang.reflect.Field: 代表类的成员变量。 -
java.lang.reflect.Method: 代表类的方法。
-
-
学会合理利用反射,一定要在安全环境下使用:反射是一把双刃剑。要充分认识到它的性能和安全风险,仅在确实需要动态性的场景下(如框架开发、插件化)使用。对于常规的业务逻辑开发,应优先使用常规的编程方式。
第二部分:枚举 (Enum)
1. 背景及定义
在枚举出现之前(JDK 1.5 以前),当我们需要表示一组固定的常量时,通常会使用 public static final int 的方式:
Java
public class OldConstants {
public static final int SEASON_SPRING = 0;
public static final int SEASON_SUMMER = 1;
public static final int SEASON_AUTUMN = 2;
public static final int SEASON_WINTER = 3;
}
这种方式存在诸多问题:
-
类型不安全:一个期望接收季节常量的方法
setSeason(int season),可以被传入任何int值(如setSeason(5)),编译器无法检查其合法性。 -
无命名空间:这些常量散落在类中,没有明确的归属感。
-
可读性差:打印一个常量
0出来,我们无法直观地知道它代表的是“春天”。 -
无法扩展:无法为这些简单的常量附加更多的信息或行为。
为了解决这些问题,Java 引入了 enum 关键字。
定义:枚举是一种特殊的类,它用于定义一组固定的、命名的常量。每个枚举常量都是该枚举类型的一个唯一的、公开的、静态的 final 实例。
Java
// 定义一个表示季节的枚举
public enum Season {
// 这四个就是枚举的实例,必须在最开始声明
SPRING, SUMMER, AUTUMN, WINTER;
}
2. 使用
枚举的使用非常简单、直观且类型安全。
Java
public class EnumUsage {
public static void main(String[] args) {
// 1. 直接使用枚举实例
Season currentSeason = Season.AUTUMN;
// 2. 在 switch 语句中使用,非常推荐
// 编译器会检查 case 是否覆盖了所有枚举实例,并给出警告
switch (currentSeason) {
case SPRING:
System.out.println("春暖花开");
break;
case SUMMER:
System.out.println("夏日炎炎");
break;
case AUTUMN:
System.out.println("秋高气爽");
break;
case WINTER:
System.out.println("冬日暖阳");
break;
}
// 3. 遍历所有枚举实例
System.out.println("\n--- 遍历所有季节 ---");
// values() 方法返回一个包含所有枚举实例的数组
for (Season season : Season.values()) {
// name() 返回实例的名称 (字符串形式)
// ordinal() 返回实例的声明顺序 (从 0 开始)
System.out.println(season.name() + " is at index " + season.ordinal());
}
// 4. 将字符串转换为枚举实例
// valueOf() 方法根据名称字符串查找对应的实例,找不到会抛出异常
Season seasonFromString = Season.valueOf("SPRING");
System.out.println("\nString 'SPRING' to enum: " + seasonFromString);
}
}
【扩展说明:让枚举变得更强大】
枚举远不止是常量集合,它可以像一个普通的类一样,拥有自己的字段、构造器和方法。
Java
// 一个更强大的季节枚举,包含了中文名和描述
public enum AdvancedSeason {
// 在声明实例时,调用构造器传入参数
SPRING("春天", "万物复苏"),
SUMMER("夏天", "烈日当空"),
AUTUMN("秋天", "硕果累累"),
WINTER("冬天", "银装素裹");
// 私有字段,用于存储额外信息
private final String chineseName;
private final String description;
// 构造器必须是 private 的(也可以省略,默认为 private)
// 枚举的构造器只在 JVM 加载枚举类时被调用,用于创建实例
private AdvancedSeason(String chineseName, String description) {
this.chineseName = chineseName;
this.description = description;
}
// 公共方法,允许外部获取额外信息
public String getChineseName() {
return chineseName;
}
public void printDescription() {
System.out.println(this.description);
}
public static void main(String[] args) {
AdvancedSeason.AUTUMN.printDescription(); // 输出: 硕果累累
System.out.println(AdvancedSeason.SUMMER.getChineseName()); // 输出: 夏天
}
}
3. 枚举的优点和缺点
优点:
-
类型安全:编译器保证了变量只能持有该枚举类型定义的实例,杜绝了非法值的传入。
-
代码清晰、可读性高:
Season.SPRING远比0更容易理解。 -
功能强大:可以拥有字段和方法,实现接口,将数据和行为封装在一起。
-
与
switch完美配合:提供了编译时的检查,增强了代码的健壮性。 -
保证单例:每个枚举常量在 JVM 中只有一个实例,这使得它们可以安全地用于实现线程安全的单例模式。
缺点:
-
不可继承:枚举类不能再继承其他类,因为它们已经隐式地继承了
java.lang.Enum。 -
内存开销:相比
static final int,每个枚举实例都是一个对象,会占用更多的内存。但在绝大多数场景下,这点开销是完全可以接受的,换来的是巨大的安全性和便利性。
4. 枚举和反射
核心问题:枚举是否可以通过反射,拿到实例对象呢?
答案是:不能。 Java 的设计者为了保证枚举的类型安全和单例特性,从根本上阻止了通过反射来创建枚举实例。
【代码验证】
Java
import java.lang.reflect.Constructor;
public class EnumReflectionTest {
public static void main(String[] args) {
try {
// 1. 获取 Season 枚举的 Class 对象
Class<Season> seasonClass = Season.class;
// 2. 尝试获取它的无参构造器
// 即使枚举有带参构造器,它也总有一个编译器生成的、用于创建实例的私有构造器
// 这里我们尝试获取一个(实际上是带 String 和 int 参数的)
Constructor<Season> constructor = seasonClass.getDeclaredConstructor(String.class, int.class);
// 3. 突破访问限制
constructor.setAccessible(true);
// 4. 尝试创建新实例
System.out.println("尝试通过反射创建 Season 的新实例...");
constructor.newInstance("FAKE_SPRING", 4); // 这里会抛出异常
} catch (Exception e) {
System.err.println("创建失败! 异常信息如下:");
e.printStackTrace();
}
}
}
运行结果:
尝试通过反射创建 Season 的新实例...
创建失败! 异常信息如下:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:493)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at EnumReflectionTest.main(EnumReflectionTest.java:18)
【原因分析】
Constructor.newInstance 方法的源码中,有明确的检查逻辑:
Java
// (伪代码)
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
如果 Class 对象被标记为枚举类型,就直接抛出异常。这是 JVM 层面的硬性规定,用以保护枚举的完整性。
5. 总结 (源于图片 image_665a02.png)
-
枚举本身就是一个类:它是一个特殊的类,其构造方法默认且必须是
private的,并且都隐式地继承自java.lang.Enum。 -
枚举可以避免反射和序列化问题:
-
反射问题:如上所述,JVM 禁止通过反射创建枚举实例。
-
序列化问题:Java 的序列化机制对枚举有特殊处理。在序列化时,只会写入枚举常量的名称;在反序列化时,会通过
Enum.valueOf()方法根据名称查找现有的枚举实例,而不是创建一个新对象。这从根本上保证了即使在序列化和反序列化之后,枚举的单例特性也不会被破坏。
-
-
枚举的优缺点:优点是类型安全、可读性高、功能强大;缺点是不可继承、相比原生常量有轻微的内存开销。
由于篇幅巨大,关于 Lambda 表达式的深入探讨将在下一条回复中继续。
继续上一部分的内容,我们将深入探讨 Java 8 引入的革命性特性——Lambda 表达式。
第三部分:Lambda 表达式 (Lambda Expression)
1. 背景
在 Java 8 之前,如果我们想传递一段代码(一个行为),通常需要使用匿名内部类。例如,对一个列表进行排序,我们需要提供一个 Comparator 的实现:
Java
// Java 8 之前的写法
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
这种写法的弊端很明显:
-
代码冗余:我们真正关心的只有
a.compareTo(b)这一行核心逻辑,但却为此写了五六行“样板代码”(new Comparator<String>(),@Override, 方法签名等)。 -
可读性差:核心逻辑被淹没在语法噪音中。
随着函数式编程思想的流行(将函数/行为视为一等公民,可以像变量一样传递),Java 迫切需要一种更简洁的方式来传递代码块。Lambda 表达式应运而生。
1.2 函数式接口 (Functional Interface)
Lambda 表达式本质上是一段可以传递的匿名代码块。但是,Java 是一个强类型语言,任何东西都得有个类型。那么,Lambda 表达式的类型是什么呢?答案是函数式接口。
定义:一个有且仅有一个抽象方法的接口,就被称为函数式接口。
-
Runnable接口(只有一个run()方法) -
Comparator接口(只有一个compare()方法) -
Callable接口(只有一个call()方法)
为了明确标识一个接口是函数式接口,Java 8 提供了 @FunctionalInterface 注解。这个注解是可选的,但加上之后,如果接口不满足“只有一个抽象方法”的条件,编译器就会报错,这是一种很好的实践。
Java
@FunctionalInterface
public interface MyFunctionalInterface {
void execute(); // 只有一个抽象方法
//可以有默认方法和静态方法
default void print() {
System.out.println("Default method");
}
}
结论:Lambda 表达式只能用于“期望一个函数式接口实例”的上下文。Lambda 表达式本身会被编译器推断为该函数式接口的一个实现类的实例。
1.1 Lambda 表达式的语法
Lambda 表达式的通用语法是:
(parameter_list) -> { method_body }
它由三部分组成:
-
参数列表
(parameter_list):与函数式接口中抽象方法的参数列表相对应。 -
箭头
->:将参数列表与方法体分开。 -
方法体
{ method_body }:实现了抽象方法的具体逻辑。
【语法精简规则】
-
参数类型可省略:编译器可以根据上下文推断出参数类型。
(String a, String b) -> a.compareTo(b) 可以简写为 (a, b) -> a.compareTo(b)
-
一个参数时,括号可省略:
(name) -> System.out.println(name) 可以简写为 name -> System.out.println(name)
-
方法体只有一行时,大括号和 return 可省略:
(a, b) -> { return a.compareTo(b); } 可以简写为 (a, b) -> a.compareTo(b)
-
无参数时,括号不可省略:
() -> System.out.println("Hello")
2. Lambda 表达式的基本使用
让我们用 Lambda 表达式重写开头的排序例子:
Java
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class LambdaBasicUsage {
public static void main(String[] args) {
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
// 使用 Lambda 表达式
// (a, b) 对应 Comparator.compare(String a, String b) 的参数
// a.compareTo(b) 是方法体,其返回值符合 compare 方法的 int 返回类型
Collections.sort(names, (a, b) -> a.compareTo(b));
System.out.println(names); // 输出: [anna, mike, peter, xenia]
}
}
可以看到,代码变得极其简洁,核心逻辑一目了然。
3. 变量捕获 (Variable Capture)
Lambda 表达式可以访问其外部作用域的变量,这个行为被称为“变量捕获”。
规则:Lambda 表达式可以捕获局部变量,但该变量必须是 final 或事实上的 final (effectively final)。
-
事实上的 final:指一个局部变量在初始化后,其值再也没有被改变过。即使没有显式声明为
final,编译器也将其视为final。
【代码示例】
Java
public class VariableCaptureExample {
public static void main(String[] args) {
// text 是一个事实上的 final 变量,因为它在初始化后没有被再次赋值
String text = "Hello, ";
// count 如果在 Lambda 之后被修改,就不是事实上的 final
int count = 10; // 也是事实上的 final
// Lambda 捕获了外部的 text 和 count 变量
Runnable r = () -> {
// 我们可以在 Lambda 内部读取被捕获的变量
System.out.println(text + "World! Count is " + count);
// 错误:不能在 Lambda 内部修改被捕获的局部变量
// text = "Hi"; // 编译错误
// count++; // 编译错误
};
r.run();
// 如果我们试图在 Lambda 定义之后修改这个变量,它将不再是事实上的 final,
// 从而导致 Lambda 表达式的定义处产生编译错误。
// count = 11; // 如果取消这行注释,上面的 Lambda 表达式会报错
}
}
【为什么有这个限制?】
这个限制主要是为了避免线程安全问题。Lambda 表达式可能会在另一个线程中执行(例如,在并行流中)。如果允许修改外部的局部变量,就会出现数据同步问题。通过将变量限制为事实上的 final,保证了 Lambda 表达式捕获的是一个固定的、不变的值,从而避免了不确定性。这与匿名内部类的变量捕获规则是一致的。
4. Lambda 在集合当中的使用
Java 8 对集合框架进行了大规模增强,加入了许多接受函数式接口作为参数的新方法,使得 Lambda 表达式能够大展身手。
Java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class LambdaWithCollections {
public static void main(String[] args) {
List<String> languages = new ArrayList<>(Arrays.asList("Java", "Python", "JavaScript", "Go", "Rust"));
// 1. 遍历 (Collection.forEach)
System.out.println("--- 1. forEach ---");
// forEach 方法接受一个 Consumer<T> 函数式接口 ( (T t) -> void )
languages.forEach(lang -> System.out.println(lang));
// 2. 删除 (Collection.removeIf)
System.out.println("\n--- 2. removeIf ---");
// removeIf 方法接受一个 Predicate<T> 函数式接口 ( (T t) -> boolean )
// 它会移除所有使 Predicate 返回 true 的元素
languages.removeIf(lang -> lang.startsWith("J"));
System.out.println("After removing languages starting with 'J': " + languages);
// 3. 替换 (List.replaceAll)
System.out.println("\n--- 3. replaceAll ---");
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// replaceAll 方法接受一个 UnaryOperator<T> 函数式接口 ( (T t) -> T )
// 它会将每个元素替换为操作后的结果
numbers.replaceAll(num -> num * 2);
System.out.println("After doubling each number: " + numbers);
// 4. 排序 (List.sort)
System.out.println("\n--- 4. sort ---");
// List.sort 方法接受一个 Comparator<T>
// 按字符串长度降序排序
languages.sort((s1, s2) -> s2.length() - s1.length());
System.out.println("Sorted by length descending: " + languages);
}
}
【扩展说明:Stream API】
Lambda 表达式最强大的应用场景是与 Stream API 结合。Stream API 提供了一种声明式、链式的方式来处理集合数据,极大地改善了集合操作。
Java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamApiExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Anna", "Alex");
// 需求:找出所有以 'A' 开头的名字,转换为大写,并按字母排序,最后收集到新的 List 中
// 传统写法
List<String> resultOld = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
resultOld.add(name.toUpperCase());
}
}
Collections.sort(resultOld);
// Stream API + Lambda 写法
List<String> resultNew = names.stream() // 1. 获取流
.filter(name -> name.startsWith("A")) // 2. 过滤 (Predicate)
.map(String::toUpperCase) // 3. 映射/转换 (Function),这里使用了方法引用,等价于 name -> name.toUpperCase()
.sorted() // 4. 排序 (使用自然顺序)
.collect(Collectors.toList()); // 5. 收集结果
System.out.println("Traditional result: " + resultOld);
System.out.println("Stream API result: " + resultNew);
}
}
Stream API 的写法不仅更简洁,而且逻辑更清晰,并且可以轻松地切换到并行计算(只需将 stream() 换成 parallelStream()),这是 Lambda 表达式带来的巨大优势。
5. 总结 (源于图片 image_665756.png)
优点:
-
代码简洁,开发迅速:消除了大量匿名内部类的样板代码,让开发者专注于核心逻辑。
-
方便函数式编程:将行为作为参数传递,使代码更具表现力。
-
非常容易进行并行计算:Stream API 的
parallelStream()让利用多核 CPU 进行数据处理变得异常简单。 -
Java 引入 Lambda,改善了集合操作:
forEach,removeIf以及强大的 Stream API 从根本上改变了我们处理集合的方式。
缺点:
-
代码可读性变差:对于不熟悉 Lambda 的人,或者当 Lambda 表达式过于复杂、层层嵌套时,代码的直观性会下降。保持 Lambda 表达式的简短是良好实践。
-
在非并行计算中,很多计算未必有传统的 for 性能要高:对于简单的、顺序的集合遍历,
for循环或forEach循环的性能通常是最好的。Stream API 会有一些额外的开销(如创建流、中间操作的装箱拆箱等),只有在数据量较大或可以并行化时,其优势才会体现。 -
不容易进行调试:当 Lambda 表达式中出现异常时,堆栈跟踪信息(Stack Trace)可能不如传统方法调用那样清晰,因为它涉及到很多由 JVM 生成的类和方法。IDE 的调试工具正在不断改进以更好地支持 Lambda 调试。
更多推荐


所有评论(0)