共计 5290 个字符,预计需要花费 14 分钟才能阅读完成。
导读
在之后的几篇文章,我会讲解我自己的 hibernate、spring、beanutils 框架,但讲解这些框架之前,我需要讲解 RTTI 和反射。
工作将近一年了,我们公司项目所使用的框架是 SSH,或者,其他公司使用的是 SSM 框架。不管是什么样的框架,其都涉及到反射。那么,什么是反射?我们在生成对象时,事先并不知道生成哪种类型的对象,只有等到项目运行起来,框架根据我们的传参,才生成我们想要的对象。
比如,我们从前端调用后端的接口,查询出这个人的所有项目,我们只要传递这个人的 id 即可。当然,数据来源于数据库,那么,问题来了,数据是怎么从持久态转化成我们想要的顺时态的?这里面,就涉及到了反射。但是,一提到反射,我们势必就提到 RTTI,即运行时类型信息(runtime Type Infomation)。
RTTI
po 类
/**
* Created By zby on 16:53 2019/3/16
*/
@AllArgsConstructor
@NoArgsConstructor
public class Pet {
private String name;
private String food;
public void setName(String name) {
this.name = name;
}
public void setFood(String food) {
this.food = food;
}
public String getName() {
return name;
}
public String getFood() {
return food;
}
}
/**
* Created By zby on 17:03 2019/3/16
*/
public class Cat extends Pet{
@Override
public void setFood(String food) {
super.setFood(food);
}
}
/**
* Created By zby on 17:04 2019/3/16
*/
public class Garfield extends Cat{
@Override
public void setFood(String food) {
super.setFood(food);
}
}
/**
* Created By zby on 17:01 2019/3/16
*/
public class Dog extends Pet{
@Override
public void setFood(String food) {
super.setFood(food);
}
}
以上是用来说明的 persistent object 类,也就是,我们在进行 pojo 常用的 javabean 类。其有继承关系,如下图:
展示信息
如一下代码所示,方法 eatWhatToday 有两个参数,这两个参数一个是接口类,一个是父类,也就是说,我们并不知道打印出的是什么信息。只有根据接口的实现类来和父类的子类,来确认打印出的信息。这就是我们输的运行时类型信息,正因为有了 RTTI,java 才有了动态绑定的概念。
/**
* Created By zby on 17:05 2019/3/16
*/
public class FeedingPet {
/**
* Created By zby on 17:05 2019/3/16
* 某种动物今天吃的是什么
*
* @param baseEnum 枚举类型 这里表示的时间
* @param pet 宠物
*/
public static void eatWhatToday(BaseEnum baseEnum, Pet pet) {
System.out.println(pet.getName() + “ 今天 ” + baseEnum.getTitle() + “ 吃的 ” + pet.getFood());
}
}
测试类
@Test
public void testPet(){
Dog dog=new Dog();
dog.setName(“ 宠物狗京巴 ”);
dog.setFood(FoodTypeEnum.FOOD_TYPE_BONE.getTitle());
FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MORNING,dog);
Garfield garfield=new Garfield();
garfield.setName(“ 宠物猫加菲猫 ”);
garfield.setFood(FoodTypeEnum.FOOD_TYPE_CURRY.getTitle());
FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MIDNIGHT_SNACK,garfield);
}
打印出的信息为:
那么,这和反射有什么关系呢?
反射获取当前类信息
正如上文提到的运行时类型信息,那么,类型信息在运行时是如何表示的?此时,我们就想到了 Class 这个特殊对象。见名知其意,即类对象,其包含了类的所有信息,包括属性、方法、构造器。
我们都知道,类是程序的一部分,每个类都有一个 Class 对象。每当编写并且执行了一个新类,就会产生一个 Class 对象(更恰当地说,是被保存在一个同名的.class 文件中)。为了生成这个类的对象,运行当前程序的 jvm 将使用到类加载器。jvm 首先调用 bootstrap 类加载器,加载核心文件,jdk 的核心文件,比如 Object,System 等类文件。然后调用 plateform 加载器,加载一些与文件相关的类,比如压缩文件的类,图片的类等等。最后,才用 applicationClassLoader,加载用户自定义的类。
加载当前类信息
反射正式利用了 Class 来创建、修改对象,获取和修改属性的值等等。那么,反射是怎么创建当前类的呢?
第一种,可以使用当前上下文的类路径来创建对象,如我们记载 jdbc 类驱动的时候,如以下代码:
/**
* Created By zby on 18:07 2019/3/16
* 通过上下文的类路径来加载信息
*/
public static Class byClassPath(String classPath) {
if (StringUtils.isBlank(classPath)) {
throw new RuntimeException(“ 类路径不能为空 ”);
}
classPath = classPath.replace(” “, “”);
try {
return Class.forName(classPath);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
第二种,通过类字面常量,这种做法非常简单,而且更安全。因为,他在编译时就会受到检查,我们不需要将其置于 try catch 的代码快中,而且,它根除了对 forName 的方法调用,所以,更高效。这种是 spring、hibernate 等主流框架使用的。
框架 hibernate 的内部使用类字面常量去创建对象后,底层通过 jdbc 获取数据表的字段值,根据数据表的字段与当前类的属性进行一一匹配,将字段值填充到当前对象中。匹配不成功,就会报出相应的错误。
类字面常量获取对象信息,如代码所示。下文,也是通过类字面常量创建对象。
/**
* Created By zby on 18:16 2019/3/16
* 通过类字面常量加载当前类的信息
*/
public static void byClassConstant() {
System.out.println(Dog.class);
}
第三种,是通过对象来创建当前类,这种会在框架内部使用。
/**
* Created By zby on 18:17 2019/3/16
* 通过类对象加载当前类的信息
*/
public static Class byCurrentObject(Object object) {
return object.getClass();
}
反射创建当前类对象
我们创建当前对象,一般有两种方式,一种是通过 clazz.newInstance(); 这种一般是无参构造器,并且创建对对象后,可以获取其属性,通过属性赋值和方法赋值,如如代码所示:
第一种,通过 clazz.newInstance() 创建对象
/**
* Created By zby on 18:26 2019/3/16
* 普通的方式创建对象
*/
public static <T> T byCommonGeneric(Class clazz, String name, BaseEnum baseEnum) {
if (null == clazz) {
return null;
}
try {
T t = (T) clazz.newInstance();
// 通过属性赋值,getField 获取公有属性,获取私有属性
Field field = clazz.getDeclaredField(“name”);
// 跳过检查,否则,我们没办法操作私有属性
field.setAccessible(true);
field.set(t, name);
// 通过方法赋值
Method method1 = clazz.getDeclaredMethod(“setFood”, String.class);
method1.setAccessible(true);
method1.invoke(t, baseEnum.getTitle());
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
测试:
@Test
public void testCommonGeneric() {
Dog dog= GenericCurrentObject.byCommonGeneric(Dog.class,
“ 宠物狗哈士奇 ”,
FoodTypeEnum.FOOD_TYPE_BONE);
FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_NOON,dog);
}
叔叔出结果为:
你会发现一个神奇的地方,就是名字没有输出来,但我们写了名字呀,为什么没有输出来?因为,dog 是继承了父类 Pet,当我们在创建子类对象时,首先,会加载父类未加载的构造器、静态代码块、静态属性、静态方法等等。但是,Dog 在这里是以无参构造器加载的,当然,同时也通过无参构造器的实例化了父类。我们在给 dog 对象的 name 赋值时,、并没有给父类对象的 name 赋值,所以,dog 的 name 是没有值的。父类引用指向子类对象,就是这个意思。
如果我们把 Dog 类中的 @Override public void setFood(String food) {super.setFood(food); } 的 super.setFood(food); 方法去掉,属性 food 也是没有值的。如图所示:
通过构造器创建对象
/**
* Created By zby on 18:26 2019/3/16
* 普通的方式创建对象
*/
public static <T> T byConstruct(Class clazz, String name, BaseEnum baseEnum) {
if (null == clazz) {
return null;
}
// 参数类型,
Class paramType[] = {String.class, String.class};
try {
// 一般情况下,构造器不止一个,我们根据构器的参数类型,来使用构造器创建对象
Constructor constructor = clazz.getConstructor(paramType);
// 给构造器赋值,赋值个数和构造器的形参个数一样,否则,会报错
return (T) constructor.newInstance(name, baseEnum.getTitle());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
测试:
@Test
public void testConstruct() {
Dog dog= GenericCurrentObject.byConstruct(Dog.class,
“ 宠物狗哈士奇 ”,
FoodTypeEnum.FOOD_TYPE_BONE);
System.out.println(“ 输出宠物的名字:”+dog.getName()+”\n”);
System.out.println(“ 宠物吃的什么:”+dog.getFood()+”\n”);
FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MIDNIGHT_SNACK,dog);
}
测试结果:
这是通过构造器创建的对象。但是注意的是,形参类型和和参数值的位数一定要相等,否则,就会报出错误的。
总结
为什么写这篇文章,前面也说了,很多框架都用到了反射和 RTTI。但是,我们的平常的工作,一般以业务为主。往往都是使用别人封装好的框架,比如 spring、hibernate、mybatis、beanutils 等框架。所以,我们不大会关注反射,但是,你如果想要往更高的方向去攀登,还是要把基础给打捞。否则,基础不稳,爬得越高,摔得越重。
我会以后的篇章中,通过介绍我写的 spring、hibernate 框架,来讲解更好地讲解反射。