反射机制和java.lang.Class< T >这个类息息相关,因为整个反射机制就是基于对Class对象的操作。了解反射机制前,我们需要先了解Class这个类。
Class这个类名,很特殊的一个名字吧,那么它的意义一定非同一般。一句话,Class这个类就表示正在运行的Java应用程序中的类和接口。如果把一个类比作一个人的话,那么Class对象就相当于这个人的资料卡片,关于这个人的一切都在这个小小的资料卡片中存储着。
介绍Class类,我们需要稍微了解一下java中关于类的加载机制。
当我们在程序中使用一个类时,java虚拟机会先判断这个类是否已经加载进内存,如果没有,就会按照下面的步骤进行类的初始化:
所谓加载,就是把*.class文件加载进内存,并为之创建一个Class对象,这个Class对象保存了关于这个类的所有信息,所有的类初次使用都会创建一个Class对象。Class对象在内存中只有一个,如果一个类多次使用,不会再重复创建Class对象,虚拟机会自己判断。
所谓链接,分为大概三个过程,
所谓初始化,就是访问该类的父类中的成员和方法,完成对子类的初始化。
那么,一个类什么时候才会被加载呢?也就是类的加载时机,大致有这么几个场景:
还需要提到的一个概念就是类的加载器,它负责将.Class文件加载到内存中,并为之生成相应的Class对象。
Class 这个类并没有提供公共的构造方法,它是由虚拟机通过调用类加载器中defineClass()方法来创建的。
类加载器分为以下几种类型:
作用:
一个很关键的概念,叫做RTTI(Run-Time Type Identification)运行时类型识别。
JVM虚拟机要运行我们的程序,就必须知道我们的每一个对象所属的类和这个类的所有信息,这就是运行时类型识别。
RTTI分为两种:
我们回想一下,一般来说,我们创建一个对象,都是基于 类名 对象名 = new 类名 (形参列表)
这种形式利用公共的构造函数来创建一个对象。这就是所谓的编译器已经确定类型。而我们的反射,是在运行时才确定类型的,也就是说,java反射机制使得在运行状态中,对于任意一个类,虚拟机都能够知道这个类的所有属性和方法,对于任意一个对象,虚拟机都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制。
要想使用反射,我们就必须获取到这个类的Class对象,因为Class对象保存了所有关于这个类的信息。
// 调用Object类中的getClass方法来获取Class对象
Date date = new Date();
// 使用这种方法获取Class对象必须要有一个实例对象
Class aClass = date.getClass();
// Class中的getName方法用来返回类名
System.out.println(aClass.getName());
// java.util.Date
这个程序通过Date类的实例化对象,调用了从Object类继承来的getClass方法,这样就取得了java.util.Date类型的反射操作类对象,通过getName()方法可以直接输出其完整名称。
// 通过类的静态属性class来获取Class对象
Class<Date> aClass1 = Date.class;
这是一种比较特殊的格式,而且不只是java的核心类,所有的类(包括自己写的)都有这个静态属性,所以不必担心自己创建的类没法通过这种方法来获取Class对象。
static Class> forName(String className) throws ClassNotFoundException
Class<?> aClass2 = Class.forName("java.util.Date");
本程序使用Class类的静态方法forName来获取Class对象,如果通过className找到了这个类,就不会抛出‘ClassNotFoundException’的异常。
以上提到的三种常见的Class类的实例化方法几乎在所有的开发框架中都会用到,也很难分辨出那种方法使用的更多,三种方法都有各自适用的场景。
我们已经学会了怎用获取一个类的Class对象,有了这个Class对象,我们就可以利用它来进行类的反射控制了。
Class类有几个常用的方法需要了解一下:
方法 | 解释 |
---|---|
static Class> forName(String className) throws ClassNotFoundException |
获取某个类的Class对象 |
public Class>[] getInterfaces() |
获取类实现的所有接口 |
public String getName() |
获取反射操作类的全名 |
public String getSimplename() |
获取反射操作类的类名 |
public Package getPackage() |
获取反射操作类所在的包 |
public Class super T>() |
获取反射操作类的父类 |
public boolean isEnum() |
判断反射操作类是否是枚举类 |
public boolean isInterface() |
判断反射操作类是否是接口 |
public boolean isArray() |
判断反射操作类是否是数组 |
public T newInstance() throws InstantiationException,IllegalAccessException |
获取反射操作类的实例化对象 |
通过这些方法我们可以发现,在反射操作中类或者接口都是利用Class来包装的,利用Class类可以表示任意类、枚举、接口、数组等引用类型的操作。
这一小节的关键方法在newInstance方法,我们就是用它来创建我们的反射操作类的实例化对象的。
值得注意的是,使用newInstance()方法来实例化对象,那么这个类型中一定要提供无参构造方法。
public static void main(String[] args) throws ClassNotFoundException,
IllegalAccessException, InstantiationException {
// 获取Class对象
Class<?> aClass2 = Class.forName("java.util.Date");
// 获取Date类的实例化对象
Date newInstance = (Date) aClass2.newInstance();
System.out.println(newInstance);
// Mon May 27 21:26:55 CST 2019
}
可以发现,这个程序中并没有使用new关键字来创建Date类的对象,而是通过由Date类创建的Class对象的newInstance()方法来创建Date对象的,newInstance方法默认访问的是Date类中的无参构造方法,返回的类型是Object类型,根据需要向下转型。
可以利用一下方法来判断是否可以转型为某个类型:
//判断这个对象能不能被转化为这个类
class.inInstance(obj)
比如:
Class<?> aClass2 = Class.forName("java.util.Date");
// Date 能不能转换成 String ?
boolean b = aClass2.isInstance(new String());
System.out.println(b); //false
好吧,现在我们已经掌握了通过反射机制创建一个类对象,可是,问题来了?你可能想说,这么做有什么用啊?我简简单单的用new实例化一个类不好吗?你整这么多行代码不都是创建对象吗?
首先,必须承认,通过new进行对象的实例化是最正统的做法,也是使用最多的方法。但是通过new实例化对象时需要明确的指定类的构造方法,所以new也是造成代码耦合的最大元凶!要想解决代码的耦合问题,就要先解决关键字 new 来创建对象的操作。比如,来看一个简单的工厂模式:
package com.xx.test1;
interface Animal{
public void eat();
}
class Dog implements Animal{
public Dog() {
System.out.println("Dog构造方法");
}
@Override
public void eat() {
System.out.println("我爱吃骨头");
}
}
class Cat implements Animal{
public Cat() {
System.out.println("Cat构造方法");
}
@Override
public void eat() {
System.out.println("我爱吃鱼");
}
}
class Factory{
public static Animal getInstance(String animalName){
if(animalName.equals("Cat")) return new Cat();
else if(animalName.equals("Dog")) return new Dog();
else return null;
}
}
class Factory{
public static Animal getInstance(String animalName){
if(animalName.equals("Cat")) return new Cat();
else if(animalName.equals("Dog")) return new Dog();
else return null;
}
}
public class newIssue {
public static void main(String[] args) {
Animal dog = Factory.getInstance("Dog");
Animal cat = Factory.getInstance("Cat");
dog.eat();
cat.eat();
}
}
// 运行结果:
Dog构造方法
Cat构造方法
我爱吃骨头
我爱吃鱼
这个程序的最大问题在于,我每新增加一个动物,就需要在Factory中改动代码,因为new这个关键字需要访问到子类的构造方法,我们需要在Factory中去调用。这就是耦合,很麻烦不是吗?我们可以利用反射机制对工厂类进行改造:
class Factory {
public static Animal getInstance(String animalName) {
//if(animalName.equals("Cat")) return new Cat();
//else if(animalName.equals("Dog")) return new Dog();
//else return null;
Animal animal = null;
try {
// 使用反射来创建对象返回
Class aClass = Class.forName(animalName);
animal = (Animal) aClass.newInstance();
} catch (Exception e) {
System.out.println("该类型不存在");
} finally {
return animal;
}
}
}
你看,这么一来,不管有多少动物,我们都无需对工厂类再做改动。
在实际的开发中,如果将以上的工厂模式再结合上一些配置文件,常用的是XML文件,就可以利用配置文件来动态定义项目中的所需的操作类,此时程序将会变得十分灵活。
值得一提的是,使用new关键字仍是创建对象最正统的方法,在应用开发中(非架构层次的开发)大部分都使用它而不是反射。
上面提到了,使用newInstance方法来实例化对象有一个要求就是类中必须提供无参构造方法,但是,实际中由于开发者个人原因,可能只会提供有参构造方法,此时就会报错。解决这个问题的方法就是使用反射调用构造方法。Class类为我们提供了这样的方法:
方法 | 解释 |
---|---|
public Constructor>[] getConstructors() throws SecurityException |
获取反射操作类中的所有构造方法 |
public Constructor |
获取指定参数类型的构造方法 |
这两个方法会返回类中的构造方法,是Constructor类型,Constructor有些常用方法需要了解一下:
方法 | 解释 |
---|---|
public Class>[] getExceptionTypes() |
获取构造方法上抛出的所有异常的类型 |
public int getModifiers() |
获取构造方法上的修饰符 |
public String getName() |
获取构造方法的名字 |
public String getParameterCount |
获取构造方法的形参个数 |
public Class>[] getParameterTypes() |
获取构造方法中的参数类型 |
public T newInstance(Object... initargs) |
调用指定参数的构造实例化类对象 |
一个有趣的方法是public int getModifiers()
,获取构造方法上的修饰符,发现了吗,它返回的是int类型。实际上,所有的修饰符都是一个数字,修饰符的组成就是数字的加法操作,比如public用1表示,static用8表示,那么public static 就是1+8=9。在java.lang.reflect.Modifer类中明确定义了各个修饰符对应的常量操作,同时也提供了将数字转成修饰符的方法public static String toString(int mod)
。
可以看到,Constructor类中也提供了newInstance
方法来实例化对象,我们取得了有参构造的Constructor对象,就用它来执行以实例化对象:
package com.xx.test1;
import java.lang.reflect.*;
class Student{
String name;
Integer age;
public Student(String name, Integer age) {
this.name = name;
this.age = age;
System.out.println("有参构造方法执行了");
}
public Student() {
System.out.println("无参构造方法执行了");
}
}
public class GetCons {
public static void main(String[] args) throws Exception {
Class aClass = Class.forName("com.xx.test1.Student");
// 调用有参构造
Constructor<Student> constructor = aClass.getConstructor(String.class, Integer.class);
// 使用有参构造的newInstance方法来实例化对象
Student student = constructor.newInstance("张三", 19);
}
}
这样,就可以明确指定用有参构造方法来实例化对象了。
另外,如果构造方法是私有的,就需要用另外一种方法来获取public Constructor>[] getDeclaredConstructors()
获取所有的构造方法,包括私有的。或者public Constructor
获取单个的构造方法包含私有的。
方法是类的主要操作手段,以往我们调用一个方法是通过对象.方法名
的方式,通过反射也可以实现类方法的操作。一样,它也是通过操作Class对象来完成的。
具体方法:
方法 | 解释 |
---|---|
public Method[] getMethods() |
获取所有的成员方法,不包括私有 |
public Method[] getDeclaredMethods() |
获取所有的成员方法,包括私有 |
public Method getMethod(String name,Class>... parameterTypes) |
通过指定参数类型的Class对象获取单个成员方法,不包括私有 |
public Method getDeclaredMethod(String name,Class>... parameterTypes) |
通过指定参数类型的Class对象获取单个成员方法,包括私有 |
其实有一个规律,如果你想用私有方法,就必须要使用getDeclaredXxxxx()这个方法,前面获取私有的构造方法也是一样的。
上述的方法会返回java.lang.reflect.Method类型的对象或者数组对象,有必要了解Method类型的常用方法:
方法 | 解释 |
---|---|
public Class>[] getExceptionTypes() |
获取方法抛出的异常类型 |
public int getModifiers() |
获取方法的修饰符 |
public Class> getReturnType() |
获取方法的返回值类型 |
public int getParameterCount() |
获取方法中定义的参数数量 |
public Class>[] getParameterTypes() |
获取方法中定义的所有参数类型 |
public Object invoke(Object obj,Object... args) |
反射调用方法并且传递参数 |
这些方法中最重要的就是这个invoke方法了,它是实现反射调用方法的核心操作。
必须之处,任何情况下调用方法都必须产生类的实例化对象,反射也不例外,invoke方法接收的第一个参数就是一个实例化对象,不过,它不需要指定具体的对象类型,我们通过Class对象的newInstance方法获取到的实例化对象是一个obj类型,不需要向下转型,可以直接当做参数传递。
还有一个需要注意的是,获取到私有方法的method对象之后,要想通过invoke方法来调用,必须设置或者取消访问检查(使用继承父类(AccessibleObject类)来的setAccessible()方法),以达到执行私有方法的目的。
举个例子:
package com.xx.test1;
import java.lang.reflect.*;
class Student {
public Student() {
System.out.println("无参构造方法执行了");
}
public void doA(String s) {
System.out.println("公有方法执行了" + s);
}
private void doB(Integer i) {
System.out.println("私有方法执行了" + i);
}
}
public class GetCons {
public static void main(String[] args) throws Exception {
Class aClass = Class.forName("com.xx.test1.Student");
// 获取公有方法,第一个参数name是方法名
// 第二个parameterTypes参数是以声明顺序标识方法的形式参数类型的Class对象的数组,可以是null
Method doA = aClass.getMethod("doA", String.class);
// 通过Student类的Class对象的newInstance方法获取实例化对象,不需要转型
Object obj = aClass.newInstance();
// 执行方法,第一个参数是对象,第二个参数是参数
doA.invoke(obj, "hahaha");
// 反射调用私有方法
Method doB = aClass.getDeclaredMethod("doB", Integer.class);
// 取消访问检查
doB.setAccessible(true);
// 执行方法
doB.invoke(obj, 10000);
}
}
// 运行结果:
// 无参构造方法执行了
// 公有方法执行了hahaha
// 私有方法执行了10000
在这个案例中,我们的Student类中有一个无参构造,有一个公有方法doA,和一个私有方法doB,我们利用反射都可以获取到并且调用,前提是获得这个类的实例化对象。
既然提到了反射访问私有方法,有人就不明白了,我们都清楚java的特性之一就是封装,现在使用反射机制,那么封装性会不会被破坏了?
答案当然是否定的,我们都知道,封装的意义在于对外部隐藏具体的细节,只暴露实现功能的具体方法,这样有利于数据的安全性。也就是说,仅仅通过公有的方法我们就可以实现这个类的所有功能,通过反射来获取私有类是没有任何意义的,不仅实现不了功能,可能还会报错。反射就相当于你看病时医生使用的X光片,医生
(开发者)可以通过它来查看问题,普通的病人看不懂也用不着来看。所以说,封装性并没有被破坏,反而提高了java语言的灵活性。
成员的获取依然要通过Class类的方法。成员有变量有常量之分,并且部分成员还有可能是从父类继承过来的。
方法 | 解释 |
---|---|
public Field[] getFields() |
获取所有的成员变量包含从父类继承过来的 |
public Field[] getDeclaredFields() |
获取所有的成员变量 包含私有的 也包含从父类继承过来的成员变量 |
public Field getField(String name) |
获取单个成员变量包含从父类继承过来的 |
public Field getDeclaredField(String name) |
获取单个成员变量 包含私有的 也包含从父类继承过来的成员变量 |
这四种方法返回的类型都是java.lang.reflect.Field类型,这个类用来描述类中的成员信息。
Field类常用的方法有:
方法 | 解释 |
---|---|
public Class> getType() |
获取该成员的类型 |
public Object get(Object obj) |
获取指定对象中的成员的内容,相当于直接访问成员 |
public void set(Object obj,Object value) |
设置指定对象中的成员内容,相当于直接利用对象调用成员设置内容 |
实际在Field中还定义了许多setXxx(),getXxx()方法,可以直接设置这些方法或方法具体的类型。
再次重申,不管使用反射调用普通方法还是成员,都必须存在实例化对象(通常依靠反射获取),因为类中的属性必须在类产生实例化对象(有堆内存空间)后才可以使用。
举个例子:
package com.xx.test1;
import java.lang.reflect.*;
class Student {
private String name;
public Student() {
}
}
public class GetCons {
public static void main(String[] args) throws Exception {
Class aClass = Class.forName("com.xx.test1.Student");
// 实例化Student类
Object obj = aClass.newInstance();
// 获取成员对象
Field nameField = aClass.getDeclaredField("name");
// 取消封装
nameField.setAccessible(true);
// 赋值
nameField.set(obj, "James");
// 相当于Student类对象.name
Object o = nameField.get(obj);
// 输出
System.out.println(o);
}
}
我们在访问私有方法和私有成员都用到了这句代码对象.setAccessible(true);
,这个方法是public void setAccessible(boolean flag) throws SecurityException
,如果设置为true则表示此内容可以被直接操作,它是AccessibleObject类中的方法。AccessibleObject类是Filed和Executable类的父类,而Executable类又是Constuctor类和Method类的父类,所以Filed,Constuctor,Method这三个类都可以使用这个方法来取消封装。
需求:通过配置文件的内容来动态创建类对象,调用类中的set方法为对象赋处置,并调用指定方法。
# 反射操作类
className=com.xx.apply.User
# 初值
name=James
age=19
# 方法名
methodName=showUser
package com.xx.apply;
public class User {
private String name;
private Integer age;
public User() {
}
public String showUser() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
package com.xx.apply;
import java.io.FileReader;
import java.lang.reflect.Method;
import java.util.Properties;
public class Test {
public static void main(String[] args) throws Exception {
// 首先要读取配置文件
Properties properties = new Properties();
properties.load(new FileReader("src\\com\\xx\\apply\\reflect.properties"));
// 获取指定参数
String className = properties.getProperty("className");
String userName = properties.getProperty("name");
String userAge = properties.getProperty("age");
String methodName = properties.getProperty("methodName");
// 获取User类的Class对象
Class<?> aClass = Class.forName(className);
User user = (User) aClass.newInstance();
// 为user对象通过set方法设值
user.setAge(Integer.valueOf(userAge));
user.setName(userName);
// 调用指定方法
Method method = aClass.getMethod(methodName);
Object invoke = method.invoke(user);
System.out.println(invoke);
// User{name='James', age=19}
}
}
大概过程就是这样,有缺陷的地方就是需要赋初值的set方法需要自己写,如果把它改为配置文件写,测试类获取,采用动态拼串的方法就非常完美了。这种应用其实就是IOC的雏形,即所谓的控制反转。这里思想一致,但效果太小儿科了,过两天在Spring分类里我会写一个比较完善的IOC例子。
如果我有一个ArrayList
的对象,我想在这个集合中添加一个字符串数据,如何实现呢?我们都知道泛型指定的类型会在编译器进行校验,既然我们不能在编译器进行,就只能在运行期完成。因为泛型到了运行期以后,泛型会自动擦除,那么要在运行期完成这个需求,那么我们就需要使用反射。
具体实现:
package com.xx.apply;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class Test2 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> list = new ArrayList<Integer>();
// 泛型在编译期会进行类型校验,我们只能插入Integer类型的数据
list.add(1);
// 而不能插入String类型的数据
//list.add("sss");
// 使用反射来插入,反射是在运行期操作的,此时泛型已经擦除
Class<? extends List> aClass = list.getClass();
Method add = aClass.getMethod("add", Object.class);
add.invoke(list, "测试");
// 输出
System.out.println(list);
// [1, 测试]
}
}
这个应用暂时还想不到有什么实际用处,有点鸡肋,姑且当个好玩的图个乐子吧。
全文完。