不学无数—初识反射

初识反射

Posted by 不学无数 on September 10, 2018

反射:运行时的类信息

运行时类型信息使得你可以在程序运行时发现和使用类型信息

1. Class对象

通过Class对象可以在运行时发现一个对象完整的类继承结构

类是程序的一部分,每一个类都会有一个Class对象。换句话说既每编写一个新的类,就会产生一个Class对象。而这些Class对象信息是保存在我们用javac 类名.java 进行编译时产生的.class文件中的。为了生成这个对象,运行这个程序的java虚拟机(JVM)会使用类加载器进行加载

1.1 什么是类加载器

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中

即类加载器是Java虚拟机将描述类的数据,例如类的各种方法,构造参数之类的信息从Class文件加载到内存中去,并且对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。

所有的类都是在对其进行第一次使用的时候,动态加载到JVM中的,即和编译时需要进行连接工作的语言不通,在java中,类型的加载、连接和初始化都是在程序运行期间完成的。

类加载器在进行加载的时候会首先检查这个类的Class对象是否已经进行了加载,如果没有加载,那么默认的类加载器就会根据类名进行查找.class文件。通过下面例子,我们可以清楚看出类是在第一次被使用的时候才会加载。

static静态代码块的加载是在类加载的时候进行的

class Tom{
    static {
        System.out.println("Loading Tom");
    }
}

class Jerry{
    static {
        System.out.println("Loading Jerry");
    }
}

class Mary{
    static {
        System.out.println("Loading Mary");
    }
}
public class Demo4 {
    public static void main(String[] args) {
        System.out.println("Inside Main");
        new Tom();
        System.out.println("After Loading Tom");
        new Mary();
        System.out.println("After Loading Mary");
        new Jerry();
    }
}

输出结果如下:

Inside Main
Loading Tom
After Loading Tom
Loading Mary
After Loading Mary
Loading Jerry

类加载器在加载类的过程中会分为三个阶段

  • 加载:加载.class文件的字节码文件
  • 连接:为类分配静态域,并为变量分配初始值
  • 初始化:会真正的执行类中定义的java程序代码,既初始化静态域中的方法和变量

同一个类加载器下,一个类型只会初始化一次。

1.2 创建Class对象

  • 根据对象的引用.getClass()方法获取
Tom tom = new Tom();
Class class = tom.getClass();
  • 根据类名.class获取
Class class = Tom.class;

  • 根据Class中的静态方法Class.forName,其中的参数必须为带包名的类路径
 Class c = Class.forName("Tom");

通常在反射中创建Class对象时使用的第三种方法,因为第一种已经有这个对象了,干嘛还需要反射,第二种的话会有局限性,需要导入所要创建对象的包路径。

2. 使用反射

我们在上面获取到Class对象,而我们拿到了Class对象以后就能对其进行操作

2.1 构造方法

Class类和javalang.reflect类库一起对反射的概念进行了支持,该类库包含了FieldMethod以及Constructor类(每个类都实现了Member接口).这些类型的对象是由JVM在运行时创建的,用以表示未知类里所对应的成员信息.这样就可以用Constructor创建新的对象。下面演示一下通过反射获取构造方法。

2.1.1 获取公有构造方法

public class A {
    private String a;
    private String b;
    public String c;

    public A(String a, String b) {
        this.a = a;
        this.b = b;
    }

    private A(String a){
        this.a=a;
    }
    public A(){}
   ——————————get.set方法省略
}
Class a=Class.forName("Practice.Day05.A");
Constructor[] constructors = a.getConstructors();
for (Constructor constructor:constructors){
    System.out.println(constructor);
}

此时我们发现打印出来的信息如下:

public Practice.Day05.A()
public Practice.Day05.A(java.lang.String,java.lang.String)

2.1.2 获取所有构造方法

我们发现没有私有的构造函数,因为getConstructors()方法获得是public的构造函数,而getDeclaredFields() 方法获得是包括public, protected, default,private的构造函数,此时如果我们将代码改成如下:

Class a=Class.forName("Practice.Day05.A");
Constructor[] constructors = a.getDeclaredConstructors();
for (Constructor constructor:constructors){
    System.out.println(constructor);

}

我们发现打印的参数就会多了一个private的构造函数

public Practice.Day05.A()
private Practice.Day05.A(java.lang.String)
public Practice.Day05.A(java.lang.String,java.lang.String)

2.1.3 获得具体类型的构造方法

我们在上面都是获得了一个构造方法的数组,如果想要获得具体的构造方法的话,那么可以通过传入构造方法的入参的类型,可以获得这个构造方法,具体例子如下:

Class a=Class.forName("Practice.Day05.A");
Constructor constructor = a.getConstructor(null);
Constructor stringConstructor = a.getConstructor(String.class,String.class);
Constructor stringPrivateConstructor=a.getDeclaredConstructor(String.class);
System.out.println(constructor);
System.out.println(stringConstructor);
System.out.println(stringPrivateConstructor);

打印的信息如下:

