Java程序设计19——类的加载和反射-Part-B

  接下来可以随意提供一个简单的主类,该主类无须编译就可使用上面的CompileClassLoader来运行它。

1 package chapter18;
2 
3 public class Hello {
4     public static void main(String[] args){
5         for(String arg:args){
6             System.out.println("运行Hello的参数:" + arg);
7         }
8     }
9 }

  无须编译该Hello.java,可以直接使用如下命令来运行该Hello.java程序。
java CompileClassLoader Hello buenas

运行结果如下:
CompileClassLoader:正在编译Hello.java...
运行Hello的参数:buenas

本示例程序提供的类加载器功能比较简单,仅仅提供了在运行之前先编译Java源文件的功能。

实际上,使用自定义的类加载器,可以实现如下常见功能:
1.执行代码前自动验证数字签名。
2.根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译class文件。
3.根据用户需求来动态地加载类
4.根据应用需求把其他数字以字节码的形式加载到应用中

3.3 URLClassLoader类    

  Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处是父类,而不是父类加载器,这里是类之间的继承关系),URLClassLoader功能比较强大,它既可以从本地文件系统读取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。
  实际上应用程序中可以直接使用URLClassLoader来加载类,URLClassLoader类提供了如下两个构造器:
  URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。
  URLClassLoader(URL[] urls,ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与之前一个构造器相同
  一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass方法来加载指定类,下面程序示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接,通过这种方式来取得数据库连接就可以无须将MySQL驱动添加到CLASSPATH环境变量中。

 1 package chapter18;
 2 
 3 import java.util.*;
 4 import java.net.URL;
 5 import java.net.URLClassLoader;
 6 import java.sql.*;
 7 
 8 public class URLClassLoaderTest {
 9     private static Connection conn;
10     //定义一个获取数据库连接的方法
11     public static Connection getConn(String url, String user, String pass) throws Exception{
12         if(conn == null){
13             //创建一个URL数组
14             URL[] urls = {new URL("file:mysql-connector-java-3.1.10-bin.jar")};
15             //以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
16             URLClassLoader myClassLoader = new URLClassLoader(urls);
17             //加载MYSQL的JDBC驱动,并创建默认实例
18             Driver driver = (Driver)myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
19             //创建一个设置JDBC连接属性的Properties对象
20             Properties props = new Properties();
21             //至少需要为对象传入user和passwd属性
22             props.setProperty("user", user);
23             props.setProperty("password", pass);
24             //调用Driver对象的connect方法来获取数据库连接
25             conn = driver.connect("jdbc:mysql://localhost:3306/mysql", props);
26         }
27         return conn;
28     };
29     public static void main(String[] args) throws Exception{
30         System.out.println(getConn("jdbc:mysql///mysql", "root", "mysqladmin"));
31     };
32 }
33 输出结果:com.mysql.jdbc.Connection@f81843

4 通过反射查看类信息                                               

  Java程序中许多对象在运行时都会出现两种类型:编译时类型和运行时类型,例如:Person p = new Student();这行代码将会生成一个p变量,该变量的编译类型为Person,运行时类型为Student;除此之外还有更极端的情形,程序在运行时接收到外部传入的一个对象,该对象的编译类型是Object,但程序又需要调用该对象运行时类型的方法。
  为了解决这些问题,程序需要在运行时发现对象和类的真实信息,通常有两种做法:
  1.第一种是假设在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。
  2.第二种是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

4.1 获得Class对象                

  每个类被加载之后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类,Java程序中获得Class对象通常有如下三种形式:
1.使用Class类的forName()静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
2.调用某个类的class属性来获取该类对应的Class对象。例如Person.class将会返回Person类对应的Class对象
3.调用某个对象的getClass()方法,该方法是java.lang.Object类中的一个方法,所以所有Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。
  对于第一种方式和第二种方式都是直接根据类来取得该类的Class对象,但相比之下,第二种方式有如下两种优势:
1.代码更安全,程序在编译阶段就可以检查需要访问的Class对象是否存在。
2.程序性能更高,因为这种方式无须调用方法,所以性能更好。
  也就是说,大部分时候我们都应该使用第二种方式来获取指定类的Class对象,但如果我们只有一个字符串,例如java.lang.String,如果需要获取更多该字符串对应的Class对象,则只能使用第一种方式了,使用Class的forName方法获取Class对象时,该方法可能抛出一个ClassNotFoundException异常。
  一旦获得了某个类所对应的Class对象之后,程序就可以调用Class对象的方法来获得该对象和该类的真实信息。

4.2 从Class中获取信息        

  Class类提供了大量实例方法来获取该Class对象所对应类的详细信息,Class类大致包含如下几种方法,下面每种方法都可能包含多个重载的版本。

 

 

  上面的多个getMethod方法和getConstructor多个方法中,都有一个需传入参数类型的Class<?>、个数可变的参数,用于获取指定的方法或指定构造器。关于这个参数的作用,假设某个类包含如下三个info方法签名:

1.public void info()
2.public void info(String str)
3.public void info(String str, Integer num)
View Code

  这两个同名方法属性重载,它们的方法名相同,但参数列表不同。在Java语言中要确定一个方法光有方法名是不行的。假如我们想要确定第二个info方法,必须指定方法名为info,形参列表为String.class————因此程序中获取该方法使用如下代码:

//前一个参数指定方法名,后面的个数可变的Class参数指定形参类型列表
clazz.getMethod("info", String.class)
如果需要获取第三个info方法,则使用如下代码:
//前一个参数指定方法名,后面的个数可变的Class参数指定形参类型列表
clazz.getMethod("info", String.class, Integer.class)
View Code

  获取构造器时无须传入构造器名————同一个类的所有构造器的名字都是相同的,所以要确定一个构造器只要指定形参列表即可。

下面程序示范了如何通过该Class对象来获取对应类的详细信息。

 1 package chapter18;
 2 
 3 import java.lang.annotation.Annotation;
 4 import java.lang.reflect.Constructor;
 5 import java.lang.reflect.Method;
 6 
 7 //使用两个注释修改该类
 8 @SuppressWarnings(value = "unchecked")
 9 @Deprecated
10 public class ClassTest {
11     //为该类定义一个私有构造器
12     private ClassTest(){
13         
14     }
15     //定义一个有参数的构造器
16     public ClassTest(String name){
17         System.out.println("执行有参数的构造器");
18     }
19     //定义一个无参数的info方法
20     public void info(){
21         System.out.println("执行无参数的info方法");
22     };
23     public void info(String str){
24         System.out.println("执行有参数的info方法,其参数是" + str);
25     };
26     //定义一个测试用的内部类
27     class Inner{
28         
29     };
30     public static void main(String[] args) throws Exception{
31          //下面代码可以获取ClassTest对应的Class
32         Class<ClassTest> clazz = ClassTest.class;
33         //获取该Class对象所对应的全部构造器
34         Constructor[] ctors = clazz.getDeclaredConstructors();
35         System.out.println("ClassTest的全部的构造器如下:");
36         for(Constructor c:ctors){
37             System.out.println(c);
38         }
39         //获取该Class对象所对应的全部Public方法
40         Method[] mtds = clazz.getMethods();
41         System.out.println("ClassTest的全部methods方法如下");
42         for(Method md:mtds){
43             System.out.println(md);
44         }
45         //获取该Class对象所对应类的指定方法
46         System.out.println("ClassTest里一个带字符串参数的info方法为"
47             + clazz.getMethod("info", String.class));
48         //获取该Class对应类的全部注释
49         Annotation[] anns = clazz.getAnnotations();
50         //ClassTest的全部annotations为
51         for(Annotation ans:anns){
52             System.out.println(ans);
53         }
54         System.out.println("该Class元素上的@SuppressWarnings注释为: "
55             + clazz.getAnnotation(SuppressWarnings.class));
56         //获取该Class对象所对应类的全部内部类
57         Class<?>[] inners = clazz.getDeclaredClasses();
58         System.out.println("ClassTest的全部内部类如下: ");
59         for(Class c:inners){
60             System.out.println(c);
61         }
62         //使用Class.forName方法来加载ClassTest的所有内部类
63         Class inClazz = Class.forName("ClassTest$Inner");
64         //通过getDeclaringClass()访问该类所在的外部类
65         System.out.println("inClazz对应的类所在的外部类为" + inClazz.getDeclaringClass());
66         System.out.println("ClassTest所在的包为" + clazz.getPackage());
67         System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
68     }
69 }
上面程序获取了ClassTest类对应的Class对象后,通过调用该Class对象的不同方法来得到该Class对象的详细信息,运行程序如下:
ClassTest的全部的构造器如下:
private chapter18.ClassTest()
public chapter18.ClassTest(java.lang.String)
ClassTest的全部methods方法如下
public static void chapter18.ClassTest.main(java.lang.String[]) throws java.lang.Exception
public void chapter18.ClassTest.info()
public void chapter18.ClassTest.info(java.lang.String)
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
Exception in thread "main" java.lang.ClassNotFoundException: ClassTest$Inner
	at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:169)
	at chapter18.ClassTest.main(ClassTest.java:63)
