java反射机制详解


首先思考一个问题,java是动态语言还是静态语言?


何谓动态语言?简单来说就是在运行时可以改变自身结构的语言。怎样才算改变了自身机构呢?比如引进了新的函数、对象或者是代码,比如已有的某些功能函数被删除,某些结构被改变等等。举个简单的例子,我用Python语言定义了一个变量a,一开始我让a = 1,a是数值型,但是我让a指向一个字符串可以么?让a指向一个类可以么?可以,当然可以,即便是在代码运行期间,a的指向也可以任意的改动,a的类型完全由它指向的具体内容所决定,这就是典型的动态语言的特征。依此看来,Python是典型的动态语言,同样为动态语言的还有PHP、Javascript等。

那么,看完了关于动态语言的介绍,你觉得java是动态语言么?

显然不是,java中任何变量的声明都需要指明类型,就这一点上它已经与Python等动态语言相差很大。java其实是一种静态语言。最为典型的静态语言莫过于C、C++了。

但作为编程语言里的佼佼者,java真的可能甘愿为地地道道的静态语言么?当然不会,java同样也可以具有类似于动态语言的特性,这让java的代码更加的灵活,而实现这种特性的就是java的反射机制。


什么是反射机制?

java的反射机制可以使java程序在运行期间拿到一个对象的所有信息,包括它的所有字段和方法,看清了是所有,因此不管这个对象中是否含有public、protected还是private,java程序都可以获取它的所有信息并且调用它们。

程序中的对象类型一般在译器就会被确定,但我们正在运行的程序有时也可能需要动态的加载一些类,这些类因为之前没有用到,因此没有被加载到JVM中,这时,java反射机制可以在运行期动态的创建对象并调用其属性。

再引出反射之前,先介绍Class对象。

一、Class对象

除了int等基本数据类型外,java中的其它类型都是class(包括interface),因此几乎都需要动态加载。而只有当JVM第一次读取到一种class类型时,才会将其加载进内存中。

而每加载一种class,JVM就会为其创建一个Class类型的实例,并关联起来。这里不要把Class与class混淆了。Class类是JVM内部创建的,每当JVM加载一个新的class,JVM会自动调用Class类的构造方法实例化一个Class对象,Class对象才是与记载的class有关联的,它存储了该class的所有完整信息。

查看java的api文档,你会看到Class类的构造方法是private的,即如下:

public final class Class {
    private Class() {}
}

这说明Class对象只有JVM能够创建且是自动创建,不需要java程序员关心这个问题。

再回到Class对象上,刚才说了,Class对象包含了一个被加载进JVM的类的所有完整信息,这包括 name、super、package、interface、field、method 等等。因此你想啊,如果我们获得了该Class对象,那岂不是就等于获得了该class的所有内容?这就是反射(Reflection)。

为了理清思绪,接下来以创建一个学生类对象为例,分步说明Class对象的加载过程。

Student stu = new Student();
  1. 当代码运行到该条语句时,JVM会加载相对应的Student.class字节码文件。因为Student类之前并未被程序所用,所以直到需要用到该对象时才动态创建;
  2. JVM沿着本地路径在本地磁盘中查找Student.class文件,并加载进JVM内存中;
  3. Student.class被加载进内存后,除了给它分配空间外,JVM自动调用Class类的构造方法创建一个Class对象,该Class对象将保存Student类的相关信息。注意对于同一个类只会产生一个Class对象。

接下来的重点似乎就在于如何获得Class对象了。


获得Class对象

接下来介绍三种获取Class对象的方法:

#1 直接通过一个class的静态变量class获取。

Class cls = String.class;

#2 通过实例变量提供的getClass()方法获取。

Student stu = new Student()
Class cls = stu.getClass();

#3 如果知道一个class的完整类名(即带包名的类路径),可以通过静态方法Class.forName()获取。

Class cls = Class.forName("java.lang.String");

另外注意:Class对象在JVM中是唯一的,它和每个第一次加载进JVM中的class一一对应,这就提醒我们在做Class对象间的比较时应该用 == 而不是 instanceof,因为intanceof不仅匹配当前类型还匹配当前类型的父类。而父类和当前类的Class对象是不同的。


使用Class获得类的基本信息

我们使用反射的主要目的是获取某个实例的信息,接下来介绍如何通过Class对象获得信息。

Class代表类的实体,在运行的java程序中代表类和接口,与之相关的方法有:

getName()  //获得类的完整路径。
getSimpleName() //获得类的名字
getPackage() //获得类的包
getSuperclass() //获得当前类的父类
getInterfaces() //获得当前类实现的类或接口(不包括父类)
newInstance() //创建类的实例
forName() //根据类名返回类的对象
isInstance(obj) //判断是否能强制转换为obj对象
isInterface() //判断是否为接口类型
isEnum() //判断是否为枚举类型
isArray() //判断是否为数组类型
isPrimitive() //判断是否为基本类型
isAnnotation() //判断是否是注解类型
isAnonymousClass() //判断是否是匿名类型

示例:

public class Main {
    public static void main(String[] args) {
        printClassInfo(String.class);
        printClassInfo(Integer[].class);
        printClassInfo(int.class);
    }
    
    static void printClassInfo(Class cls){
        System.out.println("Class name: " + cls.getName());
        System.out.println("Simple name: " + cls.getSimpleName());
        System.out.println("Super name: " + cls.getSuperclass());
        Class[] cs = cls.getInterfaces();
        for(Class csinterface : cs){
        	System.out.println(csinterface);
        }
        System.out.println("is interface: " + cls.isInterface());
        System.out.println("is enum: " + cls.isEnum());
        System.out.println("is array: " + cls.isArray());
        System.out.println("is primitive: " + cls.isPrimitive());
        System.out.println();
    }
}

运行结果:

Class name: java.lang.String
Simple name: String
Super name: class java.lang.Object
interface java.io.Serializable
interface java.lang.Comparable
interface java.lang.CharSequence
interface java.lang.constant.Constable
interface java.lang.constant.ConstantDesc
is interface: false
is enum: false
is array: false
is primitive: false

