反射学习笔记(一)
Java反射机制
- 反射被视为动态语言的关键,反射机制允许程序在运行期间借助于反射相关的API取得任何内部类的内部消息,并且直接操作任意对象的内部属性和方法。
- 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息,我们可以通过这个对象看到类的结构,这就是反射。
- 动态语言:在运行时可以改变其结构的语言,例如:新的函数,对象,甚至代码可以被引进,已有的函数可以被删除或者其他结构上的变化。即在运行时代码可以根据自身条件改变自身结构。(C#,JS,PHP,Python等)
- 静态语言:运行时结构不可变的语言就是静态语言。(Java,C,C++)
- Java不是动态语言,但Java可以被称为“准动态语言”,即Java有一定的动态性,我们可以利用反射机制,字节码操作获得类似动态语言的特性。
反射机制提供的功能:
- 在运行时判断一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断一个类所具有的成员变量和方法。
- 在运行时获取泛型信息。
- 在运行时调用任意一个对象的成员变量和方法。
- 在运行时处理注解。
- 生成动态代理。
反射相关的API
- java.lang.Class:代表一个运行时类。
- java.lang.reflect.Method:代表类的方法。
- java.lang.reflect.Filed:代表类的成员变量。
- java.lang.reflect.Constructor:代表类的构造器。
获取Class类的实例
Class类的理解:
- 类加载的过程:程序经过“javac”命令以后,会生成一个或多个字节码文件(.class结尾)。接着我们使用“java”命令对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程称为类的加载。
- 运行时类:加载到内存中的类,此运行时类就是一个Class的一个实例,即一个Class的实例对应着一个运行时类。
- 加载到内存中的运行时类,会缓存一段时间(JVM的垃圾回收机制可以回收这些缓存的Class对象),在此时间内,我们可以通过不同方式来获取此运行时类(不同方式获取同一个)。
哪些类型可以有Class对象:
- class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类。
- interface:接口
- []:数组
- enum:枚举
- 注解
- 基本数据类型
- void
@Test
public void test2(){
Class cla1 = Object.class;
Class cla2 = Collection.class;
Class cla3 = String[].class;
Class cla4 = int[][].class;
Class cla5 = ElementType.class; // 枚举
Class cla6 = Override.class; // 注解
Class cla7 = int.class; //基本数据类型
Class cla8 = char.class;
Class cla9 = void.class;
Class cla10 = Class.class;
// 均不报错
int[] a = new int[10];
int[] b = new int[100];
Class acla = a.getClass();
Class bcla = b.getClass();
// 只要数组的元素类型与维度相同,就是获取的Class实例就均指向同一个对象
System.out.println(acla == bcla); //true
}
获取Class的实例的方式:即获取运行使类
- 运行时类就是Class的一个实例。(调用运行时类的属性:.class)
- 通过运行时类的一个对象调用getClass()得到运行时类。
- 通过Class类的静态方法forName(“包名类名”),类在src下则不需要加包名。(常用!)
- 通过类加载器获取。
public class GainClassTest {
@Test
public void test1() throws ClassNotFoundException {
// 方式一:运行时类就是Class的一个实例
Class cla1 = Person.class;
System.out.println(cla1);
// 方式二:通过运行时类的一个对象调用getClass()得到运行类
Person p = new Person();
Class cla2 = p.getClass();
System.out.println(cla2);
// 方式三:通过Class类的静态方法forName(“包名类名”)
Class cla3 = Class.forName("com.solve.java.Person");
System.out.println(cla3);
// 通过类加载器获取
ClassLoader classLoader = GainClassTest.class.getClassLoader();
Class cla4 = classLoader.loadClass("com.solve.java.Person");
System.out.println(cla4);
// cla1~4均指向同一个运行时类Person
System.out.println(cla1 == cla2);
System.out.println(cla1 == cla3);
System.out.println(cla1 == cla4);
}
}
类的加载与类加载器
类的加载
类加载过程:当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载,类的连接,类的初始化这三个步骤对该类进行初始化。
- 类的加载(Load):将.class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的Java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址),所有需要访问和使用类数据只能通过这个Class对象,这个加载过程需要类加载器的参与。
- 类的链接(Link):将Java类的二进制代码合并到JVM的运行状态中的过程。
- 验证:确保加载的类信息符合JVM规范。
- 准备:正式为类变量(static修饰的变量)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
- 类的初始化(Initialize):
- 执行类构造器< clinit>()的过程,类构造器< clinit>()是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个类的< clinit>()在多线程环境中被正确加锁和同步。
< clinit>()注意点:
- 定义静态变量时赋值,再在静态代码块中修改其值
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(A.m);
}
static class A {
// 定义时赋值
static int m = 10;
// 静态代码块修改其值
static {
m = 20;
}
}
}
输出结果为:20
- 换下顺序:
static {
m = 20;
}
static int m = 10;
输出结果为:10
类加载的链接阶段结束后,此时类变量m的值为0,进入初始化阶段后,m的值由< clinit>()方法执行决定,这个A的类构造器< clinit>()方法由类变量的赋值和静态代码块中的语句按顺序合并产生。类似于:
<clinit>(){
// 1.
m = 10;
m = 20;
// 2.
m = 20;
m = 10;
}
类加载器
类加载器作用是把类(class)加载进内存的。
四类类加载器:
- 引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库,该类加载器无法直接获取。
- 扩展类加载器:负责jre/lib/ext目录下的jar包,或-D java.ext.dirs指定目录下的jar包装入工作库。
- 系统类加载器:负责java -classpath或-D java.class.path所指的目录下的类与jar包装入工作库,是最常用的类加载器。
- 自定义类加载器。
获取类加载器
@Test
public void test1(){
// 对于自定义类,使用系统类加载器加载,调用getClassLoader()获取系统类加载器
ClassLoader loader1 = ClassLoadTest2.class.getClassLoader();
System.out.println(loader1);
// 调用系统类加载器的getParent()来获取扩展类加载器
ClassLoader loader2 = loader1.getParent();
System.out.println(loader2);
// 无法调用扩展类加载器的getParent()来获取引导类加载器
ClassLoader loader3 = loader2.getParent();
System.out.println(loader3); // null
// 引导类加载器主要负责加载Java核心类库,无法加载自定义类。
ClassLoader loader4 = String.class.getClassLoader();
System.out.println(loader4); // null
}
使用类加载器读取properties配置文件
注意点:使用类加载器读取.properties文件,文件默认在当前module的src下。
public class ClassLoadTest3 {
@Test
public void test1() throws Exception{
Properties pros = new Properties();
// 流的方式读取.properties文件,junit单元测试下,默认相对路径为当前module下
// FileInputStream fis = new FileInputStream("src\\user1.properties");
// pros.load(fis);
// 通过类加载器读取.properties文件
// 使用类加载器读取.properties文件,文件默认在当前module的src下
ClassLoader classLoader = ClassLoadTest3.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("user1.properties");
pros.load(is);
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name: " + name + ",password: " + password);
}
}
如果用类加载器去读取.properties配置文件,且文件不在当前module下的src下,会报空指针异常。所以要注意main()下和Junit单元测试方法下的相对路径默认是哪里,还要注意文件是否在src文件目录下。