ClassTest里一个带字符串参数的info方法为public void chapter18.ClassTest.info(java.lang.String)
@java.lang.Deprecated()
该Class元素上的@SuppressWarnings注释为: null
ClassTest的全部内部类如下: 
class chapter18.ClassTest$Inner 

  从运行结果来看,Class提供的功能非常丰富,它可以获取该类里包含的构造器、方法、内部类、注释等信息,也可以获取该类所包括的属性信息————通过getFields()或getField(String name)方法即可。
  值得指出的是,虽然我们定义ClassTest类时使用了@SuppressWarnings注释,但程序运行时无法分享出该类里包含的该注释,这是因为@SuppressWarnings使用了@Retention(value=SOURCE)修饰,这表明@SuppressWarnings只能保存在源代码级别上,而通过ClassTest.class获取该类的运行时Class对象,所以程序无法访问到@SuppressWarnings注释。
  对于只能在源代码上保留的注释,使用运行时获得的Class对象无法访问到该注释对象。
  通过Class对象可以得到大量Method、Constructor、Field等对象,这些对象分别代表该类所包括的方法、构造器和属性等,程序还可以通过这些对象来执行实际的功能:例如调用方法、创建实例。

4.3 使用反射生成并操作对象  

  Class对象可以获得该类里包括的方法(由Method对象表示)、构造器(由Constructor对象表示)、Field(由Field对象表示),这三个类都定义在java.lang.reflect包下,并实现了java.lang.reflect.Member接口,程序可以通过Method对象来执行对应的方法,通过Constructor对象来调用对应的构造器创建对象,能通过Field对象直接访问并修改对象的属性值。

