反射和JavaBean

一、反射的基石:类Class

   1、概述:

         Java程序中各个java类同属于一类事物,描述这类事物的java类是Class。

         注意区分类修饰符class和类Class。前者是对某类事物的一个修饰,这些事物可以是任何事物,而这些事物的具体属性值是什么,则由该类的实例对象来确定,同类型事物的不同实例对象有不同的属性值;后者是对java中所有类的字节码的一种描述,是java类在内存中的一种表现形式。每一个java类都存在字节码,而这些字节码就用类Class描述,即是对类的描述。

         字节码:一个类被类加载器加载到内存中,占用一片内存储存空间,这个空间里面的内容就是字节码,不同类的字节码是不同的,相同类(即使该类事物的实例对象不同)的字节码绝对一样。

   2、获取字节码的三种方式:

         (1) 类名. class;  如:Person.class;

         (2) 对象. getClass();  如:new Date().getClass();

         (3) Class.forName("类名"); 如:Class.forName("java.util.Date"),这种方式最常用。

   3、9个预定义 Class 对象:

         参照 Class.isPrimitive() 方法的 API 文档。

         九种预定义的 Class 对象,表示八个基本类型和 void。这些类对象由 Java 虚拟机创建,与其表示的基本类型同名,即 boolean、byte、char、short、int、long、float 和 double。也只有在判断9个预定义Class对象时,isPrimitive()才返回true,否则返回false。

         示例:int.class.isPrimitive(); 返回true。

                     int.class == Integer.TYPE; 返回true。

                     int.class == Integer.class; 返回false。 

