java反射和Class对象详解
目录
一、java反射
二、运行期发现和使用类的信息
1、RTTI机制——Run-Time Type Identification 运行时类型识别
2、反射机制
三、Class对象详述
1、关于Class对象
2、获取Class对象的三种方式
3、关于类的字面常量
四、类的加载过程简述
1、类的主动引用和被动引用
2、加载过程(三阶段)
(1)加载阶段
(2)连接阶段
(3)初始化阶段
3、关于静态变量的赋值
(1)静态语句块可以对后面的静态变量赋值,但不能对其进行访问
(2)父类静态变量总是能优先赋值
五、反射常用API示例
1、通过反射创建实例
2、通过反射操作方法
3、通过反射操作域
4、利用反射进行类的分析
(1)分析构造方法
(2)分析常规方法
(3)分析内部类
一、java反射
java语言允许通过程序化的方式间接对Class进行操作。Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息,如构造函数、属性和方法等。java允许用户借由这个与Class相关的元信息对象间接调用Class对象的功能,我们把这种动态获取对象信息和调用对象方法的功能称之为反射机制。
二、运行期发现和使用类的信息
java运行时识别对象和类的信息主要有2种方式:
1、RTTI机制——Run-Time Type Identification 运行时类型识别
编译器在编译时打开和检查.class文件,它假定我们在编译时已经知道了所有的类型信息。
作用:运行时,识别一个对象类型(类型在编译时已知)
代码示例:
public class DemoTest {
static abstract class Shape {
// this 调用当前类的toString()方法,返回实际的内容
void draw(){ System.out.println(this + "draw()"); }
// 声明 toString()为abstract类型,强制集成在重写该方法
abstract public String toString();
}
static class Circle extends Shape {
public String toString(){ return "Circle"; }
}
static class Square extends Shape {
public String toString(){ return "Square"; }
}
static class Triangle extends Shape {
public String toString(){ return "Triangle"; }
}
public static void main(String[] args){
// 把Shape对象放入List<Shape>的数组的时候会向上转型为Shape,从而丢失了具体的类型信息
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
// 从数组中取出时,这种容器,实际上所有的元素都当成Object持有,会自动将结果转型为Shape,这就是RTTI的基本的使用。
for(Shape shape : shapeList){
shape.draw();
}
}
}
执行结果:
存入数组的时候,会自动向上转型为Shape,丢失了具体的类型,当从数组中取出的时候(List容器将所有的事物都当做Object持有),会自动将结果转型回Shape,这就是RTTI的基本用法。Java中所有的类型转换都是在运行时进行正确性检查的,也就是RTTI:在运行时,识别一个对象的类型。
另外,使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择性的执行子类的方法。
2、反射机制
运行时打开和检查.class文件,它允许我们在运行时发现和使用类的信息,让语言具有动态编程的特性。
下面将详述反射实现的原理和基本API的使用。
三、Class对象详述
1、关于Class对象
java有两种对象:示例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的,它包含了与类有关的信息,实例对象就是通过Class对象来创建的。java使用Class对象执行其RTTI,多态是基于RTTI实现的。
Class类没有公共的构造方法,Class对象是在类加载的时候由java虚拟机以及通过调用类加载器中的defineClass方法自动构造的,因此不能显式地声明一个Class对象。
2、获取Class对象的三种方式
Class.forName("类的全限定名")
实例对象.getClass()
类名.class(类字面常量的方式)
3、关于类的字面常量
java提供了使用类字面常量来生成Class对象的引用。像这样:Cat.class,这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且根除了对forName()方法的调用,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组及基本数据类型。
基本数据类型还有对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象。一般情况下更倾向于使用.class形式,这样可以保持于普通类的形式统一。
用.class来创建对Class对象的引用时,不会自动地初始化该Class对象(这点和Class.forName()方法不同)。类对象的初始化阶段被延迟到了对静态方法或者非静态域的首次引用时才执行。
注:一旦类被加载到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class引用,JVM不会创建两个相同类型的Class对象。
四、类的加载过程简述
类的加载过程是指类加载器尝试加载class二进制文件,并在JVM中生成对应的数据结构,然后使其分布在JVM对应的内存区域。
1、类的主动引用和被动引用
JVM虚拟机规范规定,每个类或者接口被java程序首次主动使用时才会对其进行初始化,JVM同时规范了以下几种主动使用类的场景:
(1)通过new关键字会导致类的的初始化
public class Demo {
static{
System.out.println("DemoTest进行初始化...");
}
}
public class DemoTest {
public static void main(String[] args) {
Demo demo = new Demo();
}
}
测试结果:
(2)访问类的静态变量,包括读取和更新会导致类的初始化
public class Demo {
static{
System.out.println("DemoTest进行初始化...");
}
public static int x = 10;
}
public class DemoTest {
public static void main(String[] args) {
// 访问类的静态变量x,会导致Demo类的初始化
int i = Demo.x * 2;
}
}
测试结果:
(3)访问类的静态方法会导致类的初始化
public class Demo {
static{
System.out.println("DemoTest进行初始化...");
}
public static void method(){
System.out.println("doSomething...");
}
}
public class DemoTest {
public static void main(String[] args) {
// 访问类的静态方法,会导致Demo类的初始化
Demo.method();
}
}
测试结果:
(4)对某个类进行反射操作会导致类的初始化
public class Demo {
static{
System.out.println("DemoTest进行初始化...");
}
}
public class DemoTest {
public static void main(String[] args) {
// 访问类的反射操作,会导致Demo类的初始化
try {
Class.forName("demo.otherdemo.innerclass.Demo");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
测试结果:
(5)初始化子类会导致父类的初始化
public class Demo {
static{
System.out.println("Demo进行初始化...");
}
public static int x = 10;
}
public class DemoChild extends Demo {
static {
System.out.println("DemoChild初始化...");
}
public static int y = 20;
}
public class DemoTest {
public static void main(String[] args) {
// int x = DemoChild.x; 子类调用父类静态变量,子类不会进行初始化
int y = DemoChild.y;
}
}
测试结果:
注:通过子类使用父类的静态变量只会导致父类的初始化,子类不会初始化。
(6)启动类(main()方法)所在类会被初始化
public class Demo {
static{
System.out.println("Demo进行初始化...");
}
public static void main(String[] args) {
System.out.println("这是一个启动所在类");
}
}
测试结果:
除了上述的几种情况,其他情况都属于被动调用,不会导致类的初始化,比如:
(1)构造某个类的数组时不会导致类的初始化
public class Demo {
static{
System.out.println("Demo进行初始化...");
}
}
public class DemoTest {
public static void main(String[] args) {
// 构造某个类的数组不会导致类的初始化
Demo[] demos = new Demo[10];
// 使用集合也一样
List<Demo> demoList = new ArrayList<>(10);
System.out.println("数组:"+demos.length +" 集合:"+demoList);
}
}
测试结果:
(2)引用类的简单静态常量不会导致类的初始化
public class Demo {
static{
System.out.println("Demo进行初始化...");
}
public final static int x = 10;
}
public class DemoTest {
public static void main(String[] args) {
// 引用简单静态常量不会导致类的初始化
System.out.println(Demo.x*10);
}
}
测试结果:
注意:但是如果只是把一个域设置为static final还不足以确保这种行为,Math.random()虽然会产生常量,但是需要经过复杂的运算,因此仍旧会对类进行初始化。
public class Demo {
static{
System.out.println("Demo进行初始化...");
}
static final double y = Math.random();
}
public class DemoTest {
public static void main(String[] args) {
System.out.println(Demo.y*10);
}
}
测试结果:
2、加载过程(三阶段)
类的加载过程分为三个大的阶段,分别是加载阶段、连接阶段和初始化阶段。连接阶段又包括验证、准备和解析三个阶段。
(1)加载阶段
类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。
类加载阶段的最终产物是堆内存中的class对象,对于同一个Classloader对象,不管某个类被加载对少次,对应堆内存中的class对象始终只有一个。
类加载阶段发生在连接阶段之前,但连接阶段不必等加载阶段结束才开始,可以交叉工作。
下图是类加载阶段后的内存分布情况:
(2)连接阶段
类的连接阶段包括三个小的过程:分别是验证、准备和解析
验证
验证在连接阶段的主要目的是确保class文件的字节流所包含的内容符合JVM规范,并且不会出现危害JVM自身安全的代码。当验证不符合要求时,会抛出VerifyError这样的异常或其子异常。
主要验证的内容有:
——文件格式的验证:包括文件头部的魔术因子、class文件主次版本号、class文件的MD5指纹、变量类型是否支持等
——元数据的验证:对class的字节流进行语义分析,判断是否符合JVM规范。简单来说就是java语法的正确性
——字符码的验证:主要验证程序的控制流程、如循环、分支等
——符号引用验证:验证符号引用转化为直接引用时的合法性,保证解析动作的顺利执行。比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。
准备
准备阶段主要做的事情就是在方法区为静态变量发配内存以及赋值初始默认值。
注意:final修饰的静态常量在编译阶段就已经赋值,不会导致类的初始化,是一种被动引用,因此也不存在连接阶段。
解析
解析就是在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程。
解析过程主要针对类接口、字段、类方法和接口方法四类进行。
(3)初始化阶段
初始化阶段是类加载过程的最后一个阶段,该阶段主要做的事情就是执行clinit()方法,该方法会为所有的静态变量赋予正确的值。
3、关于静态变量的赋值
(1)静态语句块可以对后面的静态变量赋值,但不能对其进行访问
(2)父类静态变量总是能优先赋值
public class Demo {
static int x = 10; //①
static{ x = 20; } //②
}
public class DemoChild extends Demo {
static int i = x;
public static void main(String[] args) {
System.out.println(DemoChild.i); //输出20
}
}
注意:如果以上代码中①行和②行位置调换,则程序输出10。准备阶段在方法区分配x的内存,值为0,初始化阶段 clinit()方法顺序执行。
五、反射常用API示例
创建父类Human:
public class Human {
public String sex = "default man";
public Human(){}
public Human(String sex){
this.sex = sex;
}
private void priavteMethod(){
System.out.println("这是Human的private方法");
}
public void publicMethod(){
System.out.println("这是Human的public方法");
}
public class superInnerClass{
private String property = "super property";
private void getProperty(){
System.out.println("super内部类的财产是:"+property);
}
}
}
创建子类Person:
public class Person extends Human {
// 私有成员变量
private String username = "默认名字";
private int userAge = 18;
public String IdCardNum;
// 无参构造方法
public Person(){
System.out.println("Person的public无参构造器");
}
// 公有有参构造方法
public Person(String idCardNum) {
IdCardNum = idCardNum;
}
// 私有有参构造方法
private Person(String username, String idCardNum) {
this.username = username;
IdCardNum = idCardNum;
}
// 私有方法
private String privateString(String str){
System.out.println("priavte Hello:"+str);
return str;
}
// 公有方法
public String publicString(String str){
System.out.println("public Hello:"+str);
return str;
}
// 公有内部类
public class publicInnerClass{
private String property = "priavte property";
private void getProperty(){
System.out.println("public内部类的财产是:"+property);
}
}
// 私有内部类
private class privateInnerClass{
private void getUsername(){
System.out.println("private内部类私有姓名:"+username);
}
}
}
1、通过反射创建实例
反射创建实例有两种方法
方法一:通过字节码.newInstance()方法,条件是该类具备公有的无参构造器
方法二:通过字节码,获取指定构造器进行实例化(非常强大,应用普遍)
public class DemoTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
/**
* 获取字节码有三种方式:类.class/对象.getClass()/Class.forName()
*/
Class<?> cl = Class.forName("demo.otherdemo.reflect.Person");
/**
* 演示(1):操作私有构造器创建Person实例
* 1、获取指定构造器:public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
* 2、getConstructors()获取所有的公有构造器,不包括父类的
* 3、getDeclaredConstructors()获取所有本类中声明的构造器,包括私有的,不包括父类的
*/
Constructor<?> con = cl.getDeclaredConstructor(String.class, String.class);
// 因为反射的是私有构造器,需要禁用安全检查
con.setAccessible(true);
// 通过私有构造器创建Person实例,需要进行强壮
Person person01 = (Person) con.newInstance("888888","测试人名");
person01.publicString("person01——私有有参构造器创建Person实例");
/**
* 演示(2):通过字节码直接创建Person实例对象——newInstance()
* 条件:必须具备公有的无参构造器
*/
Person person02 = (Person) cl.newInstance();
person02.publicString("person02——公有无参构造器创建Person实例");
}
}
演示结果:
2、通过反射操作方法
反射操作方法步骤:
1、首先需要通过字节码获取指定的方法对象,如果方法为私有方法时,还需要禁用安全检查
2、在调用一个对象的方法时,需要构建该对象的实例
3、方法对象.invoke(对象实例,方法参数)
public class DemoTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
/**
* 获取字节码有三种方式:类.class/对象.getClass()/Class.forName()
*/
Class<?> cl = Class.forName("demo.otherdemo.reflect.Person");
/**
* 演示:通过反射调用私有方法
*/
// 操作指定的私有方法
Method method = cl.getDeclaredMethod("privateString", String.class);
// 禁用安全检查
method.setAccessible(true);
// 反射调用方法,需要指定对象和入参(如果有参数的话)
method.invoke(cl.newInstance(),"反射的私有方法调用");
}
}
演示结果:
3、通过反射操作域
反射操作域步骤:
1、通过字节码获取指定域对象,私有与对象需要禁用安全检查
2、创建对象的实例
3、通过get(实例)的方法获取域对象的值(结果需强转);通过set(对象,域新值)设置域的值
public class DemoTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
/**
* 获取字节码有三种方式:类.class/对象.getClass()/Class.forName()
*/
Class<?> cl = Class.forName("demo.otherdemo.reflect.Person");
/**
* 演示:通过反射操作私有域
*/
// 获取指定的操作域
Field userAge = cl.getDeclaredField("userAge");
// 禁用安全检查——这个成员变量是私有的
userAge.setAccessible(true);
/**
* 获取成员变量的值
* 1、得到的是obj,需要进行强转,如果类型错误会抛出异常
* 2、在获取时,需要指定实例(没有的化需要创建)
*/
Person person = (Person) cl.newInstance();
int age = (int) userAge.get(person);
System.out.println("获取值:" + age);
/**
* 设置成员变量的值
* 需要指定实例对象和设置的值
*/
userAge.set(person, 22);
System.out.println("获取设置后的值:" + (int) userAge.get(person));
}
}
演示结果:
4、利用反射进行类的分析
(1)分析构造方法
public class DemoTest {
public static void main(String[] args) throws ClassNotFoundException {
/**
* 获取字节码有三种方式:类.class/对象.getClass()/Class.forName()
*/
Class<?> cl = Class.forName("demo.otherdemo.reflect.Person");
/**
* 演示:分析构造方法
*/
// 获取该类中所有申明的构造方法,包括私有方法
Constructor<?>[] declaredConstructors = cl.getDeclaredConstructors();
if(Objects.nonNull(declaredConstructors)){
for(Constructor con : declaredConstructors){
printConstructor(con);
}
}
System.out.println("--------------------公有构造器--------------------------");
// 获取所有的公有构造方法,构造方法不会获取父类的
Constructor<?>[] constructors = cl.getConstructors();
if(Objects.nonNull(constructors)){
for(Constructor con : constructors){
printConstructor(con);
}
}
System.out.println("--------------------父类构造器--------------------------");
// 获取父类的构造器,需要先获取父类的字节码对象
Class<?> superclass = cl.getSuperclass();
Constructor<?>[] supConstructors = superclass.getConstructors();
if(Objects.nonNull(supConstructors)){
for(Constructor con : supConstructors){
printConstructor(con);
}
}
}
public static void printConstructor(Constructor con){
// 获取所有的参数
Parameter[] parameters = con.getParameters();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("(");
for(int i =0;i<parameters.length;i++){
if(i == parameters.length-1){
// 获取参数类型和参数名字
stringBuilder.append(parameters[i].getType()+" "+parameters[i].getName());
}else{
stringBuilder.append(parameters[i].getType()+" "+parameters[i].getName()+",");
}
}
stringBuilder.append(")");
// 获取修饰语
String modifier = Modifier.toString(con.getModifiers());
System.out.println(modifier+" "+con.getName()+stringBuilder.toString());
}
}
演示结果:
private demo.otherdemo.reflect.Person(class java.lang.String username,class java.lang.String idCardNum)
public demo.otherdemo.reflect.Person(class java.lang.String idCardNum)
public demo.otherdemo.reflect.Person()
--------------------公有构造器--------------------------
public demo.otherdemo.reflect.Person(class java.lang.String idCardNum)
public demo.otherdemo.reflect.Person()
--------------------父类构造器--------------------------
public demo.otherdemo.reflect.Human()
public demo.otherdemo.reflect.Human(class java.lang.String sex)
其他相关API:
// 获取抛出的异常类型
con.getExceptionTypes();
// 判断是否被声明为采用可变数量的参数,是则为true
con.isVarArgs();
// 获取参数的数量
con.getParameterCount();
(2)分析常规方法
public class DemoTest {
public static void main(String[] args) throws ClassNotFoundException {
/**
* 获取字节码有三种方式:类.class/对象.getClass()/Class.forName()
*/
Class<?> cl = Class.forName("demo.otherdemo.reflect.Person");
/**
* 演示:分析方法
*/
// 获取该类中所有申明的方法,包括私有方法
Method[] declaredMethods = cl.getDeclaredMethods();
if(Objects.nonNull(declaredMethods)){
for(Method method : declaredMethods){
printConstructor(method);
}
}
System.out.println("--------------------公有方法--------------------------");
// 获取所有的公有方法,包括获取父类的
Method[] methods = cl.getMethods();
if(Objects.nonNull(methods)){
for(Method method : methods){
printConstructor(method);
}
}
}
public static void printConstructor(Method method){
// 获取所有的参数
Parameter[] parameters = method.getParameters();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("(");
for(int i =0;i<parameters.length;i++){
if(i == parameters.length-1){
// 获取参数类型和参数名字
stringBuilder.append(parameters[i].getType()+" "+parameters[i].getName());
}else{
stringBuilder.append(parameters[i].getType()+" "+parameters[i].getName()+",");
}
}
stringBuilder.append(")");
// 获取修饰语
String modifier = Modifier.toString(method.getModifiers());
System.out.println(modifier+" "+method.getReturnType()+" "+method.getName()+stringBuilder.toString());
// 获取抛出的异常类型
method.getExceptionTypes();
// 判断是否被声明为采用可变数量的参数,是则为true
method.isVarArgs();
// 获取参数的数量
method.getParameterCount();
// 获取反参类型
method.getReturnType();
}
}
演示结果:
另,我们看到通过反射获取到了一个奇怪的方法:
static class java.lang.String access$000(class demo.otherdemo.reflect.Person arg0)
这涉及到JAVA编译器中对内部类的处理方式。
为了让内部类能够访问外部类的数据成员,JAVA编译器会为外部类自动生成 static Type access$iii(Outer)方法,供内部类调用。
编译器对这些自动生成的方法,在编译时进行检查,是不允许程序员直接来调用的,也就是不能直接调用这些后门。
(3)分析内部类
public class DemoTest {
public static void main(String[] args) throws ClassNotFoundException {
/**
* 获取字节码有三种方式:类.class/对象.getClass()/Class.forName()
*/
Class<?> cl = Class.forName("demo.otherdemo.reflect.Person");
// 获取公有的内部类字节码,包括父类的
Class<?>[] classes = cl.getClasses();
for(Class c :classes){
//获取所传类从java语言规范定义的格式输出getCanonicalName()
System.out.println("--------"+Modifier.toString(c.getModifiers())+" "+c.getSimpleName()+"--------");
Method[] methods = c.getDeclaredMethods();
if(Objects.nonNull(methods)){
for(Method m :methods){
printConstructor(m);
}
}
}
}
public static void printConstructor(Method method){
// 获取所有的参数
Parameter[] parameters = method.getParameters();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("(");
for(int i =0;i<parameters.length;i++){
if(i == parameters.length-1){
// 获取参数类型和参数名字
stringBuilder.append(parameters[i].getType()+" "+parameters[i].getName());
}else{
stringBuilder.append(parameters[i].getType()+" "+parameters[i].getName()+",");
}
}
stringBuilder.append(")");
// 获取修饰语
String modifier = Modifier.toString(method.getModifiers());
System.out.println(modifier+" "+method.getReturnType()+" "+method.getName()+stringBuilder.toString());
}
}
演示结果:
还没有评论,来说两句吧...