4.3.1 创建对象      

  通过反射来生成对象有如下两种方式:
1.使用Class对象的newInstance()方法来创建该Class对象对应类的实例,这种方式要求该Class对象的对应类由默认构造器,而执行newInstance()方法时实际上是利用默认构造器来创建该类的实例。
2.先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用某个类的指定构造器来创建实例。
  通过第一种方式来创建对象是比较常见的情形,因为在很多Java EE框架中都需要根据配置文件信息来创建Java对象,从配置文件读取的知识某个类的字符串类名,程序就需要根据该字符串来创建对应的实例,就必须使用反射。
  下面程序就实现了一个简单的对象池,该对象池会根据配置文件读取name-value对,然后创建这些对象,并将这些对象放入一个HashMap中。

View Code

  上面程序里createObject方法里的两行代码就是根据字符串来创建Java对象的关键代码,程序调用Class对象的newInstance即可创建一个Java对象。程序的initPool()方法会读取属性文件,对属性文件中每个name-value对创建一个Java对象,其中value是该Java对象的实现类,而name是该Java对象放入对象池中的名字。为该程序提供如下配置文件
a=java.util.Date
b=javax.swing.JFrame
得到一个运行时间如下:
Fri Jul 26 08:43:07 CST 2013
  编译、运行上面的ObjectPoolFactory程序,程序执行main方法,将可以看到输出当前系统时间————这表明对象池已经有了一个名为a的对象,该对象是一个java.util.Date对象。
  这种使用配置文件来配置对象,然后有程序根据配置文件来创建对象的方式非常有用,著名的Spring框架就是采用这种方式大大简化了Java EE应用的开发。当然,Spring采用的XML配置文件————毕竟属性文件能配置的信息太有限了,而XML配置文件能配置的信息就丰富多了。

  如果我们不想利用默认构造器来创建Java对象,而想利用指定的构造器来创建Java对象,则需要利用Constructor对象了,每个Constructor对应一个构造器。为了利用指定构造器来创建Java对象需要如下三个步骤:
1.获取该类的Class对象
2.利用Class对象的getConstructor()方法来获取指定构造器
3.调用Constructor的newInstance()方法来创建Java对象
下面程序利用反射来创建一个JFrame对象,而且使用指定的构造器

View Code

  上面程序中Class<?> jframeClazz = Class.forName("javax.swing.JFrame");用于获取JFrame中的指定构造器,前面已经提到:如果要唯一地确定某类中的构造器,只要指定构造器的形参列表即可。本类中获取构造器时传入了一个String类型,即表明想获取只有一个字符串参数的构造器。
  该行代码Constructor ctor = jframeClazz.getConstructor(String.class);Object obj = ctor.newInstance("测试窗口");使用指定的构造器的newInstance方法来创建一个Java对象,当调用Constructor对象的newInstance()方法时通常需要传入参数,因为调用Constructor的newInstance()方法实际上等于调用它对应的构造器,传给newInstance()方法的参数实际将作为对应构造器的参数。
  对于上面的CreateFrame.java中已知java.swing.JFrame类的情形,通常没有必要使用反射来创建该对象,毕竟通过反射创建对象时性能要稍低一些,实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性毕竟广的框架、基础平台时可能会大量使用反射。

4.4 调用方法                    

  当获得某个类对应的Class对象后,就可以通过该Class对象的getMethods()方法或者getMethod()方法来获取全部方法或指定方法————这两个方法的返回值是Method对象数组或者Method对象
  每个Method对象对应一个方法,获得Method对象后,程序就可通过该Method来调用对应方法,在Method里包含一个invoke方法,该方法的签名如下:
1.Object invoke(Object obj,Object...args):该方法中的obj是执行该方法的主调,后面的args是执行该方法时传入该方法的实数。
  下面程序对前面的对象池工厂进行加强,程序允许在配置文件增加配置对象的属性值,对象池工厂会读取该对象的属性值,并利用该对象对应的setter方法为对应属性设置值。

View Code

  当通过Method的invoke方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限。如果程序确实需要调用某个对象的private方法,可以先调用Method对象的如下方法:
1.setAccessible(boolean flag):当Method对象的accessible标志设置为指示的布尔值。值为true则指示该Method在使用时应该取消Java语言访问权限检查。值为false则指示该Method在使用时应该实施Java语言访问权限检查。

4.5 访问属性值                  

  通过Class对象的getFields()或getField()方法可以获取该类所包括的全部Field(属性)或指定Field。Field提供了如下两组方法来访问属性:
1.getXxx(Object obj):获取obj对象该Field的属性值,此处的Xxx对应8个基本类型,如果该属性的类型是引用类型,则取消get后面的Xxx
2.setXxx(Object obj,Xxx val):将obj对象的该Field设置成val值。此处的Xxx对应8个基本类型,如果该属性的类型是引用类型则取消set后面的Xxx
使用这两个方法可以随意地访问指定对象的所有属性,包括private访问控制的属性

View Code

  上面程序先定义了一个Person类,该类里包含两个private属性:name和age,通常情况下,这两个属性只能在Person类里访问。本程序在FieldTest的main方法中通过反射修改了Person对象的name属性值。
  本程序使用getDeclaredField()方法获取了名为name的Field,注意此处不是使用getField()方法,因为getField只能获取public访问控制的Field,而getDeclared方法可以获取所有Field。