二、反射

   1、概念:把java类中各种成分映射成相应的java类或理解为通过类加载器加载类,并解剖出类的各个组成部分。

         一个类中的成员变量、方法、构造方法、包、注释、参数等信息,用一个个java中相应的实例对象来表示,他们是:Field(字段/成员变量),Method,Constructor,Package,Annotation,Parameter。

   2、类Constructor:描述类中的构造方法

         (1) 得到某个类的所有/一个构造方法:

               示例:Constructor[] cons = Class.forName("java.util.String").getConstructors();

                           Constructor con = Class.forName("java.util.String").getConstructor("StringBuilder.class");

         (2) 创建实例对象:

               一般方式:String str = new String(new StringBuilder("hit me"));

               反射方式:String str = (String)con.newInstance(new StringBuilder("hit me"));

         (3) Class类中的方法 newInstance( ):

               示例:String str = (String)Class.forName("java.util.String").newInstance();

               解析:该方法内部先得到无参的构造方法,然后用该构造方法创建实例对象。该方法内部用缓存机制来保存默认的构造方法的实例对象。

               作用:简化代码获取构造方法的步骤,使用方便。

   3、类Field:描述类中的成员变量(字段)

         问题:得到的Field对象是对应到类上面的字段还是该类实例对象上的成员变量?

             答:类只有一个,而类的实例对象有多个,如果与实例对象相关联,那么就不确定到底是关联的哪一个对象,所以FieldX代表的是X的定义(即类的成员变量),而不是具体的X变量(对象字段)。类的成员变量是一种属性描述,可能不具备具体的值,但是对象的成员变量一定有特定的值来表示。

         需求:将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"a"改成"@" 。

          public void changeStringValue(Object obj) throws Exception{
                    Field[] fields = obj.getClass().getFields();
                    for (Field field : fields) {
                              //不用equals的原因:因为相同类都只有一个字节码,一对一的比较用==比较好。
                              // if(field.getType().equals(String.class))
                              if (field.getType()==String.class) {
                                        String oldValue = (String)field.get(obj);
                                        String newValue = (String)oldValue.replace("a", "@");
                                        field.set(obj, newValue);
                              }
                    }
          }

   4、类Method:描述类中的方法

         (1) 得到类中的某一个方法:

               Method charAtMethod = Class.forName("java.lang.String").getMethod("charAt", int.class);

                  getMethod方法中的两个参数:前者是需要调用的方法名,后者是该方法中的参数的字节码表现形式。

         (2) 方法调用:

               一般方式:System.out.println(str.charAt(2));

               放射方式:System.out.println(charAtMethod.invoke(str, 2));

               int  invoke(Object obj, Object...args); 

               invoke方法返回的是对应Method对象的哈希值,如果 x.invoke(null, 2); 这意味着该Method对象对应的是一个静态方法。因为静态方法是直接用类名调用的,不需要实例对象。

               需求:写一个程序,根据用户提供的类名,去执行该类中的静态的main方法。

               涉及问题:main方法的参数是一个字符串数组,通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?

               答:按JDK1.5语法,整个数组是一个参数,而按JDK1.4的语法,数组中的每一个元素对应为一个参数,当把一个字符串数组作为参数传递给invoke方法时,java编译器会按JDK1.4的语法进行处理,把数组打散成若干个单独的参数。所以在给main方法传递参数时不能使用代码:mainMethod.invoke(null, new String[]{"xxx"}); 会出现IllegalArgumentException异常。

               解决办法:

               一般方式:类名. main(new String[]{"abc", "def", "xyz"});

               反射方式:Method mainMethod = Class.forName((String)obj).getMethod("main", String[].class);               

               第一种传值方式:methodMain.invoke(null, new Object[]{new String[]{"abc", "def", "xyz"}});

               第二种传值方式:methodMain.invoke(null, (Object)new String[]{"abc", "def", "xyz"});

               方式一是将字符串数组封装到 Object 数组中作为一个元素,那么编译器拆包 Object 数组后得到的是一个数组型的元素。方式二是将字符串数组强制转换成 Object 类型,告诉编译器传递的是一个 Object 类型的元素,而不是数组,但这个元素的确是数组。其实就是给参数打了个包。  

   5、数组的反射:

         (1) 具有相同维数和元素类型的数组属于同一个类型,即具有相同的 Class 实例对象,字节码相同。

         (2) 代表数组的 Class 实例对象的 getSuperClass( ) 方法返回的父类为 Object 类对应的 Class 。

         (3) 基本类型的一维数组可以当作 Object 类型使用,不可以被当作 Object[] 使用,其他类类型的数组两者均可。

         (4) Arrays.asList()方法处理 int[ ] 和 String[ ] 时的差异:

                  JDK1.5中:asList(T... a); JDK1.4中:asList(Object[ ] obj);

                  两者在转向集合时,对于 int 类型的数组 ,会将该数组作为一个对象传递到集合中,即传递进去的是一个地址。而对于 String 类型的数组,会将其拆包,获取String类型的值,所以最后会得到String数组中的每一个元素。

         (5) Array 数组类反射工具用于完成对数组的反射操作。

               需求:用反射来完成对基本类型数组的取值操作。

               public static void printObject(Object obj) throws Exception{
                        Class clazz = obj.getClass();
                        if (clazz.isArray()) {
                                 for (int i = 0; i < Array.getLength(obj); i++) {
                                          System.out.println(Array.get(obj, i));
                                 }
                        } else {
                                 System.out.println(obj);
                        }
               }

               思考:用数组引用怎么得到数组引用的类型名(即是int型还是String型等类型)?

               答:这个做不到,只能得到该引用的数组类型名,而且只能通过数组中具体的元素来获取其类型名。

                       Sting[] str = {"abc", "def", "xyz"};

                       str.getClass(); //返回:Class [S java.lang.String

                       str.getClass().getName(); //返回:[S java.lang.String

                       str[1].getClass();//返回:Class java.lang.String

                       str[1].getClass().getName(); //返回:java.lang.String

               注:若为基本数据类型数组,int[] arr = {1, 2, 3}; arr[0].getClass();这种写法错误,正解为:int.class

   6、配置文件存放位置和加载方式:

         位置:通常情况下我们会把配置文件存放在 .class 文件夹中,或在 .class 文件夹中创建一个资源文件夹,专门用来存放配置文件,这样更有层次感。

         4种加载方式:下面四种加载配置文件的方式在开发中经常用到,注意后两种格式后面加载配置文件路径的不同之处,后面两种要写绝对路径,即最前面带 "/" 。

         InputStream is = Reflect.class.getClassLoader().getResourceAsStream("cn/itcast/day1/config.properties");
         InputStream is = Reflect.class.getResourceAsStream("resources/config.properties");    //相对路径
         InputStream is = Reflect.class.getResourceAsStream("/cn/itcast/day1/resources/config.properties");//绝对路径
         InputStream is = Class.class.getResourceAsStream("/cn/itcast/day1/resources/config.properties");//绝对路径      

   7、反射的作用:实现框架功能

         (1) 框架是提前做好的,它不会知道开发者具体传入什么样的类,所以在程序中不能直接 new 某个对象,而要用到反射方式来做,这时是框架在调用开发者。

          (2) 工具类 也是提前做好的,但是内容都是固定的,而且是开发者调用工具类,会明确知道该工具类有什么作用,类名是什么。

   8、一般调用就可以实现操作,为什么还要使用反射?

         答:在某些时候,我们可能不知道用户具体传进来的是什么类,但知道一定会有一个特定的方法,这时就不能使用一般方式用对象引用来调用该方法,所以就用反射,即不用知道具体的类名,只要知道有这么个方法就行。JavaBean就是根据反射的特性来调用某类中根据规则命名的方法的。