public Practice.Day05.A()
public Practice.Day05.A(java.lang.String,java.lang.String)
private Practice.Day05.A(java.lang.String)

####2.1.4 通过构造方法实例化类

我们获得了Constructor对象以后,可以通过其中的newInstance方法进行实例化对象。例子如下:

Class a=Class.forName("Practice.Day05.A");
Constructor constructor = a.getConstructor(null);
Constructor stringConstructor = a.getConstructor(String.class,String.class);
A nullA= (A) constructor.newInstance();
A stringA= (A) stringConstructor.newInstance("BuXueWuShu","BuXueWuShu");
nullA.setA("BuXueWuShu");
System.out.println("nullA:"+nullA.getA());
System.out.println("stringA:"+stringA.getA());

打印信息如下:

nullA:BuXueWuShu
stringA:BuXueWuShu

2.2 成员变量

还是上面的实体类的例子,其中有私有的两个变量是a和b,私有的变量是c。

2.2.1 获取成员变量

如果类中有属性的话,两个方法都是返回一个Field数组.其中getDeclaredFields()方法返回的是所有参数包括public, protected, default,private,而getFields()只返回public的参数。例如如下

Class a=Class.forName("Practice.Day05.A");
Field[] allDeclaredFields = a.getDeclaredFields();--获得所有成员变量
Field[] fields = a.getFields();--只获得public的成员变量
for (Field field:allDeclaredFields){
    System.out.println(field);
}
System.out.println("-----------------------");
for (Field field:fields){
    System.out.println(field);
}

打印的参数如下:

private java.lang.String Practice.Day05.A.a
private java.lang.String Practice.Day05.A.b
public java.lang.String Practice.Day05.A.c
-----------------------
public java.lang.String Practice.Day05.A.c

2.2.2 获得指定成员变量

获得指定的成员变量其实和上面的获取指定的构造方法是一样的。举例如下:

Class a=Class.forName("Practice.Day05.A");
Field c1 = a.getField("c");
Field a1 = a.getDeclaredField("a");
System.out.println(c1);
System.out.println("---------------------");
System.out.println(a1);

打印参数如下:

public java.lang.String Practice.Day05.A.c
---------------------
private java.lang.String Practice.Day05.A.a

2.2.3 为成员变量赋值

获得了Field的对象后,可以调用get()set()方法对某个对象中的属性进行取值和赋值的操作。例子如下

Class a=Class.forName("Practice.Day05.A");
Constructor nullClass=a.getDeclaredConstructor(null);
A nullA= (A) nullClass.newInstance();--获得A的实例化对象
Field a1 = a.getDeclaredField("a");
a1.setAccessible(true);--变量a是private的,所以需要解除私有限定
a1.set(nullA,"BuXueWuShu");--为nullA对象中的变量a进行赋值操作
System.out.println("nullA="+a1.get(nullA));--取出nullA对象中的变量a

打印信息如下:

nullA=BuXueWuShu

注意在对私有的成员变量进行赋值操作时,要解除私有限定,调用setAccessible()方法,赋值为true

2.3 成员方法

2.3.1 获取成员方法

通过获得的Class对象,调用它的getDeclaredMethods()getMethods()方法可以获得类中的方法的信息。例如有以下的一个类。


public class TestMethod {

    private String playName;

    public void show(String playName){
        System.out.println("I Love "+playName);
    }

    private String returnPlayName(){
        return playName;
    }
}

做如下的调用:

Class a=Class.forName("Practice.Day05.TestMethod");
Method[] allDeclaredMethods = a.getDeclaredMethods();
Method[] methods = a.getMethods();
for (Method method:allDeclaredMethods){
    System.out.println(method);
}
System.out.println("-----------------");
for (Method method:methods){
    System.out.println(method);
}

可以发现打印如下的信息:

public void Practice.Day05.TestMethod.show(java.lang.String)
private java.lang.String Practice.Day05.TestMethod.returnPlayName()
-----------------
public void Practice.Day05.TestMethod.show(java.lang.String)
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

getDeclaredMethods()方法会获得自身和实现的接口中所有的方法,但是不会获得继承来的方法,getMethods()方法会获得所有的无论是实现的接口还是继承来的public的方法

2.3.2 获得指定的方法

通过方法名和方法中的参数类型就可以获得指定的方法

Class a=Class.forName("Practice.Day05.TestMethod");
Method show = a.getDeclaredMethod("show", String.class);
System.out.println(show);

打印信息如下:

public void Practice.Day05.TestMethod.show(java.lang.String)

2.3.3 使用方法

可以通过调用Method对象的invoke()方法进行调用方法。例子如下:

Class a=Class.forName("Practice.Day05.TestMethod");
Constructor nullClass=a.getDeclaredConstructor(null);
TestMethod nullTestMethod= (TestMethod) nullClass.newInstance();
Method show = a.getDeclaredMethod("show",String.class);
show.invoke(nullTestMethod,"BasketBall");

打印参数如下:

I Love BasketBall

如果调用的是private的方法,那么在使用invoke()方法之前要先解除私有限定,即调用setAccessible()方法,赋值为true