4.6 操作数组                    

  在java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array来动态地创建数组,操作数组元素等。
Array提供了如下几类方法:

static Object newInstance(Class<?> componentType, int length):创建一个具有指定的组件类型和长度的新数组。 
static void set(Object array, int index, Object value):将指定数组对象中索引组件的值设置为指定的新值。 
static Object get(Object array, int index):返回指定数组对象中索引组件的值。 
View Code

  下面程序示范了如何使用Array来生成数组,为指定数组元素赋值,并获取指定数组元素的方式。

View Code

  上面通过Array创建数组,为数组元素设置值,访问数组元素的值。
  下面创建一个三维数组

View Code

5 使用反射生成JDK动态代理                            

  在Java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口可以生成JDK动态代理类或动态代理对象。
使用Proxy和InvocationHandler创建动态代理
  Proxy提供用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。如果我们在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy来创建动态代理类:如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。
  Proxy提供了如下两个方法来创建动态代理类和动态代理实例:

static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces):创建一个动态代理类所对应的Class对象,该代理类将实现interfaces所指定的多个接口。第一个ClassLoader指定生产动态代理类的类加载器。
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法。
View Code

  实际上,即使采用第一种方式获取了一个动态代理类之后,当程序需要通过该代理类来创建对象时一样需要传入一个InvocationHandler对象。也就是说,系统生成的每个代理对象都有一个与之关联的InvocationHandler对象。执行动态代理对象里每个方法时,实际上都是执行InvocationHandler对象的invoke方法

  程序中生成动态代理对象可以采用生成一个动态代理类,然后通过动态代理类来创建代理对象的方式来生成一个动态代理对象。

View Code

下面程序示范了使用Proxy和InvocationHandler来生成动态代理对象。

View Code

  程序分析:上面程序中提供了一个People接口,该接口中包含了walk和sayHello两个抽象方法,接着程序定义了一个简单的InvocationHandler实现类,定义该实现类时需要重写invoke方法————执行代理对象所有方法执行时将会替换成执行此invoke方法。该invoke方法中三个参数的解释如下:
1.proxy:代表动态代理对象
2.method:代表正在执行的方法
3.args:代表执行代理对象的方法时传入的实参
  上面程序中创建了一个InvocationHandler对象,接着根据InvocationHandler对象创建一个动态代理对象,运行程序即可得到上面的结果。
  从结果可以看出,不管程序执行代理对象的walk()方法,还是执行代理对象的sayHello()方法,实际上都是执行InvocationHandler对象的invoke()方法。
  看完了上面示例程序,可能觉得这种程序没有什么价值,难以理解Java动态代理的美丽,实际上,在普遍编程过程中,确实无须使用动态代理,但在编写框架或底层基础代码时,动态代理的作用就很大。

5.1 动态代理和AOP             

  前面介绍的Proxy和InvocationHandler,很难看出这种动态代理优势,下面介绍一种更实用的动态代理机制。
  只要我们开发一个实际使用的软件系统,总会出现相同代码段重复出现的情形,这种情形下,刚开始从事软件开发的人,通常会ctl+c然后ctl+v,如果仅仅从软件功能上来看,他们确实已经实现了开发。前面介绍接口时候已经说了,这样做,当需要修改代码时候,那么将有N个地方需要修改,修改、维护代码的工作量将会非常恐怖。
  在这种情况下,大部分有经验的开发者都会将这段重复代码定义成一个方法,然后让另外三段代码块直接调用该方法即可。

  虽然如此,采用这种方式来实现代码复用依然产生一个重要问题:代码块1、代码块2、代码块3和深色代码块分开了,但代码块1、代码块2、代码块3又和一个特定方法耦合了。最理想的效果是:代码块1、代码块2、代码块3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用深色代码的方法,这时候,就可以通过动态代理来达到这种效果。
