在之后的几篇文章,我会讲解我自己的hibernate、spring、beanutils框架,但讲解这些框架之前,我需要讲解RTTI和反射。
工作将近一年了,我们公司项目所使用的框架是SSH,或者,其他公司使用的是SSM框架。不管是什么样的框架,其都涉及到反射。那么,什么是反射?我们在生成对象时,事先并不知道生成哪种类型的对象,只有等到项目运行起来,框架根据我们的传参,才生成我们想要的对象。
比如,我们从前端调用后端的接口,查询出这个人的所有项目,我们只要传递这个人的id即可。当然,数据来源于数据库,那么,问题来了,数据是怎么从持久态转化成我们想要的顺时态的?这里面,就涉及到了反射。但是,一提到反射,我们势必就提到RTTI,即运行时类型信息(runtime Type Infomation)。
/** * 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来创建、修改对象,获取和修改属性的值等等。那么,反射是怎么创建当前类的呢?
/** * 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; }
框架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();这种一般是无参构造器,并且创建对对象后,可以获取其属性,通过属性赋值和方法赋值,如如代码所示:
/** * 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框架,来讲解更好地讲解反射。