Class name: [Ljava.lang.Integer;
Simple name: Integer[]
Super name: class java.lang.Object
interface java.lang.Cloneable
interface java.io.Serializable
is interface: false
is enum: false
is array: true
is primitive: false

Class name: int
Simple name: int
Super name: null
is interface: false
is enum: false
is array: false
is primitive: true

注意到数组也是一种class,而且不同于Integer.class,它的类名是[Ljava.lang.Integer。另外,JVM为每一种基本类型如int也创建了Class,通过int.class访问。

在第三个例子中,未输出接口,这是因为如果某个类没有实现任何一个接口,那么getInterfaces()方法返回一个空数组。

另外,Class对象的newInstance()方法可用于创建实例,与new的功能类似,如下:

Class cls = String.class;
String s = (String)cls.newInstance(); //相当于new String()

这种方法虽然可以创建实例,但只限于调用publc的无参构造方法,除此之外的非public的构造方法或者带参的构造方法均不能直接通过该方法创建实例。


二、使用Field类与Class类访问字段

与之相关的Class对象的方法有:

getField() //获得某个公有的属性对象 (包括父类)
getFields() //获得所有公有的属性对象 (包括父类)
getDeclaredField() //获得指定的某个属性对象 (不包括父类)
getDeclaredFields() //获得所有属性对象 (不包括父类)

示例:

import java.lang.reflect.Field; //Field类需要导入
public class Main {

	public static void main(String[] args) throws Exception{
    	Class cls = Student.class;
    	System.out.println(cls.getField("name")); //获得公有name字段
    	System.out.println(cls.getDeclaredField("score")); //获得私有score字段
    	
    	Field[] fields = cls.getFields();
            for (Field field : fields) {
            	System.out.println(field);
            }
        
            Field[] declearfields = cls.getDeclaredFields();
            for (Field declearfield : declearfields) {
            	System.out.println(declearfield);
            }
    }
}

class Person{
	public String name;
	public int age;
}

class Student extends Person{
	private int score;
	private int weight;
}

运行结果:

public java.lang.String pack.Person.name
private int pack.Student.score
public java.lang.String pack.Person.name
public int pack.Person.age
private int pack.Student.score
private int pack.Student.weight

getField()、getDeclaredField()返回一个Field对象,而getFields()、getDeclaredFields()则返回一个Field数组。

注意到一个Field对象由以下三部分组成:

  • Modifier :字段修饰符。
  • Type :字段类型。
  • Name :字段名称。

与之相应的Field对象就有以下三种方法:

  • getName() :返回字段名称。
  • getType() :返回字段类型。
  • getModifiers() :返回字段修饰符。注意该方法返回的是一个int类型的值,有关它的用法可以参看下面的示例。

使用示例如下:

//注意要导入Field类和Modifier类
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
......
Field f = Student.class.getDeclaredField("score");
System.out.println(f.getName());
System.out.println(f.getType());
int t = f.getModifiers();
System.out.println(Modifier.isFinal(t));
System.out.println(Modifier.isPublic(t));
System.out.println(Modifier.isProtected(t));
System.out.println(Modifier.isPrivate(t));
System.out.println(Modifier.isStatic(t));

运行结果:

score
int
false
false
false
true
false

Field类的get()与set()方法

java为Field对象提供有get()与set()方法,用于获取字段的值并动态修改字段的值。


获取字段的值:

import java.lang.reflect.Field;
public class Main {
    public static void main(String[] args) throws Exception {
        Student stu = new Student(59);
        Class cls = stu.getClass();
        Field f = cls.getDeclaredField("score");
        Object value = f.get(stu);
        System.out.println(value); //59
    }
}
class Student {
    private int score;
    Student(int score)
    {
        this.score = score;
    }
}

在Eclipse环境下,你会发现上面这段代码在编译前未提示错误,但编译期却弹出了java.lang.IllegalAccessException,这是因为score字段是private的,尽管我们可以用getDeclaredField()方法得到该字段,但正常情况下,Main类是无法访问其他类的private字段的值的。要解决这个问题,除了将private权限更改为public之外,也可以在调用f.get()方法之前设置Field对象的setAccessible()方法,如下:

f.setAccessible(true);

该方法由Field对象提供,参数设置为true,意思是不管该字段的权限等级如何,一律允许访问。

既然就算是private字段的值也可以访问,那么类的封装是否还有意义呢?
大多数情况下我们都是通过类名.字段的形式来访问字段值,这种访问方式确实能够达到数据封装的目的。而反射是一种非常规的用法,它更多的是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。

此外,setAccessible(true)方法也可能因为SecurityManager的运行期检查而导致失败。

但不可否认的是,通过反射读写字段确实在一定程度上破坏了类的封装。


修改字段的值:

import java.lang.reflect.Field;
public class Main {
    public static void main(String[] args) throws Exception {
        Student stu = new Student(61);
        Class cls = stu.getClass();
        Field f = cls.getDeclaredField("score");
        f.setAccessible(true);
        f.set(stu,59);
        Object value = f.get(stu);
        System.out.println(value); //59
    }
}
class Student {
    private int score;
    Student(int score)
    {
        this.score = score;
    }
}

三、通过Class与Method类调用方法

Class类提供的用于获取Method的方法有:

getMethod(name,Class...) //获取某个public的Method (包括父类)
getDeclaredMethod(name,Class...) //获取当前类的某个Method (不包括父类)
getMethods() //获取所有public的Method (包括父类)
getDeclaredMethods() //获取所有Method (不包括父类)

示例:

import java.lang.reflect.Method;
public class Main {
    public static void main(String[] args) throws Exception {
        Class cls = B.class;
        //注意如果方法有参数,则需要在getMethod()和getDeclaredMethod()方法里传入所有参数的Class实例
        System.out.println(cls.getMethod("f0",int.class)); //因为f0方法有参数,额外传入int.class实例
        System.out.println(cls.getMethod("f1",String.class)); //额外传入String.class实例
        System.out.println(cls.getDeclaredMethod("f2")); //f2方法无参数,故只传入方法名即可
    }
}

class A {
    public int f0(int x) {
        return x;
    }
}
class B extends A {
    public String f1(String s) {
        return s;
    }
    private int f2() {
        return 0;
    }
}

运行结果:

public int pack.A.f0(int)
public java.lang.String pack.B.f1(java.lang.String)
private int pack.B.f2()

可以看到,getMethod()等方法与getField()类似,getMethod()返回一个Method对象,该对象包含以下信息:

  • Name : 方法名。
  • Modifier :方法修饰符。
  • ReturnType :方法返回值类型。
  • ParameterType :方法参数类型。

于是一个Method对象对应有以下方法:

  • getName() :返回方法名。
  • getModifiers() :返回方法修饰符,返回值与Field对象的该方法类似,为一个int型的值。
  • getReturnType() :返回方法的返回值类型,为一个Class实例,如String.class。
  • getParameterTypes() :返回方法的参数类型,因为参数不止有一个,因此它的返回值是一个Class数组,如{String.class,int.class}。

示例:

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
...
Class cls = B.class;
Method m = cls.getMethod("f1",String.class);
System.out.println(m.getName());
System.out.println(m.getReturnType());
Class[] paramtertypes = m.getParameterTypes();
for(Class paramtertype : paramtertypes)
    System.out.println(paramtertype);

int t = m.getModifiers();
System.out.println(Modifier.isFinal(t));
System.out.println(Modifier.isPublic(t));
System.out.println(Modifier.isProtected(t));
System.out.println(Modifier.isPrivate(t));
System.out.println(Modifier.isStatic(t));

运行结果:

f1
class java.lang.String
class java.lang.String
false
true
false
false
false

调用方法

Method对象提供有invoke()方法,用于运行时动态调用类方法。

invoke()方法第一个参数为需要调用的方法所在的实例对象,因此如果调用一个静态方法时,由于无需指定实例对象,因此第一个参数传入为null。

除第一个参数外,之后依次传入调用的方法需要的参数。

示例如下:

import import java.lang.reflect.Method;
public class Main {
    public static void main(String[] args) throws Exception {
        TextClass tc = new TextClass();
        Class cls = tc.getClass();
        Method m0 = cls.getMethod("f",int.class); 
        System.out.println(m0.invoke(tc,6)); //调用public的f方法
        
        Method m1 = cls.getMethod("fnum",int.class,int.class);
        System.out.println(m1.invoke(tc,6,6)); //调用public的fnum方法
        
        Method m2 = cls.getDeclaredMethod("fs",String.class);
        m2.setAccessible(true); //调用非public方法需使用该语句
        System.out.println(m2.invoke(tc,"hahaha")); //调用private的fs方法
        
        Method m3 = cls.getMethod("fd",double.class);
        System.out.println(m3.invoke(null,6.666)); //调用static的fd方法
    }
}

class TextClass {
	public int f(int i) {
		return i;
	}
	public int fnum(int a,int b) {
		return a + b;
	}
	private String fs(String s) {
		return s;
	}
	public static double fd(double d) {
		return d;
	}
}

运行结果:

6
12
hahaha
6.666

invoke()方法的多态特性

invoke()方法也具有多态的性质,它同样建立在继承和方法重写的前提上,如下例子:

import java.lang.reflect.Method;
public class Main {
    public static void main(String[] args) throws Exception {
        Method m = Person.class.getMethod("draw");
        m.invoke(new Student()); //I am a Student.
    }
}

class Person {
    public void draw(){
        System.out.println("I am a Person.");
    }
}

class Student extends Person {
    public void draw(){
        System.out.println("I am a Student.");
    }
}

从结果可以看到,从Person的Class对象得到的Method对象,调用该Method对象的invoke()方法后,传入子类Student实例,调用的也是子类的draw()方法,而不是父类。


四、调用构造方法

在前面已经提到,通过Class对象newInstance()方法可以创建实例,但它只限于调用public的无参的构造方法来创建实例。为了调用任意的构造方法,java反射机制提供了Constructor对象。

通过Class对象提供的以下四种方法可以获得该对象:

getConstructor() :获取某个public的Constructor
getDeclaredConstructor() :获取某个Constructor
getConstructors() :获取全部public的Constructor
getDeclaredConstructors() :获取全部Constructor

利用Constructor对象,我们通过以下步骤就可以创建任意构造方法的对象。

获取Class对象,通过Class对象的getConstructor()方法获得Constructor对象
利用Constructor对象的newInstance()方法调用构造方法
同时,Constructor对象的newInstance()方法会返回类的实例

示例:

import java.lang.reflect.Constructor;
public class Text {
    public static void main(String[] args) throws Exception {
        Class cls = TextClass.class;
        Constructor cr = cls.getConstructor(String.class);
        TextClass tc = (TextClass)cr.newInstance("biubiubiu");
        System.out.println(tc.getS()); //biubiubiu
    }
}

class TextClass {
    private String s;
    public TextClass(String s){
        this.s = s;
    }
    public String getS(){
        return s;
    }
}

注意getConstructor()方法也需要传入Class参数,如int.class、String.class等,如果调用的是非public的构造方法,则也需要设置 Constructor.setAccessible(true)。


后续相关:

java中的动态代理
java运用反射机制的例子

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值