PS:什么是硬编码
  在计算机程序或文本编辑中,硬编码是指将可变变量用一个固定值来代替的方法。用这种方法编译后,如果以后需要更改此变量就非常困难了。大部分程序语言里,可以将一个固定数值定义为一个标记,然后用这个特殊标记来取代变量名称。当标记名称改变时,变量名不变,这样,当重新编译整个程序时,所有变量都不再是固定值,这样就更容易的实现了改变变量的目的。尽管通过编辑器的查找替换功能也能实现整个变量名称的替换,但也很有可能出现多换或者少换的情况,而在计算机程序中,任何小错误的出现都是不可饶恕的。最好的方法是单独为变量名划分空间,来实现这种变化,就如同前面说的那样,将需要改变的变量名暂时用一个定义好的标记名称来代替就是一种很好的方法。通常情况下,都应该避免使用硬编码方法。 
java小例子: int a=2,b=2;
硬编码:if(a==2) return false;
不是硬编码 if(a==b) return true;
一个简单的版本:
顾名思义, 就是把数值写成常数而不是变量
如求圆的面积 的问题 PI(3.14)
3.14*r*r (这个3.14就是hardcode)
PI*r*r (这里的PI用的是变量形式,就不是hardcode)
C++例子:
int user[120];
如果突然在程序中出现下面一段代码
for (int i=0; i<120; i++){
...
}
120是什么,为什么是120?这里的120就属于数字式“硬编码”,这不仅让程序很难读,而且不易维护。如果要修改120,就的修改程序中所有与此有关的120。应将数字式“硬编码”声明成一个宏,这样程序不仅易读,而且还可以一改全改。
#define MAX_USER_CNT 120
for (int i=0; i<MAX_USER_CNT; i++){
...
}[1]


  由于JDK动态代理只能创建指定接口的动态代理,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在改接口里定义了两个方法。

View Code

  上面接口里只是简单定义了两个方法,并未提供方法实现。如果我们直接使用Proxy为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。在这种情况下,我们将先为该Dog接口提供一个简单的实现类:GunDog

View Code

  回顾开始我们需要实现的功能:让代码块1、代码块2和代码块3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用深色代码的方法。此时假设info、run两个方法代表代码块1、代码块2,那么要求:程序执行info、run方法时能调用某个通用方法,但又不想以硬编码方式调用该方法。下面提供一个DogUtil类,该类里包含两个通用方法。

View Code

  借助于Proxy和InvocationHandler就可以实现:当程序调用info方法和run方法时,系统就可以自动将method1和method2两个通用方法插入info和run方法执行中。
  这个程序的关键在于下面的MyInvocationHandler类,该类是一个InvocationHandler实现类,该实现类的invoke方法将会作为代理对象的方法实现。

View Code

  上面程序实现invoke方法时包含了Object result = method.invoke(target , args);,这行代码通过反射以target作为主调来执行method方法,这就是实现了调用target对象的原有方法。

  下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例。

View Code

  上面动态代理工厂类提供了一个getProxy方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法————从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler对象的invoke方法。例如调用动态代理对象的info方法,程序将开始执行invoke方法,执行步骤如下:
1.创建DogUtil实例。
2.执行DogUtil实例的method1方法
3.使用反射以target作为调用者执行info方法
4.执行DogUtil实例的method2方法

  看到上面的执行过程,当我们使用动态代理对象来代替target对象时,代理对象的方法就实现了前面的要求:程序执行info、run方法时能调用method1、method2通用方法,但GunDog的方法中又没有以硬编码的方式调用method1和method2.
下面提供一个主程序测试这种动态代理的效果。

View Code

运行结果:
=====模拟第一个通用方法======
我是一只猎狗
=====摸你第二个通用方法=======
=====模拟第一个通用方法======
我跑的很快
=====摸你第二个通用方法=======
  上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行dog的info和run方法时,实际上会先执行DogUtil的method1,再执行target对象的info和run方法,最后再执行DogUtil的method2.

  从运行结果来看,采用动态代理可以非常灵活地实现解耦,通常而言,当我们使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象来生成动态代理。