三、JavaBean

   1、释义:JavaBean 是一种特殊的 java 类,主要用于传递数据信息,这种 java 类中的方法主要用于访问私有的字段,且方法名符合某种命名规则。满足这种命名规则的类,可以把它当作 JavaBean 来使用,也可以当作普通类来操作。不是说所有的 Java 类都可以当作 JavaBean 来使用,但所有 JavaBean 都可以当作普通类来使用。

   2、规则:在两个模块之间传递多个信息,可以将这些信息封装到一个 JavaBean 中,这种 JavaBean 的实例对象通常称之为值对象(VO:Value Object)。这些信息在类中用私有字段来存储,如果读取或设置这些字段的值,则需要通过一些相应的方法来访问,这个规则即是 Setter 和 Getter 。JavaBean 的属性是根据其中的 Setter 和 Getter 方法来确定的,而不是根据其中的成员变量。如方法名为 getAge,意思为设置 age ,至于设置的哪个变量不用管。方法 getAge 同理。去掉get和set前缀,剩余的部分既是属性名,如果剩余部分的第二个字母是小写的,则把第一个字母改成小写,若第二个字母是大写,则投头字母也要大写。

         示例:setId() 的属性名:id ;

                     getCPU() 的属性名:CPU ;

   3、JavaBean 的属性描述

         JavaBean  的属性不是指被当作 JavaBean  的类中所声明的字段,而是指通过 get 或 set 来描述字段的。如:public int getProper() {return xxx;} ,其类中没有声明 proper这么个成员变量,但是用 JavaBean  来看,却存在 proper这个属性/字段。任何 JavaBean  都有一个默认的字段 class,因为Object 类中有一个 getClass() 方法。所以在 JavaBean  中有多少个 get/set 就有多少个 JavaBean  属性。

   4、将一个符合 JavaBean 特点的普通类当作 JavaBean使用的好处。

         (1) 在 JavaEE开发中,经常用到 JavaBean ,很多环境要求按 JavaBean 方式进行操作,所以我们没的选择必须这么做。

         (2) JDK 中提供了对 JavaBean 进行操作的一些 API ,这套 API 就称之为内省(Introspector)。在不知道类名的情况下,如果开发者去通过 get 方法反问该类中的私有成员变量,那么得先获取该类的字节码,在通过 Class 类来获取该类中所有的方法返回到一个 Method 数组中,在遍历该数组来查看需要的 get 和 set 方法,再通过 invoke 方法来实现对私有成员变量的操作,这样做太麻烦,而且占用内存,不利于优化。所以使用内省这套 API 操作 JavaBean 比普通类的方式更方便。

         对 JavaBean 的简单内省操作: 

	//通过JavaBean来设置"y"这个属性。
	public static void setPropertyName(Object obj, String propertyName,
			Object value) throws Exception{
		//获取obj对象中的实例化属性(字段)propertyName。
		PropertyDescriptor pd = new PropertyDescriptor(propertyName, obj.getClass());
		//反射:获取只写(Setter)方法。
		Method methodSetY =  pd.getWriteMethod();
		//通过反射获取的方法来设置propertyName的值
		methodSetY.invoke(obj, value);
	}
	//通过JavaBean来获取"y"这个属性。
	public static Object getPropertyName(Object obj, String propertyName) 
			throws Exception {
		PropertyDescriptor pd = new PropertyDescriptor(propertyName, obj.getClass());
		//反射:获取只读(Getter)方法。
		Method methodGetY = pd.getReadMethod();
		Object retVal = methodGetY.invoke(obj);
		return retVal;
	}

         上面是通过 java.beans.PropertyDescriptor 类来完成 JavaBean 操作的,比较简单。

         需求:采用一种老式的方法完成,比较麻烦,但是一定要理解思想。

         思路:

         (1) 采用遍历 BeanInfo 的所有属性方式来查找和设置某个 obj 对象的属性名为 propertyName 的属性,通过Introspector.getBeanInfo() ,得到的 BeanInfo 对象封装引用 obj 所标识类的所有成员。

         (2) 其次通过 BeanInfo 中的方法 getPropertyDescriptors()  来获取 obj 所标识类中的所有成员变量,并将这些变量封装到数组中。

         (3) 遍历该数组,获取数组中与 propertyName 同名的成员变量,然后获取该字段对应的 get 或 set 方法,通过 getWriteMethod 或 getReadMethod 来完成。

         (4) 最后调用 Method 类中的 invoke 方法实现对字段的 set 或 get 操作。

	//通过一个比较麻烦的方法来做
	public static Object accessProperyName(Object obj, String propertyName,
			boolean flag, Object value) throws Exception {
		Object retVal = null;
		//通过内省获取对象obj的所有属性、公开的方法和事件
		BeanInfo info = Introspector.getBeanInfo(obj.getClass());
		//获取其所有字段
		PropertyDescriptor[] pdes = info.getPropertyDescriptors();
		for (PropertyDescriptor propertyDescriptor : pdes) {
			if (propertyDescriptor.getName().equals(propertyName)) {
				//判断为真,则进行set字段值的操作
				if (flag) {
					Method methodSet = propertyDescriptor.getWriteMethod();
					methodSet.invoke(obj,value);
				}
				Method methodGet = propertyDescriptor.getReadMethod();
				retVal = methodGet.invoke(obj);
				break;
			}
		}
		return retVal;
	}

   5、BeanUtils工具包:主要讲解包中的 BeanUtils 和 PropertyUtils工具类

         (1) 用BeanUtils 类先get 原来设置好的属性,再将其 set 为一个新值。

              注:get 属性时返回的结果是一个字符串,set 属性时可以接收任意类型的对象,通常使用字符串。

              ReflectPoint rp = new ReflectPoint (3);

              String propertyName = "y";

              String str = BeanUtils.getProperty(rp, propertyName); // str = rp.getY( )  =  "3";

              BeanUtils.setProperty(rp, propertyName, "9"); //重新设置y 的值为9

         (2) 用PropertyUtils 工具类先get 原来设置好的属性,再将其set 为一个新值。

              注:get 属性时返回的结果为该属性本来的类型,set 属性时只接收属性本来的类型。

              PropertyUtils.setProperty(rp, propertyName, 12);

              int time = PropertyUtils.getProperty(rp, propertyName);

         注意:BeanUtils 默认只支持8种基本数据类型同String字符串的转换,若要使得String能同其他类类型(如Date日期类)进行转换需要使用BeanUtils工具类中定义好的转换器(若有)或自己写一个转换器。

        总结:反射思想是java开发中的一个重点,实现反射的基础是通过字节码来完成对某个类中的成员进行获取并封装到对应类的实例对象中,然后在进行对所需成员进行操作,如:创建实例对象,调用某个方法,更改某个字段的值或对方法中的参数及其类型进行获取。

        反射在开发中的作用是实现框架功能,即提前实现某些功能,而不需要针对具体的类。它是提前做好的,只要满足其功能条件,就可以对不同类进行操作。一般方法不具备这样的特新,因为一般方法都得知道具体的类名才能对该类中的成员进行操作。

        反射的一个特是 JavaBean,很多类的描述都满足了 JavaBean 的条件,可以将这些类当作 JavaBean 来处理,所以针对其有了BeanUtils 这么个工具包,简化了使用 Introspector 和 BeanInfo 的操作。

你可能感兴趣的:(反射和JavaBean)