深入解析Java 反射、枚举与 Lambda 表达式
本篇笔记整理自《13. 反射、枚举以及 lambda 表达式》课程讲义,在保留原文全部知识点与示例代码的基础上,对每个要点做了展开说明,并修正了原文中个别表述与示例中的笔误。文中代码均为 JDK 8 环境可运行示例。
1. 反射
本节目标:
- 掌握反射
- 掌握枚举
- 掌握 Lambda 表达式的使用
1.1 定义
Java 的反射(Reflection)机制,是指在运行时检查、访问和修改类、接口、字段和方法的机制。这种在程序运行过程中动态获取类的信息、以及动态调用对象方法的功能,就称为 Java 语言的反射机制。
理解反射的关键在于"运行时"三个字。普通的 Java 代码在编译期就已经确定了要调用哪个类、哪个方法,而反射允许我们在程序运行起来之后,再去"探问"一个对象到底属于哪个类、这个类里有哪些属性、有哪些方法、有哪些构造器,并且可以绕过 private 等访问限制去读写属性、调用方法。这种"程序操作程序自身结构"的能力,就是反射的本质。
1.2 用途(了解)
反射并不是日常业务代码中频繁使用的工具,但它是几乎所有主流框架的底层基石。常见用途包括:
- 框架开发:Spring、MyBatis 等框架通过反射把配置文件或注解中声明的类实例化并注入,实现"控制反转(IoC)"。
- 注解处理:注解本身只是元数据,只有通过反射读取到注解,框架才能在运行时根据注解做出相应行为(如
@Override、@Autowired)。 - 动态代理:JDK 动态代理、AOP 面向切面编程都依赖反射在运行时生成代理类。
- 配置文件解析:读取
xxx.properties或xxx.xml中的类全限定名后,用Class.forName()动态加载并创建对象。 - 等等:如序列化/反序列化、单元测试框架(JUnit 反射调用被
@Test标注的方法)、IDE 的代码提示等。
一句话记忆:凡是"事先不知道要操作哪个类,需要在运行时才决定"的场景,往往都用得到反射。
1.3 反射相关的类(重要)
反射相关的所有类都位于 java.lang.reflect 包(以及 java.lang.Class)下,核心是四个类:
- Class:代表"类"本身,是反射的入口与起源;
- Field:代表类中的成员属性;
- Method:代表类中的方法;
- Constructor:代表类中的构造器。
1.3.1 Class 类(反射机制的起源)
Class 类代表"类的实体",在运行的 Java 应用程序中表示类和接口。
它的来源可以这样理解:Java 文件被编译后生成了 .class 字节码文件;JVM 在加载时要去解读这个 .class 文件,并把"被编译后的 .class 文件"解析为一个对象,这个对象就是 java.lang.Class 类型。也就是说,程序在运行时,每一个 .java 文件最终都变成了 Class 类对象的一个实例。我们通过反射机制去操作这个实例,就可以获取、甚至添加和改变这个类的属性和动作,使这个类成为一个"动态的类"。
这里有一个非常重要的结论:一个类在 JVM 中只会有一个 Class 实例,即无论用什么方式获取同一个类的 Class 对象,得到的都是同一个引用。
Class 类中常用的方法可分为以下几组(具体的调用方式见 1.3.2 的示例):
常用获取"类"相关的方法(重要)
String getName()/String getSimpleName():获取类的全限定名 / 简单类名;Class<?> getSuperclass():获取父类的Class对象;Class<?>[] getInterfaces():获取该类实现的所有接口;Class.forName(String className):根据类的全限定名获取Class对象(最常用,可能抛ClassNotFoundException);newInstance():调用类的无参公共构造方法创建对象(JDK 9 后已不推荐使用,建议改用构造器对象的newInstance)。
常用获取"属性"相关的方法(重要,返回值与
Field相关)Field getDeclaredField(String name):获取本类声明的指定名称属性(包含private,不含继承来的属性);Field[] getDeclaredFields():获取本类声明的所有属性(含private);Field getField(String name)/Field[] getFields():只能获取**公有(public)**属性,且包含从父类继承的公有属性。
获取"注解"相关的方法(了解)
Annotation[] getAnnotations()/Annotation getAnnotation(Class annotationClass):获取类上的注解。
常用获取"构造器"相关的方法(重要,返回值与
Constructor相关)Constructor<?> getDeclaredConstructor(Class... parameterTypes):获取指定参数列表的构造器(含private);Constructor<?>[] getDeclaredConstructors():获取所有构造器(含private);Constructor<?> getConstructor(Class... parameterTypes)/getConstructors():只能获取公有构造器。
常用获取"方法"相关的方法(重要,返回值与
Method相关)Method getDeclaredMethod(String name, Class... parameterTypes):获取本类声明的指定方法(含private);Method[] getDeclaredMethods():获取本类声明的所有方法(含private);Method getMethod(String name, Class... parameterTypes)/getMethods():只能获取公有方法,且包含继承来的公有方法。
小结:带
Declared的方法能看到"本类自己声明的全部成员(包括私有)",不带Declared的getXxx方法只能看到公有成员并包含继承而来的公有成员。这一区别在反射操作私有成员时尤为关键。
1.3.2 反射示例
1.3.2.1 获得 Class 对象的三种方式
在做反射之前,第一步要做的就是拿到当前需要反射的类的 Class 对象,再通过 Class 对象的核心方法达到反射的目的——即在运行状态中,对于任意一个类,都能知道它的所有属性和方法;对于任意一个对象,都能调用它的任意方法和属性。既然能拿到这些信息,我们甚至可以修改其中的部分类型信息。
获取 Class 对象有三种方式:
使用
Class.forName("类的全路径名")静态方法。
前提:已明确类的全路径名(若类在某个包下,必须带上完整的包路径)。这是用得最多的方式,但可能抛出ClassNotFoundException。使用
类名.class的方式。
说明:仅适合在编译前就已经明确要操作的类。该方法最为安全可靠、性能也最高,因为任何一个类都有一个隐含的静态成员变量class。使用类对象的
getClass()方法。
已经拿到对象实例时,通过对象.getClass()获取其Class对象。
下面用一个 Student 类作为后续所有反射示例的被反射对象(注意其中刻意包含了私有的属性、构造器和方法):
class Student {
// 私有属性 name
private String name = "bit";
// 公有属性 age
public int age = 18;
// 不带参数的构造方法
public Student() {
System.out.println("Student()");
}
private Student(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Student(String,name)");
}
private void eat() {
System.out.println("i am eat");
}
public void sleep() {
System.out.println("i am pig");
}
private void function(String str) {
System.out.println(str);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}演示三种获取 Class 对象的方式,并验证"同一个类在 JVM 中只有一个 Class 实例":
public class TestDemo {
public static void main(String[] args) {
// 1. 通过 getClass 获取 Class 对象
Student s1 = new Student();
Class c1 = s1.getClass();
// 2. 直接通过 类名.class 的方式得到,该方法最为安全可靠、程序性能更高
// 这说明任何一个类都有一个隐含的静态成员变量 class
Class c2 = Student.class;
// 3. 通过 Class 对象的 forName() 静态方法来获取,用得最多,但可能抛 ClassNotFoundException
Class c3 = null;
try {
// 注意这里是类的全路径,如果有包需要加上包的路径
c3 = Class.forName("Student");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 一个类在 JVM 中只会有一个 Class 实例,
// 即对上面获取的 c1、c2、c3 做 equals 比较,结果都是 true
System.out.println(c1.equals(c2));
System.out.println(c1.equals(c3));
System.out.println(c2.equals(c3));
}
}运行后三行比较结果均为 true,印证了"同一个类在 JVM 中只有一个 Class 对象"这一结论。
1.3.2.2 反射的使用
接下来正式开始"用反射"。我们依旧反射上面的 Student 类,把反射逻辑写到另外的类中以便理解。
注意: 所有和反射相关的类都在
java.lang.reflect包下面,使用前需要import。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectClassDemo {
// 创建对象
public static void reflectNewInstance() {
try {
Class<?> classStudent = Class.forName("Student");
Object objectStudent = classStudent.newInstance();
Student student = (Student) objectStudent;
System.out.println("获得学生对象:" + student);
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 反射私有的构造方法(把注释切换为下一行可获得公有构造方法)
public static void reflectPrivateConstructor() {
try {
Class<?> classStudent = Class.forName("Student");
// 注意传入对应的参数类型
Constructor<?> declaredConstructorStudent =
classStudent.getDeclaredConstructor(String.class, int.class);
// Constructor<?> declaredConstructorStudent = classStudent.getConstructor();
// 设置为 true 后可修改(绕过)访问权限
declaredConstructorStudent.setAccessible(true);
Object objectStudent = declaredConstructorStudent.newInstance("高博", 15);
// Object objectStudent = declaredConstructorStudent.newInstance();
Student student = (Student) objectStudent;
System.out.println("获得私有构造函数且修改姓名和年龄:" + student);
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 反射私有属性
public static void reflectPrivateField() {
try {
Class<?> classStudent = Class.forName("Student");
Field field = classStudent.getDeclaredField("name");
field.setAccessible(true);
Object objectStudent = classStudent.newInstance();
Student student = (Student) objectStudent;
// 可以修改该属性的值
field.set(student, "小明");
String name = (String) field.get(student);
System.out.println("反射私有属性修改了 name:" + name);
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 反射私有方法
public static void reflectPrivateMethod() {
try {
Class<?> classStudent = Class.forName("Student");
Method methodStudent =
classStudent.getDeclaredMethod("function", String.class);
System.out.println("私有方法的方法名为:" + methodStudent.getName());
// 私有的一般都要加 setAccessible
methodStudent.setAccessible(true);
Object objectStudent = classStudent.newInstance();
Student student = (Student) objectStudent;
methodStudent.invoke(student, "我是给私有的 function 函数传的参数");
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
// reflectNewInstance();
// reflectPrivateConstructor();
// reflectPrivateField();
reflectPrivateMethod();
}
}逐条解释这段代码体现的反射要点:
- 反射创建对象:
Class.forName(...).newInstance()会调用类的无参公共构造方法来实例化对象,再向下强转回Student类型。 - 反射私有构造器:先用
getDeclaredConstructor(String.class, int.class)按参数类型拿到那个私有构造器,再调用setAccessible(true)取消权限校验,就能用newInstance(实参...)创建对象——这就是"反射可以突破private限制"。 - 反射私有属性:用
getDeclaredField("name")拿到私有字段,setAccessible(true)后,再用field.set(obj, 新值)写值、field.get(obj)读值。 - 反射私有方法:用
getDeclaredMethod("function", String.class)拿到私有方法,setAccessible(true)后用method.invoke(obj, 实参...)调用。
setAccessible(true)的作用:它关闭的是 Java 的"访问权限检查"(安全检查),而不是修改了修饰符本身。对于private成员,反射时必须先调用它才能正常读写,否则会抛出IllegalAccessException。
1.4 反射的优点和缺点
优点:
- 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。
- 增加程序的灵活性和扩展性,降低耦合性,提高自适应能力。
- 反射已被广泛运用在很多流行框架中,如 Struts、Hibernate、Spring 等。
缺点:
- 存在性能开销。反射是在运行时动态解析的,相比直接调用要慢,频繁使用会导致程序效率降低。
- 带来维护问题。反射绕过了源代码层面的类型检查与编译期检查,反射代码比相应的直接代码更复杂、更难阅读和调试,也更易在重构时出错。
1.5 重点总结
- 反射的意义:在运行时动态获取并操作类的结构信息。
- 反射中最重要的几个类:
Class类、Field类、Method类、Constructor类。 - 学会合理利用反射,一定要在安全环境下使用——反射能破坏封装,滥用会带来安全与稳定性隐患。
2. 枚举的使用
2.1 背景及定义
枚举是在 JDK 1.5 以后引入的特性。它的主要用途是:把一组常量组织起来。在枚举出现之前,表示一组常量通常使用定义常量的方式:
public static final int RED = 1; public static final int GREEN = 2; public static final int BLACK = 3;
但这种"常量"方式有不好的地方:例如某处恰好出现了一个数字 1,它有可能被误认为是 RED,可读性差、也不安全(类型只是普通 int,无法约束取值范围)。现在我们可以直接用枚举来组织,这样一来常量就拥有了自己的类型——枚举类型,而不再是普通的整型 1。
public enum TestEnum {
RED, BLACK, GREEN;
}
- 优点: 将常量组织起来统一进行管理。
- 典型场景: 错误状态码、消息类型、颜色划分、状态机等。
- 本质: 枚举是
java.lang.Enum的子类。也就是说,即使我们自己写的枚举类没有显式地extends Enum,它也默认继承了这个类。
2.2 使用
1. 用在 switch 语句中
枚举是 switch 的天然搭档(case 后直接写枚举常量,不需要 break 之外的限定):
public enum TestEnum {
RED, BLACK, GREEN, WHITE;
public static void main(String[] args) {
TestEnum testEnum2 = TestEnum.BLACK;
switch (testEnum2) {
case RED:
System.out.println("red");
break;
case BLACK:
System.out.println("black");
break;
case WHITE:
System.out.println("white");
break;
case GREEN:
System.out.println("green");
break;
default:
break;
}
}
}注:原文示例中
case GREEN误打印了"black",此处已修正为"green"。运行结果为black。
2. Enum 类的常用方法
Enum 提供了一些内置方法,常用的有:
values():返回包含所有枚举常量的数组(顺序即定义顺序);ordinal():返回该常量在枚举中声明的顺序索引(从 0 开始);valueOf(String name):按名称字符串返回对应的枚举常量,找不到则抛IllegalArgumentException;compareTo(E o):按ordinal()比较两个枚举常量的先后顺序。
示例一(values / ordinal / valueOf):
public enum TestEnum {
RED, BLACK, GREEN, WHITE;
public static void main(String[] args) {
TestEnum[] testEnum2 = TestEnum.values();
for (int i = 0; i < testEnum2.length; i++) {
System.out.println(testEnum2[i] + " " + testEnum2[i].ordinal());
}
System.out.println("=========================");
System.out.println(TestEnum.valueOf("GREEN"));
}
}输出(每个常量及其序号,最后通过名称取出 GREEN):
RED 0 BLACK 1 GREEN 2 WHITE 3 ========================= GREEN
示例二(compareTo):
public enum TestEnum {
RED, BLACK, GREEN, WHITE;
public static void main(String[] args) {
// 拿到枚举实例 BLACK
TestEnum testEnum = TestEnum.BLACK;
// 拿到枚举实例 RED
TestEnum testEnum21 = TestEnum.RED;
System.out.println(testEnum.compareTo(testEnum21));
System.out.println(BLACK.compareTo(RED));
System.out.println(RED.compareTo(BLACK));
}
}输出:
1 1 -1
compareTo 比较的是两个枚举常量的 ordinal 之差:BLACK(1) - RED(0) = 1,RED(0) - BLACK(1) = -1。
3. 带构造器与属性的枚举
刚刚说过,在 Java 当中枚举实际上就是一个类。所以我们在定义枚举的时候,还可以这样定义和使用枚举——给枚举加上属性和构造方法:
重要:枚举的构造方法默认是私有的。
public enum TestEnum {
RED("red", 1), BLACK("black", 2), WHITE("white", 3), GREEN("green", 4);
private String name;
private int key;
/**
* 1、当枚举对象有参数后,需要提供相应的构造函数
* 2、枚举的构造函数默认是私有的,这个一定要记住
*/
private TestEnum(String name, int key) {
this.name = name;
this.key = key;
}
public static TestEnum getEnumKey(int key) {
for (TestEnum t : TestEnum.values()) {
if (t.key == key) {
return t;
}
}
return null;
}
public static void main(String[] args) {
System.out.println(getEnumKey(2));
}
}说明:枚举里的每一项
RED("red", 1)其实就是在调用那个私有构造器创建的一个实例。getEnumKey(2)遍历所有枚举项,找到key为 2 的BLACK并返回,输出BLACK。
2.3 枚举的优点和缺点
优点:
- 枚举常量更简单、更安全(有类型约束,取值被限定在有限的集合内)。
- 枚举具有内置方法,代码更优雅。
缺点:
- 不可继承、无法扩展——因为所有枚举都隐式继承了
java.lang.Enum,而 Java 不支持多继承,所以枚举不能再继承别的类。
2.4 枚举和反射
2.4.1 枚举是否可以通过反射拿到实例对象呢?
前面在反射部分看到:任何一个类,哪怕其构造方法是私有的,我们也可以通过反射拿到它的实例对象。那么枚举的构造方法也是私有的,我们能不能通过反射拿到枚举实例呢?下面来实验一下。
沿用上面带属性的枚举类,尝试用反射调用它的私有构造器:
public static void reflectPrivateConstructor() {
try {
Class<?> classStudent = Class.forName("TestEnum");
// 当前枚举类提供了两个参数,分别是 String 和 int
Constructor<?> declaredConstructorStudent =
classStudent.getDeclaredConstructor(String.class, int.class);
declaredConstructorStudent.setAccessible(true);
Object objectStudent = declaredConstructorStudent.newInstance("绿色", 666);
TestEnum testEnum = (TestEnum) objectStudent;
System.out.println("获得枚举的私有构造函数:" + testEnum);
} catch (Exception ex) {
ex.printStackTrace();
}
}
第一次运行结果:
java.lang.NoSuchMethodException: TestEnum.<init>(java.lang.String, int)
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at TestEnum.reflectPrivateConstructor(TestEnum.java:40)
at TestEnum.main(TestEnum.java:54)
原因分析: 异常是 NoSuchMethodException,说明找不到这个签名是 (String, int) 的构造器。虽然我们自己写的是两个参数(一个 String、一个 int),但枚举的构造器默认还会再追加两个参数。看 Enum 类的源码就明白了:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
也就是说,我们自己的构造器有两个参数(String、int),同时它默认还会带上父类 Enum 的两个参数(String、int)。所以真正需要传入的是 4 个参数:
public static void reflectPrivateConstructor() {
try {
Class<?> classStudent = Class.forName("TestEnum");
// 后两个为父类 Enum 的参数,可将 key 类型改为 double 进一步验证
Constructor<?> declaredConstructorStudent =
classStudent.getDeclaredConstructor(String.class, int.class, String.class, int.class);
declaredConstructorStudent.setAccessible(true);
Object objectStudent = declaredConstructorStudent.newInstance("父类参数", 666, "子类参数", 888);
TestEnum testEnum = (TestEnum) objectStudent;
System.out.println("获得枚举的私有构造函数:" + testEnum);
} catch (Exception ex) {
ex.printStackTrace();
}
}
此时再运行,得到的是我们想要的"最终答案":
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:416)
at TestEnum.reflectPrivateConstructor(TestEnum.java:46)
at TestEnum.main(TestEnum.java:55)
这次异常换成了 IllegalArgumentException,报错点落在 newInstance() 这个方法上。查看它的源码可以看到,JDK 在 newInstance() 内部专门做了一个判断:只要构造器所属的类被标记为枚举(Modifier.isEnum(...)),就直接抛出 IllegalArgumentException("Cannot reflectively create enum objects")。
结论:不能通过反射创建枚举类的实例。 这正是"为什么用枚举实现单例模式是安全的"的原因——哪怕攻击者拿到反射手段,也无法反射地再 new 出一个新的枚举实例,从而保证了枚举单例的唯一性。这是一道经典的阿里面试题(2017 年),同学们务必记住这个坑。
2.5 总结
- 枚举本身就是一个类,其构造方法默认为私有的,且都默认继承自
java.lang.Enum。 - 枚举可以避免反射和序列化破坏单例的问题。
- 掌握枚举的优点和缺点(见 2.3)。
2.6 面试问题(单例模式学完后可以回顾)
下面给出三种常见的单例写法,作为本章收尾与面试延伸。
1. 双重检查锁(DCL)实现单例
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
// 进入同步区域后,再检查一次,如果仍是 null,才创建实例
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}要点:两次 null 判断(“双重检查”)保证多线程下只创建一个实例;volatile 关键字防止指令重排序,避免出现"拿到未初始化完的对象"的情况。
2. 静态内部类实现单例
class Singleton {
/** 私有化构造器 */
private Singleton() {}
/** 对外提供公共的访问方法 */
public static Singleton getInstance() {
return UserSingletonHolder.INSTANCE;
}
/** 写一个静态内部类,在内部类中实例化外部类 */
private static class UserSingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
public class Main {
public static void main(String[] args) {
Singleton u1 = Singleton.getInstance();
Singleton u2 = Singleton.getInstance();
System.out.println("两个实例是否相同:" + (u1 == u2));
}
}要点:利用"类的加载机制"实现懒加载与线程安全——只有在调用 getInstance() 触发内部类被加载时才会创建实例,由 JVM 保证 INSTANCE 的初始化只发生一次。
3. 枚举实现单例
public enum TestEnum {
INSTANCE;
public TestEnum getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
TestEnum singleton1 = TestEnum.INSTANCE;
TestEnum singleton2 = TestEnum.INSTANCE;
System.out.println("两个实例是否相同:" + (singleton1 == singleton2));
}
}要点:枚举的每个常量在类加载时只会实例化一次,天然线程安全,而且如 2.4 所述无法被反射破坏,是《Effective Java》推荐的实现单例的最佳方式。
3. Lambda 表达式
3.1 背景与基本语法
Lambda 表达式是 Java SE 8 中一个重要的新特性。它允许你通过表达式来代替功能接口(函数式接口)。Lambda 表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。Lambda 表达式(Lambda expression)基于数学中的 λ 演算得名,也可称为闭包(Closure)。
基本语法:
(parameters) -> expression
或
(parameters) -> { statements; }
Lambda 表达式由三部分组成:
- parameters:类似方法中的形参列表,这里的参数就是函数式接口里的参数。参数类型可以显式声明,也可以不声明而由 JVM 隐含推断;当只有一个参数且为推断类型时,可以省略圆括号。
->:可理解为"被用于 / 指向"的意思,把左侧的参数传给右侧的方法体。- 方法体:可以是表达式,也可以是代码块,是函数式接口里那个抽象方法的实现。代码块可以返回一个值或者什么都不返回,它等同于方法的方法体;如果是表达式,同样可以返回一个值或者什么都不返回。
下面看几组直观示例:
// 1. 不需要参数,返回值为 2 () -> 2 // 2. 接收一个参数(数字类型),返回其 2 倍的值 x -> 2 * x // 3. 接受 2 个参数(数字),并返回它们的和 (x, y) -> x + y // 4. 接收 2 个 int 型整数,返回它们的乘积 (int x, int y) -> x * y // 5. 接受一个 String 对象,并在控制台打印,不返回任何值(相当于返回 void) (String s) -> System.out.print(s)
3.1.2 函数式接口
要了解 Lambda 表达式,首先需要了解什么是函数式接口。函数式接口的定义:一个接口有且只有一个抽象方法。
注意:
- 如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口。
- 如果我们在某个接口上声明了
@FunctionalInterface注解,那么编译器就会按照函数式接口的定义来要求该接口——此时若接口里有两个抽象方法,程序编译就会报错。从某种意义上说,只要你保证接口中只有一个抽象方法,可以不加这个注解;加上之后编译器会自动帮你做检测。
定义方式:
@FunctionalInterface
interface NoParameterNoReturn {
// 注意:只能有一个方法
void test();
}
接口里还可以有 default 默认方法(它不是抽象方法),下面的写法同样合法:
@FunctionalInterface
interface NoParameterNoReturn {
void test();
default void test2() {
System.out.println("JDK1.8 新特性,default 默认方法可以有具体的实现");
}
}
补充:除了唯一的那个抽象方法外,函数式接口中还可以有
default方法、static方法,以及从Object类中重写的方法(如equals),它们都不会破坏"只有一个抽象方法"的判定。
3.2 Lambda 表达式的基本使用
首先,我们事先准备好几个函数式接口,覆盖"有无返回值 × 参数个数"六种组合:
// 无返回值、无参数
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
// 无返回值、一个参数
@FunctionalInterface
interface OneParameterNoReturn {
void test(int a);
}
// 无返回值、多个参数
@FunctionalInterface
interface MoreParameterNoReturn {
void test(int a, int b);
}
// 有返回值、无参数
@FunctionalInterface
interface NoParameterReturn {
int test();
}
// 有返回值、一个参数
@FunctionalInterface
interface OneParameterReturn {
int test(int a);
}
// 有返回值、多个参数
@FunctionalInterface
interface MoreParameterReturn {
int test(int a, int b);
}我们在上面提到过,Lambda 可以理解为:Lambda 就是匿名内部类的简化,实际上仍然创建了一个类、实现了接口、并重写了接口的方法。
先看没有使用 Lambda 时(匿名内部类)的调用方式:
NoParameterNoReturn noParameterNoReturn = new NoParameterNoReturn() {
@Override
public void test() {
System.out.println("hello");
}
};
noParameterNoReturn.test();使用 Lambda 之后,六种接口分别对应如下写法:
public class TestDemo {
public static void main(String[] args) {
NoParameterNoReturn noParameterNoReturn = () -> {
System.out.println("无参数无返回值");
};
noParameterNoReturn.test();
OneParameterNoReturn oneParameterNoReturn = (int a) -> {
System.out.println("一个参数无返回值:" + a);
};
oneParameterNoReturn.test(10);
MoreParameterNoReturn moreParameterNoReturn = (int a, int b) -> {
System.out.println("多个参数无返回值:" + a + " " + b);
};
moreParameterNoReturn.test(20, 30);
NoParameterReturn noParameterReturn = () -> {
System.out.println("有返回值无参数!");
return 40;
};
// 接收函数的返回值
int ret = noParameterReturn.test();
System.out.println(ret);
OneParameterReturn oneParameterReturn = (int a) -> {
System.out.println("有返回值有一个参数!");
return a;
};
ret = oneParameterReturn.test(50);
System.out.println(ret);
MoreParameterReturn moreParameterReturn = (int a, int b) -> {
System.out.println("有返回值多个参数!");
return a + b;
};
ret = moreParameterReturn.test(60, 70);
System.out.println(ret);
}
}可以看到,凡是形如 new 接口名(){ @Override ... } 的匿名内部类,在函数式接口的前提下都能被压缩成 参数 -> 方法体 的形式,代码量显著减少。
3.2.1 语法精简
Lambda 的书写规则可以进一步精简,共四条:
- 参数类型可以省略;如果要省略,则每个参数的类型都要省略(要么都带、要么都不带)。
- 参数的小括号里只有一个参数时,小括号可以省略。
- 如果方法体当中只有一句代码,那么大括号可以省略。
- 如果方法体中只有一条语句,且是
return语句,那么大括号可以省略,且要去掉return关键字。
示例代码:
public static void main(String[] args) {
// 省略参数类型
MoreParameterNoReturn moreParameterNoReturn = (a, b) -> {
System.out.println("无返回值多个参数,省略参数类型:" + a + " " + b);
};
moreParameterNoReturn.test(20, 30);
// 只有一个参数,省略小括号
OneParameterNoReturn oneParameterNoReturn = a -> {
System.out.println("一个参数无返回值,小括号可以省略:" + a);
};
oneParameterNoReturn.test(10);
// 方法体中只有一行代码,省略大括号
NoParameterNoReturn noParameterNoReturn = () ->
System.out.println("无参数无返回值,方法体中只有一行代码");
noParameterNoReturn.test();
// 方法体中只有一条语句,且是 return 语句,省略大括号和 return 关键字
NoParameterReturn noParameterReturn = () -> 40;
int ret = noParameterReturn.test();
System.out.println(ret);
}注:原文中"小括号可以胜率"系"省略"的笔误,已修正。
3.3 变量捕获
Lambda 表达式中存在变量捕获。了解了变量捕获之后,我们才能更好地理解 Lambda 表达式的作用域。事实上,Java 当中的匿名类里同样存在变量捕获。
3.3.1 匿名内部类
匿名内部类就是没有名字的内部类。这里只是为了说明变量捕获,所以匿名内部类只要会使用即可。
class Test {
public void func() {
System.out.println("func()");
}
}
public class TestDemo {
public static void main(String[] args) {
new Test() {
@Override
public void func() {
System.out.println("我是内部类,且重写了 func 这个方法!");
}
};
}
}在上述 main 函数里,new Test(){ @Override ... } 就是一个匿名内部类的简单使用:它现场定义了一个 Test 的子类并重写了 func 方法。
3.3.2 匿名内部类的变量捕获
class Test {
public void func() {
System.out.println("func()");
}
}
public class TestDemo {
public static void main(String[] args) {
int a = 100;
new Test() {
@Override
public void func() {
System.out.println("我是内部类,且重写了 func 这个方法!");
System.out.println("我是捕获到变量 a == " + a
+ " 我是一个常量,或者是一个没有改变过值的变量!");
}
};
}
}上面代码中的变量 a 就是"被捕获的变量"。这个变量要么被 final 修饰;如果不是 final 修饰的,你要保证在使用之前它没有被修改过(即"事实上的最终变量 / effectively final")。如下代码就是错误的:
public class TestDemo {
public static void main(String[] args) {
int a = 100;
new Test() {
@Override
public void func() {
a = 99; // 编译错误:被内部类捕获的局部变量不能被修改
System.out.println("我是内部类,且重写了 func 这个方法!");
System.out.println("我是捕获到变量 a == " + a
+ " 我是一个常量,或者是一个没有改变过值的变量!");
}
};
}
}该代码会直接编译报错。原因:局部变量存放在栈帧中,方法执行完即销毁;而内部类对象可能比这个栈帧活得更久。为保证内部类里读到的值是一致的,Java 规定被捕获的局部变量必须是常量或事实上的常量。
3.3.3 Lambda 的变量捕获
在 Lambda 当中也可以进行变量捕获,规则与匿名内部类一致,代码如下:
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
public static void main(String[] args) {
int a = 10;
NoParameterNoReturn noParameterNoReturn = () -> {
// a = 99; // error:被捕获的变量不能再修改
System.out.println("捕获变量:" + a);
};
noParameterNoReturn.test();
}一旦把注释里的 a = 99; 放开,同样会编译报错——Lambda 捕获的局部变量也必须是常量或事实上的常量。
3.4 Lambda 在集合当中的使用
为了让 Lambda 能和 Java 的集合类更好地一起使用,集合当中也新增了部分接口,以便与 Lambda 表达式对接。这些方法的具体作用可自行查阅帮助手册,这里示例几个常用方法的使用。
注意:
Collection的forEach()方法是从java.lang.Iterable接口继承过来的。
3.4.1 Collection 接口
forEach() 方法演示。该方法在接口 Iterable 当中,原型如下:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
该方法表示:对容器中的每个元素执行 action 指定的动作。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("bit");
list.add("hello");
list.add("lambda");
list.forEach(new Consumer<String>() {
@Override
public void accept(String str) {
// 简单遍历集合中的元素
System.out.print(str + " ");
}
});
}
输出结果:Hello bit hello lambda
我们可以用 Lambda 把它改写得更简洁:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("bit");
list.add("hello");
list.add("lambda");
// 表示调用一个不带返回值的、带一个参数的方法,花括号内即为原来的函数体内容
list.forEach(s -> {
System.out.println(s);
});
}
3.4.2 List 接口
sort() 方法演示。sort 方法源码:该方法根据 c 指定的比较规则对容器元素进行排序。
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
使用示例(用匿名内部类传入比较规则):
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("bit");
list.add("hello");
list.add("lambda");
list.sort(new Comparator<String>() {
@Override
public int compare(String str1, String str2) {
// 注意这里比较的是长度
return str1.length() - str2.length();
}
});
System.out.println(list);
}
输出结果:[bit, Hello, hello, lambda]
改写为 Lambda 表达式后:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("bit");
list.add("hello");
list.add("lambda");
// 调用带有 2 个参数的方法,且返回长度的差值
list.sort((str1, str2) -> str1.length() - str2.length());
System.out.println(list);
}
输出结果同样是:[bit, Hello, hello, lambda]
3.4.3 Map 接口
HashMap 的 forEach()。该方法原型如下:
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
作用是对 Map 中的每个映射执行 action 指定的操作。注意 Map 版的 forEach 接收的是 BiConsumer(带两个入参:key 与 value),与 Collection 版的 Consumer(单入参)不同。
代码示例(匿名内部类写法):
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "hello");
map.put(2, "bit");
map.put(3, "hello");
map.put(4, "lambda");
map.forEach(new BiConsumer<Integer, String>() {
@Override
public void accept(Integer k, String v) {
System.out.println(k + "=" + v);
}
});
}
输出结果:
1=hello 2=bit 3=hello 4=lambda
使用 Lambda 表达式后的代码:
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "hello");
map.put(2, "bit");
map.put(3, "hello");
map.put(4, "lambda");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}
输出结果:
1=hello 2=bit 3=hello 4=lambda
说明:
HashMap遍历顺序取决于哈希实现,上例中恰好按插入顺序输出;若换用LinkedHashMap可保证严格插入顺序。
3.5 总结
Lambda 表达式的优点很明显,在代码层次上使代码变得非常简洁;缺点也很明显,代码不易读。
优点:
- 代码简洁,开发迅速。
- 方便函数式编程。
- 非常容易进行并行计算(配合 Stream 的
parallelStream)。 - Java 引入 Lambda,改善了集合操作。
缺点:
- 代码可读性变差(尤其是嵌套、链式调用较长时)。
- 在非并行计算中,很多计算未必有传统
for循环的性能高。 - 不容易进行调试(Lambda 中的异常栈不如普通方法直观)。
全篇小结:反射提供了"运行时操作类结构"的能力,是框架的底层支撑,但要为性能与封装性付出代价,务必在安全环境下使用;枚举用类型安全的方式组织常量,且天然抗反射、抗序列化破坏,是实现单例的最佳选择;Lambda 则把函数当作参数传递,让集合与函数式编程的写法更简洁。三者共同构成了 Java 从"面向对象"迈向"兼顾函数式风格"的重要基础。
到此这篇关于深入解析Java 反射、枚举与 Lambda 表达式的文章就介绍到这了,更多相关Java 反射、枚举与 Lambda 表达式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
SpringBoot使用@Cacheable出现预览工具乱码的解决方法
直接使用注解进行缓存数据,我们再使用工具去预览存储的数据时发现是乱码,这是由于默认序列化的问题,所以接下来将给大家介绍一下SpringBoot使用@Cacheable出现预览工具乱码的解决方法,需要的朋友可以参考下2023-10-10
java项目怎么集成stable diffusion图文生成算法
在开发Java项目过程中,我们经常需要使用消息传递来实现不同组件之间的通信,Stable Diffusion是一种基于消息传递的实时通信解决方案,使用Java调用外部服务(如Python脚本或API服务),这些服务运行Stable Diffusion模型,本文将介绍如何将Stable Diffusion集成到Java项目2024-07-07
java通过HttpServletRequest获取post请求中的body内容的方法
本篇文章主要介绍了java通过HttpServletRequest获取post请求中的body内容的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2018-02-02


最新评论