这种动态代理在AOP(面向切面编程)里被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。

6 反射和泛型                                                

  反射和泛型 从JDK1.5之后,Java的Class类增加了泛型功能,从而允许使用泛型来限制Class类,例如String.class的类型实际上是Class<String>。如果将Class对应的类暂时未知,则使用Class<?>。通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换。 泛型和Class类 使用Class<T>泛型可以避免强制类型转换,例如下面提供一个简单的对象工厂,该对象工厂可以根据指定类来提供该类的实例。

View Code

  上面程序中根据指定字符串类型创建了一个新对象,但这个对象的类型是Object,因此当我们需要使用MyObjectFactory的getInstance()方法来创建对象时,将会看到如下代码: //获取实例后需要强制类型转换 Date d = (Date)MyObjectFactory.getInstance("java.util.Date"); 甚至出现如下代码 JFrame f = (JFrame)MyObjectFactory.getInstance("java.util.Date"); 上面代码在编译时候不会有问题,但运行时候将抛出ClassCastException异常,因为程序强制将一个Date对象转换成JFrame对象。 如果我们将上面的MyObjectFactory工厂类改写成使用泛型后的Class,就可以避免这种情况。

View Code

  在上面程序的getInstance()方法中传入一个Class<T>参数,这是一个泛型化的Class对象,调用该Class对象的newInstance()方法将返回一个T对象。接下来当我们使用 OurObjectFactory工厂类的getInstance()方法来产生对象时,无须使用强制类型转换,系统会执行更严格的检查,不会出现ClassCastException运行时异常。 前面介绍使用Array类来创建数组时,曾有如下代码 //Array的newInstance方法来创建一个数组 Object arr = Array.newInstance(String.class, 10); 对于上面的代码其实并不是很方便,因为我们知道newInstance方法返回的确实是一个String数组,而不是简单Object对象,如果我们需要将arr对象仓储String数组使用时,必须再次使用枪支类型转换————这是不安全的操作。 Array的newInstance方法的签名如下形式: public static Object newInstance(Class<?>componentType, int length):在这个方法前面使用了Class<?>泛型,这有些浪费,如果将该方法签名改为如下: public static Object newInstance(Class<T>T[], int length):这样调用该方法后无需强制类型转换了。

View Code

  使用反射来获取泛型信息 通过制定类对应的Class对象,程序可以获得该类里包括的所有Field,不管该Field使用private修饰,还是使用public修饰,获得了Field对象后,就可以很容易地获得该Field的数据类型,即使用如下代码可获得指定Field的类型。

//获取Field对象f的类型 Class<?> a = f.getType();

但通过这种方式只对普通类型的Field有效,如果该Field的类型是有泛型限制的类型,如Map<String, Integer>类型,则不能准确得多该Field的泛型参数。 为了获得指定Field的泛型类型,应先使用如下方法来获取指定Field的泛型类型:

//获得Field实例f的泛型类型 Type gType = f.getGenericType();

然后将Type对象强制类型转换为ParameterizedType对象,Parameterized代表被参数化的类型,也就是增加了泛型限制的类型。Parameterized类提供了两个方法: getRawType():返回被泛型限制的类型 getActualTypeArguments():返回泛型参数类型。 下面是一个获取泛型类型的完整程序

View Code
View Code

  从上面运行结果可以看出,直接使用Field的getType()方法只能获得普通类型的Field的数据类型;对于泛型类型的Field,应该使用getGenericType方法来取得其类型。 Type也是java.lang.reflect包下的一个接口,该接口代表所有类型的公共高级接口,Class是Type接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型等。

7 本章小结               

  本章详细介绍了Java反射的相关知识.从类的加载、初始化开始介绍,深入介绍了Java类加载器的原理和机制。除此之外还介绍了Class、Method、Field、Constructor、Type、ParameterizedType等类和接口的用法,包括动态创建Java实例和动态调用Java对象的方法。

 

 

 

posted @ 2013-07-30 21:59  朗道二级相变  阅读(560)  评论(1编辑  收藏  举报