Java反射与枚举的使用
1. Java反射机制
1.1 反射的定义
-
反射的概念:反射(reflection)机制是指在运行过程中,对于任意一个类,能够获得这个类的所有内部信息。并且对于任何一个对象,能够访问并修改到他的所有属性和调用方法的功能,反射机制是动态语言的关键。
-
反射的用途和场景:
- 在日常开发过程中,我们经常会碰到一个类的属性或者方法是私有的,或者只对内部系统开放。那么这个时候我们就可以通过反射机制绕过封装,获取到所需要的属性和方法
- 在各类通用框架中非常常见,例如说Spring框架内部由Spring容器来管理Bean,例如spring读取XML配置文件,而配置文件中就包含了各个类及其之间的内部关系信息,当spring进行依赖注入时就是通过这些类信息,利用反射机制动态创建出类的实例及其依赖关系。
-
反射的基本信息:Java中许多对象在运行时会出现两种类型:运行时类型、编译时类型,例如说
Person p = new Student()
,这句代码中p的编译时类型为Person,运行时类型为Student,而程序在运行过程中需要知道对象和类的真实信息,通过反射机制就可以进行判断。 -
反射的起源:当Java文件被编译时会生成以
.class
为后缀的字节码文件,该字节码文件被JVM解析为一个对象,这个对象就是Java.lang.Class
,这样一来,每一个Java源程序最终都会变为Java.lang.Class类对象的一个实例,我们就是通过反射机制访问到了这个实例,并且能够访问甚至修改该对象的属性及其方法。
1.2 反射相关类和方法
常用获得类相关的方法
方法 | 用途 |
---|---|
getClassLoader() | 获得类的加载器 |
getDeclaredClasses() | 返回一个数组,数组中包含该类中所有的类和对象 |
forName(String className) | 根据类名返回该类的对象 |
newInstance() | 创建类的实例 |
getName() | 获取该类的完整路径名字 |
常用获得类中属性相关的方法
方法 | 用途 |
---|---|
getField(String name) | 获得某个公有的属性字段 |
getDeclaredField(String name) | 获得某个属性字段 |
getFields() | 获得所有公有的属性字段 |
getDeclaredFields() | 获得所有属性字段 |
获得类中构造器相关的方法
方法 | 用途 |
---|---|
getConstructor(Class<?>… parameterTypes) | 获取与参数类型匹配的公有构造方法 |
getDeclaredConstructor(Class<?>… parameterTypes) | 获取与参数类型匹配的构造方法 |
getConstructors() | 获取所有公有的构造方法 |
getDeclaredConstructors() | 获取所有构造方法 |
获得类中方法相关的方法
方法 | 用途 |
---|---|
getMethod(String name, Class<?>… parameterTypes) | 获取名称与参数类型匹配的公有方法 |
getDeclaredMethod(String name, Class<?>… parameterTypes) | 获取名称与参数类型匹配的方法 |
getMethods() | 获得该类的所有公有方法 |
getDeclaredMethods() | 获得该类的所有方法 |
1.3 反射的使用
注:所有与反射相关的类都在java.lang.reflect
包中
1.3.1 获取Class对象的三种方式
在反射之前,我们首先需要的就是获取需要反射的类的Class对象,然后通过Class对象的核心方法来达到反射的目的。
方式一:如果已知类的全路径名(如果存在包名需要加上包名),那么我们可以通过Class.forName(String className)
的方式获取Class对象
方式二:如果在编译前已经确定好需要进行反射的类时,可以通过类.class的方式获取Class对象
方式三:通过类的对象调用getClass()方法获取Class对象
示例:
import org.junit.Test;
class Student {
public String name = "jack";
private int age = 18;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
private void eat(String foodName) {
System.out.println(this.name + "在吃" + foodName);
}
public void learn() {
System.out.println(this.name + "在学习...");
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TestReflect {
@Test
public void test_getClass() throws ClassNotFoundException {
// 1. 通过全路径名方式获取
Class<?> clazz1 = Class.forName("Student");
System.out.println(clazz1);
// 2. 通过类.class方式获取
Class<?> clazz2 = Student.class;
System.out.println(clazz2);
// 3. 通过类对象.getClass()方式获取
Class<?> clazz3 = new Student().getClass();
System.out.println(clazz3);
// 验证class对象是否相同
System.out.println(clazz1 == clazz2 && clazz1 == clazz3); // true
}
}
注:我们经常通过第一种方式即全路径名来获取Class对象,因为更加动态灵活可扩展,例如我们可以将类路径名写在配置文件中,这样我们就避免了硬编码的内伤,做到了灵活可扩展。
1.3.2 创建反射对象
我们可以直接通过Class的newInstance方法创建反射类的对象(不推荐)
语法格式:Object obj = Class.forName(className).newInstance()
示例(以上述的Student对象为例):
@Test
public void test_getInstance() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class<?> studentClazz = Class.forName("Student");
// 通过newInstance()获取实例
Student student = (Student) studentClazz.newInstance();
System.out.println(student); // Student{name='jack', age=18}
}
1.3.3 通过反射获取指定构造器
我们可以通过Class的getDeclaredContructor()方法获取指定的构造器
语法格式:Class.forName(className).getDeclaredConstructor(Class<?>... params)
示例(以上述的Student对象为例):
@Test
public void test_getDeclaredField() throws Exception {
Class<?> studentClazz = Class.forName("Student");
// 1. 先构造对象
Student student = (Student) studentClazz.newInstance();
// 2. 获取指定字段
Field field = studentClazz.getDeclaredField("age");
// 3. 如果是私有字段,需要设置访问权限
field.setAccessible(true);
// 4. 调用set方法
field.set(student, 25);
System.out.println(student); // Student{name='jack', age=25}
}
1.3.4 通过反射获取指定的字段
我们可以通过Class的getDeclaredField(String name)方法获取指定的字段
语法格式:Class.forName(className).getDeclaredField(String name)
使用步骤:
- 使用
Field field = getDeclaredField(String name)
方法获取指定字段 - 创建该反射类的实例对象
obj
- 如果该字段修饰符为
private
,那么需要额外设置访问权限:field.setAccessible(true)
- 使用
field.set(Object obj, Object value)
方法设置指定对象的字段值
@Test
public void test_getDeclaredField() throws Exception {
Class<?> studentClazz = Class.forName("Student");
// 1. 先构造对象
Student student = (Student) studentClazz.newInstance();
// 2. 获取指定字段
Field field = studentClazz.getDeclaredField("age");
// 3. 如果是私有字段,需要设置访问权限
field.setAccessible(true);
// 4. 调用set方法
field.set(student, 25);
System.out.println(student); // Student{name='jack', age=25}
}
1.3.5 通过反射获取指定的方法
我们可以通过Class的getDeclaredMethod()方法获取指定的方法
语法格式:Class.forName(className).getDeclaredMethod(String name, Class<?>... params)
使用步骤:
- 使用
Method method = Class.forName(className).getDeclaredMethod(String name, Class<?>... params)
获取指定方法 - 创建该反射类的实例对象
obj
- 如果该方法修饰符为
private
,那么需要额外设置访问权限:method.setAccessible(true)
- 使用
method.invoke(Object obj, Object... params)
调用方法
@Test
public void test_getDeclaredMethod() throws Exception {
// 1. 获取Class对象
Class<?> studentClazz = Class.forName("Student");
// 2. 获取指定方法
Method method = studentClazz.getDeclaredMethod("eat", String.class);
// 3. 如果method为私有,额外设置访问权限
method.setAccessible(true);
// 4. 指定调用方法的对象
Student student = (Student) studentClazz.newInstance();
// 5. 调用方法
method.invoke(student, "水果"); // jack在吃水果
}
1.4 反射总结
反射优点:
- 对于任意一个类来说,通过反射可以获取它的所有内部信息,对于任意一个对象来说,能够访问甚至修改它的属性以及调用其方法
- 反射增加了程序的灵活性和可扩展性,提高自适应能力
- 已经被大量运用于框架源码中,如Hibernate、Spring中
反射缺点:
- 使用反射会具有效率问题
- 使用反射绕过了源代码封装等机制,且实现较为复杂,可读性不高
2. Java枚举的使用
2.1 枚举的背景和定义
枚举:枚举是在JDK1.5之后引入的,其用途是将一组常量组织起来进行管理
本质:枚举的本质就是java.lang.Enum
的子类,也就是说如果自己实现了一个枚举类,默认继承自java.lang.Enum
类
2.2 枚举的使用
-
枚举字段配合switch语句使用
public enum TestEnum { RED, GREEN, BLUE; public static void main(String[] args) { TestEnum color = TestEnum.RED; switch (color) { case RED: System.out.println("红色"); break; case GREEN: System.out.println("绿色"); break; case BLUE: System.out.println("蓝色"); break; default: System.out.println("其他"); break; } } }
-
枚举的常用方法
方法 描述 values() 以数组的形式返回所有的枚举类型成员 valueOf(“”) 将指定的字符串转化为对应枚举实例 ordinal() 返回枚举成员的索引位置 compareTo() 比较两个枚举成员在定义时的顺序 示例:
public enum TestEnum { RED, GREEN, BLUE; public static void main(String[] args) { // 1. 通过values方法返回所有枚举类型数组 TestEnum[] testEnums = TestEnum.values(); for (TestEnum testEnum : testEnums) { System.out.println(testEnum); } // 2. 通过valueOf方法返回指定名称的枚举类型 TestEnum testEnum = TestEnum.valueOf("BLUE"); System.out.println(testEnum); // BLUE // 3. 通过ordinal方法返回定义的索引 System.out.println(testEnum.ordinal()); // 2 // 4. 通过compareTo方法比较定义的顺序 TestEnum red = TestEnum.RED; TestEnum green = TestEnum.GREEN; if (red.compareTo(green) < 0) { System.out.println(red + "在" + green + "前面"); // RED在GREEN前面 } else if (red.compareTo(green) > 0) { System.out.println(green + "在" + red + "前面"); } } }
-
枚举类也可以定义构造方法
(重要)注:枚举类的构造方法默认是私有的
public enum TestEnum { RED("红色", 0), GREEN("绿色", 1), BLUE("蓝色", 2); private String name; private int key; private TestEnum(String name, int key) { this.name = name; this.key = key; } public static TestEnum getEnumKey(int key) { for (TestEnum testEnum : TestEnum.values()) { if (testEnum.key == key) { return testEnum; } } return null; } public static void main(String[] args) { System.out.println(getEnumKey(2)); // BLUE } }
2.3 枚举和反射
我们刚才介绍了反射机制可以调用类对象的私有方法,现在我们看到枚举类型的构造方法是私有的,我们是否也能够尝试利用反射机制获取对应的实例对象呢?
当我们尝试编写如下代码时却报错了:
public static void main(String[] args) throws Exception {
// 尝试利用反射机制获取实例
Class<?> clazz = Class.forName("TestEnum");
// 获取构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
// 设置访问权限
constructor.setAccessible(true);
// 创建实例
TestEnum testEnum = (TestEnum) constructor.newInstance("黑色", 5);
System.out.println(testEnum);
}
我们根据异常信息可以推断出NoSuchMethodException
异常,我们可以意识到由于编译器会自动完成对其父类的初始化,所以我们至少需要4个参数,其中两个参数用于父类初始化,所以我们将代码更改如下:
public static void main(String[] args) throws Exception {
// 尝试利用反射机制获取实例
Class<?> clazz = Class.forName("TestEnum");
// 获取构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class, String.class, int.class);
// 设置访问权限
constructor.setAccessible(true);
// 创建实例
TestEnum testEnum = (TestEnum) constructor.newInstance("黑色", 5);
System.out.println(testEnum);
}
但是又抛出了如下异常:
此时我们可以得出结论:枚举类型是不允许被反射创建的!!!
2.4 枚举总结
- 枚举实质上是一个类,其构造方法是私有的,这个类默认继承自
java.lang.Enum
- 枚举可以避免反射和序列化问题
- 枚举是比较安全的(不能被反射实例化对象)——因此可以实现安全的单例模式