1.反射
1.1 出现背景
Java 程序中,所有的对象都有两种类型:编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致。 Object obj = new String(“hello”);
obj.getClass()
例如:某些变量或形参的声明类型是 Object 类型,但是程序却需要调用该对象运行时类型的方法,该方法不是 Object 中的方法,那么如何解决呢?
解决这个问题,有两种方案:
- 方案 1:在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用 instanceof 运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
- 方案 2:编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
1.2 介绍
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
1.3 反射机制提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成动态代理
1.4 反射相关的API
- java.lang.Class:代表一个类
- java.lang.reflect.Method:代表类的方法
- java.lang.reflect.Field:代表类的成员变量
- java.lang.reflect.Constructor:代表类的构造器
1.5 反射的优缺点
1.5.1 优点
- 提高了 Java 程序的灵活性和扩展性,降低了耦合性,提高自适应能力。
- 允许程序创建和控制任何类的对象,无需提前硬编码目标类。
1.5.2 缺点
- 反射的性能较低。
- 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
- 反射会模糊程序内部逻辑,可读性较差。
2.Class类
2.1 介绍
针对于给写好的.java源文件进行编译(使用javac.exe),会生成一个或多个.class字节码文件。接下来,我们使用java.exe命令对指定的.class文件进行解释运行。这个解释运行的过程中,我们需要将.class字节码文件加载(使用类的加载器)到内存中(存放在方法区)。加载到内存中的.class文件对应的结构即为Class的一个实例。
比如。加载到内存中的Person类或String类或User类,都作为Class的一个一个的实例。
class clazz1 = Person.class;
class claz2z2 = String.class;
class clazz3 = User.class;
class clazz4 = Comparable.class;
说明:运行时类在内存中会缓存起来,在整个执行期间只会加载一次。
2.2 获取Class类的实例
- 方式 1:要求编译期间已知类型
- 前提:若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高。
- 方式 2:获取对象的运行时类型
- 前提:已知某个类的实例,调用该实例的 getClass()方法获取 Class 对象。
- 方式 3:可以获取编译期间未知的类型(使用最多)
- 前提:已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法forName()获取,可能抛出 ClassNotFoundException。
- 方式 4:其他方式(不做要求)
- 前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型。
@Test
public void test1() throws ClassNotFoundException {
//1.调用运行时类的静态属性class
Class clazz1 = User.class;
System.out.println(clazz1);
//2.调用运行时类的对象的getClass()
User user = new User();
Class clazz2 = user.getClass();
System.out.println(clazz2);
//3.调用Class的静态方法forName(String className)
Class clazz3 = Class.forName("p145.User");
System.out.println(clazz3);
//4.使用类的加载器
Class<?> clazz4 = ClassLoader.getSystemClassLoader().loadClass("p145.User");
System.out.println(clazz4);
}
2.3 Class对象可用于的类型
所以Java都可以使用。
- class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
- interface:接口
- []:数组
- enum:枚举
- annotation:注解@interface
- primitive type:基本数据类型
- void
说明:只要元素类型与维度相同,就是同一个Class。
3.类的加载过程
- 过程1:类的加载(loading)
- 将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象。此过程由类加载器完成。
- 过程2:类的链接(linking)
- ① 验证Verify:确保加载的类信息符合 JVM 规范,例如:以 cafebabe 开头,没有安全方面的问题。
- ② 准备(Prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- ③ 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
- 过程3:类的初始化(initialization)
- 执行类构造器()方法的过程。类构造器()方是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
4.类的加载器(以JDK8为例)
4.1 作用
负责类的加载,并对应于一个Class的实例。
4.2 分类
4.2.1 BootstrapClassLoader
引导类加载器/启动类加载器。
- 使用 C/C++语言实现的,不能通过Java代码获取其实例。
- 负责加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下的内容)。
4.2.2 ExtensionClassLoader
扩展类加载器。
- 继承于 ClassLoader 类。
- 负责加载从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext子目录下加载类库。
4.2.3 SystemClassLoader/ApplicationClassLoader
系统类加载器/应用程序类加载器。
- 继承于 ClassLoader 类。
- 我们自定义的类,默认使用的类的加载器。
4.2.4 用户自定义类的加载器
- 继承于 ClassLoader 类。
5.反射的基本应用
5.1 创建运行时类的对象
5.1.1 实现方法
通过Class的实例调用newInstance()方法即可。
5.1.2 成功创建对象的条件
- 要求运行时类中必须提供一个空参的构造器。
- 要求提供的空参的构造器的权限要足够。
5.1.3 JavaBean中要求给当前类提供公共的空参构造器的作用
- 子类对象在实例化时,子类构造器的首行默认调用父类空参的构造器。
- 在反射中,经常用来创建运行时类的对象。那么我们要求各个运行时类都提供一个空参的构造器,便于我们编写创建运行时类对象的代码。
5.1.4 在JDK9中被替换的结构
在JDK9中被标识为过时,替换为通过Constructor类调用newInstance(…)。
5.2 获取运行时类的内部结构
- 获取运行时类的内部的结构:所有属性、所有方法、所有构造器。
- 获取运行时类的内部结构:父类、接口、包、带泛型的父类、父类的泛型等。
public class OtherTest {
@Test
public void test1() throws ClassNotFoundException {
//获取运行时类的父类
Class clazz = Class.forName("p129.test1.Person");
Class superClass = clazz.getSuperclass();
System.out.println(superClass);
}
@Test
public void test2() throws ClassNotFoundException {
//获取运行时类实现的接口
Class clazz = Class.forName("p129.test1.Person");
Class[] interfaces = clazz.getInterfaces();
for (Class c :
interfaces) {
System.out.println(c);
}
}
@Test
public void test3() throws ClassNotFoundException {
//获取运行时类所在的包
Class clazz = Class.forName("p129.test1.Person");
Package aPackage = clazz.getPackage();
System.out.println(aPackage);
}
@Test
public void test4() throws ClassNotFoundException {
//获取运行时类带泛型的父类
Class clazz = Class.forName("p129.test1.Person");
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass);
}
@Test
public void test5() throws ClassNotFoundException {
//获取运行时类的父类的泛型
Class clazz = Class.forName("p129.test1.Person");
//获取带泛型的父类(Type是一个接口,Class实现了该接口)
Type superclass = clazz.getGenericSuperclass();
//如果父类是带泛型的,则可以强转为ParameterizedType
ParameterizedType paramType = (ParameterizedType) superclass;
//调用getActualTypeArguments()获取泛型参数,结果是一个数组,因为可能有多个泛型参数
Type[] arguments = paramType.getActualTypeArguments();
//获取泛型参数的名称
System.out.println(((Class)arguments[0]).getName());
}
}
5.3 调用指定的结构
指的是调用指定的属性、方法和构造器。
5.3.1 调用指定属性
- 通过Class实例调用getDeclaredField(String fieldName)获取运行时类指定名的属性。
- 确保此属性可以访问。
- 通过Field的实例调用get和set方法来获取或设置此属性的值。
public class ReflectTest {
@Test
public void test2() throws Exception {
Class clazz = Person.class;
Person per = (Person) clazz.newInstance();
//1.通过Class实例调用getDeclaredField(String fieldName)获取运行时类指定名的属性
Field nameFiled = clazz.getDeclaredField("name");
//2.确保此属性可以访问
nameFiled.setAccessible(true);
//3.通过Field的实例调用get和set方法来获取或设置此属性的值
nameFiled.set(per,"张三");
System.out.println(nameFiled.get(per));
}
}
5.3.2 调用指定方法
- 通过Class实例调用getDeclaredMethod(String methodName,Class…args)获取指定方法。
- 确保此方法可以访问。
- 通过Method的实例调用invoke(Object obj,Object…args),即为对Method对应方法的调用。
- invoke方法的返回值即为Method对应方法的返回值。
- 若Method对应方法的返回值为void,则invoke方法返回值为null。
@Test
public void test3() throws Exception {
Class clazz = Person.class;
Person per = (Person) clazz.newInstance();
//1.通过Class实例调用getDeclaredMethod(String methodName,Class...args)获取指定方法
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
//2.确保此方法可以访问
setNameMethod.setAccessible(true);
//3.通过Method的实例调用invoke(Object obj,Object...args),即为对Method对应方法的调用
//invoke方法的返回值即为Method对应方法的返回值
//若Method对应方法的返回值为void,则invoke方法返回值为null
Object returnValue = setNameMethod.invoke(per, "10");
System.out.println(returnValue);
}
5.3.3 调用指定构造器
- 通过Class实例调用getDeclaredConstructor(Class…args)获取指定参数类型的构造器。
- 确保此方法可以访问。
- 通过Constructor的实例调用newInstance(Object…args),返回一个运行时类的实例。
@Test
public void test4() throws Exception {
Class clazz = Person.class;
//1.通过Class实例调用getDeclaredConstructor(Class...args)获取指定参数类型的构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
//2.确保此方法可以访问
constructor.setAccessible(true);
//3.通过Constructor的实例调用newInstance(Object...args),返回一个运行时类的实例
Person per = (Person) constructor.newInstance("tom", 12);
System.out.println(per);
}
5.4 获取注解信息
6.反射的动态性
public class FruitTest {
@Test
public void test1() throws Exception {
//1.读取配置文件中的信息,获取全类名
Properties pros = new Properties();
File file = new File("C:\\Users\\22923\\Desktop\\Java\\Java SE\\src\\p192\\config.properties");
FileInputStream fis = new FileInputStream(file);
pros.load(fis);
String fruitName = pros.getProperty("fruitName");
//2.通过反射创建指定全类名对应的类的实例
Class clazz = Class.forName(fruitName);
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Fruit fruit = (Fruit) constructor.newInstance();
//3.通过榨汁机对象调用run()
fruit.squeeze();
}
}