天天看点

Java 类加载机制与反射

Java 类加载机制与反射

1.1 JVM 和类

当调用 java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里。

同一个 JVM 的所有线程、所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区域。

当系统出现以下几种情况时, JVM 进程将被终止

    1. 程序运行到最后正常结束

    2. 程序运行到使用 System.exit() 或 Runtime.getRuntime().exit()代码处结束程序

    3. 程序在执行过程中遇到未捕获的异常或错误而结束

    4. 程序所在的平台强制结束了 JVM 进程

1.2 类的加载

------------------------------------------------------

当程序主动使用某个类时,如果该类还没有被加载到内存中,则系统会通过加载、连接、初始化三个步骤对该类进行初始化,如果没有意外, JVM 将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或初始化

类加载指的是将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。

每个类是一批具有相同特征的对象的抽象,而系统中所有的类实际上也是实例,它们都是 java.lang.Class 的实例。

类的加载由类加载器完成,类加载器通常由 JVM 提供,这些类加载器也是所有程序运行的基础, JVM 提供的这些类加载器通常被称为系统类加载器。

除此之外,开发者可以通过继承 ClassLoader 基类创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

    1. 从本地文件系统加载 class 文件

    2. 从 JAR 包加载 class  文件

    3. 通过网络加载 class 文件

    4. 把一个 Java 源文件动态编译,并执行加载

类加载器通常无须等到 “首次使用” 该类时才加载该类, Java 虚拟机规范允许系统预先加载某些类。

1.3 类的连接

----------------------------------------------------

当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到 JRE 中。

类连接又可以分为如下三个阶段:

    1. 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致

    2. 准备:类准备阶段则负责为类变量分配内存,并设置默认初始值

    3. 解析:将类的二进制数据的符号引用替换成直接引用

1.4 类的初始化

-----------------------------------------------------

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。

在 Java 类中对类变量指定初始值有两种方式:①声明类变量时指定初始值;②使用静态初始化块为类变量指定初始值

public class Test

{

    // 声明变量 a 时指定初始值

    static int a = 5;

    static int b;

    static int c;

    static

    {

        // 使用静态初始化块为类变量 b 指定初始值

        b = 6;

    }

}

声明变量时指定初始值,静态初始化块都将被当成类的初始化语句, JVM 会按这些语句在程序中的排列顺序一次执行它们。

JVM 初始化一个类包含如下几个步骤:

    1. 假如这个类还没有被加载和连接,则程序先加载并连接该类

    2. 假如该类的直接父类还没有初始化,则先初始化其直接父类

    3. 假如类中有初始化语句,则系统依次执行这些初始化语句

JVM 最先初始化的总是 java.lang.Object 类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会初始化

1.5 类初始化的时机

------------------------------------------------------------------

当 Java 程序首次通过下面6种方式使用某个类或接口时,系统就会初始化该类或接口:

    1. 创建类的实例:为某个类创建实例的方式包括:使用 new 操作符来创建实例,通过放射来创建实例,通过反序列化的方式创建实例

    2. 调用某个类的方法(静态方法)

    3. 访问某个类或接口的类变量,或为该类变量赋值

    4. 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。例如, Class.forName("Person"), 如果系统还未初始化 Person 类,则这行代码将会导致该 Person 类被初始化,并返回 Person 类对应的 java.lang.Class 对象

    5. 初始化某个类的子类。当初始化某个类的子类时,该类的所有父类都会被初始化

    6. 直接使用 java.exe 命令来运行某个主类,程序会先初始化该主类

除此之外,下面几种情况需要特别指出。

    1.  对于 final 型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于宏变量。 Java 编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。

        当某个类变量(也叫静态变量)使用了 final 修饰符,而且它的值可以在编译时就确定下来,那么程序其他地方使用该变量时,实际上并没有使用该类变量,而是相当于使用常量。

        反之,如果 final 修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。

    2. 当使用 ClassLoader 类的 loadClass() 方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。 使用 Class 的 forName() 静态方法才会导致强制初始化该类。

*

*

*

2 类加载器

-----------------------------------------------------------------------------------------------------------

类加载器负责将 .class 文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的 java.lang.Class 对象。一旦一个类被载入 JVM 中,统一各类就不会被再次载入。

当 JVM 启动时,会形成由三个类加载器组成的初始类加载器层次结构:

    1. Bootstrap ClassLoader: 根类加载器

    2. Extension ClassLoader: 扩展类加载器

    3. System ClassLoader: 系统类加载器

    Bootstrap ClassLoader 被称为引导(也称为原始或根)类加载器,它负责加载 java 的核心类。

    在 sun 的 JVM 中,当执行 java.exe 命令时,使用 -Xbootclasspath 选项或 -D 选项指定 sun.boot.class.path 系统属性值可以指定加载附加的类。

    根类加载器非常特殊,它并不是 java.lang.ClassLoader 的子类,而是 JVM 自身实现的。

    下面的程序可以获得根类加载器所加载的核心类库

public class BootstrapTest

{

    public static void main(String[] args)

    {

        // 获取根类加载器所加载的全部URL数组

        URL[] urls = sun.misc.Launcher.

        getBootstrapClassPath().getURLs();

        // 遍历、输出根类加载器加载的全部URL

        for (int i = 0; i < urls.length; i++)

        {

            System.out.println(urls[i].toExternalForm());

        }

    }

}

    Extension ClassLoader 被称为扩展类加载器,它负责加载 JRE 的扩展目录 (%JAVA_HOME%/jre/lib/ext 或者由 java.ext.dirs 系统属性指定的目录) 中的 JAR 包的类

    System ClassLoader 被称为系统 (也称为应用)类加载器,它负责在 JVM 启动时加载来自 Java 命令的 -classpath 选项、 java.class.path 系统属性, 或 CLASSPATH 环境变量所指定的 JAR 包和类路径。

    程序可以通过 ClassLoader 的静态方法 getSystemClassLoader() 获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。

2.2 类加载机制

-----------------------------------------------

JVM 的类加载机制主要有如下三种

    1. 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个 class 时,该 Class 所依赖的和引用的其他的 Class 也将由该类加载器负责载入,除非显示地使用另外一个类加载器载入

    2. 父类委托:所谓父类委托,则是先让父类加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

    3. 缓存机制:缓存机制将会保证所有加载过的 Class 都会缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜索该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。

    除了可以使用 Java 提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承 ClassLoader 来实现。

    类加载器之间的父子关系并不是继承上的父子关系,这里的父子关系是类加载器实例之间的关系。

    JVM 中这4种类加载器的层次结构图:

                |-------------------|

                |    根类加载器        |

                |--------|----------|    

                        /|\

                         |

                |--------|----------|

                |    扩展类加载器    |

                |--------|----------|    

                        /|\

                         |

                |--------|----------|

                    系统类加载器

                |--------|----------|

                        /|\

                         |

                |--------|----------|

                |    用户类加载器    |

                |-------------------|

                JVM 中的4种类加载器

public class ClassLoaderPropTest

{

    public static void main(String[] args)

        throws IOException

    {

        // 获取系统类加载器

        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();

        System.out.println("系统类加载器:" + systemLoader);

        Enumeration<URL> em1 = systemLoader.getResources("");

        while(em1.hasMoreElements())

        {

            System.out.println(em1.nextElement());

        }

        // 获取系统类加载器的父类加载器:得到扩展类加载器

        ClassLoader extensionLader = systemLoader.getParent();

        System.out.println("扩展类加载器:" + extensionLader);

        System.out.println("扩展类加载器的加载路径:"

            + System.getProperty("java.ext.dirs"));

        System.out.println("扩展类加载器的parent: "

            + extensionLader.getParent());

    }

}

从运行结果看出,系统类加载器是 AppClassLoader 的实例,扩展类加载器是 ExtClassLoader 的实例。实际上,这两个类都是 URLClassLoader 类的实例。

JVM 的根类加载器并不是 Java 实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回 null。

类加载器加载 Class 大致要经过如下 8 个步骤:

    1. 检测此 Class 是否载入过 (即在缓存区中是否有此 Class), 如果有则直接进入第 8 步,否则接着执行第2步

    2. 如果父类加载器不存在(如果没有父类加载器,则要么 parent 一定是根加载器,要么本身就是根加载器),则跳到第4步,如果父类加载器存在,则接着执行第3步。

    3. 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步

    4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步

    5. 当前类加载器尝试寻找 Class 文件(从此 ClassLoader 相关路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。

    6. 从文件中载入 Class, 成功后跳到第8步

    7. 抛出 ClassNotFoundException 异常

    8. 返回对应的 java.lang.Class 对象。

    其中,第5、6步允许重写 ClassLoader 的 findClass() 方法来实现自己的载入策略,甚至重写 loadClass() 方法来实现自己的载入过程。

2.3 创建并使用自定义的类加载器

---------------------------------------------------------------------

ClassLoader 类有如下两个关键方法:

    protected Class<?>     loadClass(String name, boolean resolve) : 该方法为 ClassLoader 的入口点,根据指定名称加载类, 系统就是调用该方法来获取指定类对应的 Class 对象。

    protected Class<?>     findClass(String name) : 根据指定名称查找类

    如果需要实现自定义 ClassLoader, 可以通过重写以上连个方法实现,通常推荐重写 findClass() 方法。

    loadClass() 方法的执行步骤:

        1. 调用 findLoadedClass(String) 检查是否已经加载类,如果已经加载则直接返回

        2. 在父类加载器上调用 loadClass() 方法,如果父类加载器为 null, 则使用根类加载器来加载

        3. 调用 findClass(String) 方法查找类。

    ClassLoader 里还有一个核心方法: protected Class<?>     defineClass(String name, byte[] b, int off, int len), 该方法负责将指定类的字节码文件读入字节数组 byte[] b 内,并把它转换为 Class 对象。该字节码文件可以来源于文件、网络等。该方法为 final , 无须重写。

2.4 URLClassLoader 类

---------------------------------------------------------------------

Java 为 ClassLoader 提供了一个 URLClassLoader 实现类,该类是系统类加载器和扩展类加载器的父类(此处的父类指的是类与类之间的继承关系)。

URLClassLoader 功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类,程序可以直接使用 URLClassLoader 加载类。

构造器:

    URLClassLoader(URL[] urls)

    URLClassLoader(URL[] urls, ClassLoader parent)

    URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)

一旦得到 URLClassLoader 对象之后,就可以调用该对象的 loadClass() 方法来加载指定的类。

例子:

public class URLClassLoaderTest

{

    private static Connection conn;

    // 定义一个获取数据库连接方法

    public static Connection getConn(String url ,

        String user , String pass) throws Exception

    {

        if (conn == null)

        {

            // 创建一个URL数组

            URL[] urls = {new URL(

                "file:mysql-connector-java-5.1.30-bin.jar")};

            // 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader

            URLClassLoader myClassLoader = new URLClassLoader(urls);

            // 加载MySQL的JDBC驱动,并创建默认实例

            Driver driver = (Driver)myClassLoader.

                loadClass("com.mysql.jdbc.Driver").newInstance();

            // 创建一个设置JDBC连接属性的Properties对象

            Properties props = new Properties();

            // 至少需要为该对象传入user和password两个属性

            props.setProperty("user" , user);

            props.setProperty("password" , pass);

            // 调用Driver对象的connect方法来取得数据库连接

            conn = driver.connect(url , props);

        }

        return conn;

    }

    public static void main(String[] args)throws Exception

    {

        System.out.println(getConn("jdbc:mysql://localhost:3306/mysql"

            , "root" , "32147"));

    }

}

*

*

*

3 通过反射查看类的信息

---------------------------------------------------------------------------------------------------------------

Java 程序中的许多对象在运行时会出现两种类型:编译时类型和运行时类型

Person p = new Student();

这行代码将会生成一个 p 变量,该变量的编译时类型是 Person, 运行时类型为 Student;

有更极端的情形,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是 Object, 但程序又需要调用该对象运行时类型的方法。

为了解决这些问题,程序需要在运行时发现对象和类的真实信息。解决该问题有以下两种做法:

    1. 第一种做法是假设在编译时和运行时都完全知道类型的具体信息,这种情况下,可以先用 instanceof 运算符进行判断,再利用强制类型转换将其转换成其运行时类型变量。

    2. 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

3.1 获取 Class 对象

----------------------------------------------------------------

每个类被加载之后,系统就会为该类生成一个对应的 Class 对象,通过该 Class 对象就可以访问到 JVM 中的这个类。在 Java 程序中获得 Class 对象通常有三种方法:

    1. 使用 Class 类的 forName(String clazzName) 静态方法。该字符串参数的值是某个类的全限定名(添加完整的包名)。

    2. 调用某个类的 class 属性来获取该类对应的 Class 对象,例如 Person.class 将会返回 Person 类对应的 Class 对象

    3. 调用某个对象的 getClass() 方法,该方法是 java.lang.Object 类的一个方法,所以所有的 Java 对象都可以调用该方法,该方法返回该对象所属类对应的 Class 对象

对于第一种方式和第二种方式都可以直接根据类来取得该类的 Class 对象,相比之下,第二种方式有两个优势

        1. 代码更安全,程序在编译阶段就可以检查需要访问的 Class 对象是否存在

        2. 程序性能更好

        因此,大部分情况下都应该使用第二种方式来获取类的 Class 对象

获得某个类的 Class 对象后,程序就可以调用 Class 对象的方法来获取该对象和类的真实信息

3.2 从 Class 中获取信息

-----------------------------------------------------------------

Class 类提供了大量的实例方法来获取该 Class 对象所对应类的详细信息:

     Constructor<T>     getConstructor(Class<?>... parameterTypes);

     Constructor<?>[]     getConstructors();

     Constructor<T>     getDeclaredConstructor(Class<?>... parameterTypes);

     Constructor<?>[]     getDeclaredConstructors();

     Method     getMethod(String name, Class<?>... parameterTypes);

     Method[]     getMethods();

     Method     getDeclaredMethod(String name, Class<?>... parameterTypes);

     Method[]     getDeclaredMethods();

     Field     getField(String name);

     Field[]     getFields();

     Field     getDeclaredField(String name);

     Field[]     getDeclaredFields();

3.3 Java 8 新增的方法参数反射

------------------------------------------------------------------

Java 8 java.lang.reflect 包下新增了一个 Executable 抽象基类,该对象代表可执行的类成员,该类派生了 Constructor、 Method 两个子类

Parameter 也是 Java 8 新增的 API。

需要指出的是,使用 javac 命令编译 Java 源文件时,默认生成的 class 文件并不包含方法的形参名信息,因此调用 isNamePresent() 方法将返回 false, 调用 getName() 方法也不能得到该参数的形参名。

如果希望javac 命令编译 Java 源文件时保留形参信息,则需要为该命令指定 -parameters 选项。

class Test

{

    public void replace(String str, List<String> list){}

}

public class MethodParameterTest

{

    public static void main(String[] args)throws Exception

    {

        // 获取String的类

        Class<Test> clazz = Test.class;

        // 获取String类的带两个参数的replace()方法

        Method replace = clazz.getMethod("replace"

            , String.class, List.class);

        // 获取指定方法的参数个数

        System.out.println("replace方法参数个数:" + replace.getParameterCount());

        // 获取replace的所有参数信息

        Parameter[] parameters = replace.getParameters();

        int index = 1;

        // 遍历所有参数

        for (Parameter p : parameters)

        {

            if (p.isNamePresent())

            {

                System.out.println("---第" + index++ + "个参数信息---");

                System.out.println("参数名:" + p.getName());

                System.out.println("形参类型:" + p.getType());

                System.out.println("泛型类型:" + p.getParameterizedType());

            }

        }

    }

}

执行:javac -parameters MethodParameterTest.java

*

*

*

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

--------------------------------------------------------------------------------------------------------

Class 对象可以获得该类里的方法(由 Method 对象表示)、构造器(由 Constructor 对象表示)、成员变量(由 Field 对象表示)。

程序可以通过 Method 对象来执行对应的方法,通过 Constructor 对象来调用对应的构造器创建实例,能通过 Field 对象直接访问并修改对象的成员变量值。

4.1 创建对象

---------------------------------------------------------------

通过反射生成对象有如下两种方式

    1. 使用 Class 对象的 newInstance() 方法创建该 Class 对象对应的类的实例,这种方式要求该 Class 对象的对应类有默认构造器,而执行 newInstance() 方法时实际上是利用默认构造器来创建该类的实例。

    2. 先用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建该 Class 对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例。

通过第一种方式来创建对象比较常见,很多 Java EE 框架都需要根据配置文件信息来创建 Java 对象,从配置文件读取的只是某个类的字符串的类名,程序需要根据该字符串来创建对应的实例,就必须用到反射。

例子:

public class ObjectPoolFactory

{

    // 定义一个对象池,前面是对象名,后面是实际对象

    private Map<String ,Object> objectPool = new HashMap<>();

    // 定义一个创建对象的方法,

    // 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象

    private Object createObject(String clazzName)

        throws InstantiationException

        , IllegalAccessException , ClassNotFoundException

    {

        // 根据字符串来获取对应的Class对象

        Class<?> clazz = Class.forName(clazzName);

        // 使用clazz对应类的默认构造器创建实例

        return clazz.newInstance();

    }

    // 该方法根据指定文件来初始化对象池,

    // 它会根据配置文件来创建对象

    public void initPool(String fileName)

        throws InstantiationException

        , IllegalAccessException ,ClassNotFoundException

    {

        try(

            FileInputStream fis = new FileInputStream(fileName))

        {

            Properties props = new Properties();

            props.load(fis);

            for (String name : props.stringPropertyNames())

            {

                // 每取出一对key-value对,就根据value创建一个对象

                // 调用createObject()创建对象,并将对象添加到对象池中

                objectPool.put(name ,

                    createObject(props.getProperty(name)));

            }

        }

        catch (IOException ex)

        {

            System.out.println("读取" + fileName + "异常");

        }

    }

    public Object getObject(String name)

    {

        // 从objectPool中取出指定name对应的对象。

        return objectPool.get(name);

    }

    public static void main(String[] args)

        throws Exception

    {

        ObjectPoolFactory pf = new ObjectPoolFactory();

        pf.initPool("obj.txt");

        System.out.println(pf.getObject("a"));      // ①

        System.out.println(pf.getObject("b"));      // ②

    }

}

如果不想利用默认构造器创建 Java 对象,而想用指定的构造器来创建对象,则需要利用 Constructor 对象,每个 Constructor 对应一个构造器,需要如下三个步骤:

    1. 获取该类的 Class 对象

    2. 利用 Class 对象的 getConstructor() 获取指定的构造器

    3. 调用 Constructor 的 newInstance() 方法创建 Java 对象

例子:

public class CreateJFrame

{

    public static void main(String[] args)

        throws Exception

    {

        // 获取JFrame对应的Class对象

        Class<?> jframeClazz = Class.forName("javax.swing.JFrame");

        // 获取JFrame中带一个字符串参数的构造器

        Constructor ctor = jframeClazz

            .getConstructor(String.class);

        // 调用Constructor的newInstance方法创建对象

        Object obj = ctor.newInstance("测试窗口");

        // 输出JFrame对象

        System.out.println(obj);

    }

}

如果要唯一地确定某个类中的构造器,只要指定构造器的形参列表即可。

当调用 Constructor 对象的 newInstance() 方法时通常需要传入参数,因为调用 Constructor 的 newInstance() 方法实际上等于调用它对应的构造器,传给 newInstance() 方法的参数将作为构造器的参数。

对于上面 CreateJFrame 中已知 java.swing.JFrame 类的情形,通常没有必要使用反射来创建该对象,毕竟通过反射创建对象时性能要稍低些,

实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。

4.2 调用方法

------------------------------------------------------------------------------

当获得某个类对应的 Class 对象后,就可以通过该 Class 对象的 getMethods() 方法或者 getMethod() 方法获取全部方法或指定方法。

每个 Method 对象对应一个方法,获得 Method 对象后,程序就可以通过该 Method() 来调用它对应的方法。

Method 对象包含一个 invoke() 方法,用于调用 Method 对象对应的方法。

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

方法中 obj 是执行该方法的主体对象,后面的 args 是执行该方法时传入该方法的实参

当通过 Method 的 invoke() 方法调用对应的方法时, Java 会要求程序必须有调用该方法的权限,如果程序确实需要调用某个对象的 private 方法,则可以先调用    

void setAccessible(boolean flag);

将 Method 对象的 accessible 设置为指定的布尔值,值为 true, 指示该 Method 在使用时应该取消 Java 语言的访问权限检查,值为 false,则指示 Method 在使用时要实施 Java 语言的访问权限检查

实际上, setAccessible() 方法并不属于 Method, 而是属于它的父类 AccessibleObject 。因此 Method Constructor Field 都可以调用该方法,从而实现通过反射来调用 private 方法、private 构造器、private 成员变量。

它们通过该方法取消访问权限的检查,通过反射即可访问 private 成员。

4.3 访问成员变量的值

---------------------------------------------------------------------------------    

通过 Class 对象的 getFields() 或 getField() 方法可以获取该类所包含的全部成员变量或指定成员变量。

Field 提供了如下两组方法读取或设置成员变量的值

    1. getXxx(Object obj): 获取 obj 对象的该成员变量的值

    2. setXxx(Object obj, Xxx val): 将 obj 对象的该成员变量设置成 val 值

例子:

class Person

{

    private String name;

    private int age;

    public String toString()

    {

        return "Person[name:" + name +

        " , age:" + age + " ]";

    }

}

public class FieldTest

{

    public static void main(String[] args)

        throws Exception

    {

        // 创建一个Person对象

        Person p = new Person();

        // 获取Person类对应的Class对象

        Class<Person> personClazz = Person.class;

        // 获取Person的名为name的成员变量

        // 使用getDeclaredField()方法表明可获取各种访问控制符的成员变量

        Field nameField = personClazz.getDeclaredField("name");

        // 设置通过反射访问该成员变量时取消访问权限检查

        nameField.setAccessible(true);

        // 调用set()方法为p对象的name成员变量设置值

        nameField.set(p , "Yeeku.H.Lee");

        // 获取Person类名为age的成员变量

        Field ageField = personClazz.getDeclaredField("age");

        // 设置通过反射访问该成员变量时取消访问权限检查

        ageField.setAccessible(true);

        // 调用setInt()方法为p对象的age成员变量设置值

        ageField.setInt(p , 30);

        System.out.println(p);

    }

}

4.4 操作数组

-------------------------------------------------------------------------------------------------

在 java.lang.reflect 包下提供了 Array 类,Array 对象可以代表所有数组,程序可以通过使用 Array 来动态创建数组,操作数组元素等

Array 提供三类静态方法:    

    static Object     newInstance(Class<?> componentType, int length)

    static Object     newInstance(Class<?> componentType, int... dimensions)

    static Object     get(Object array, int index)

    static void     set(Object array, int index, Object value)

例子:

public class ArrayTest1

{

    public static void main(String args[])

    {

        try

        {

            // 创建一个元素类型为String ,长度为10的数组

            Object arr = Array.newInstance(String.class, 10);

            // 依次为arr数组中index为5、6的元素赋值

            Array.set(arr, 5, "疯狂Java讲义");

            Array.set(arr, 6, "轻量级Java EE企业应用实战");

            // 依次取出arr数组中index为5、6的元素的值

            Object book1 = Array.get(arr , 5);

            Object book2 = Array.get(arr , 6);

            // 输出arr数组中index为5、6的元素

            System.out.println(book1);

            System.out.println(book2);

        }

        catch (Throwable e)

        {

            System.err.println(e);

        }

    }

}

*

*

*

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

---------------------------------------------------------------------------------------------------------------------------

代理(Proxy) 在《现代汉语词典》里的解释是:受当事人委托,代表他进行某种活动。

应用到面向对象领域,应该是:代理对象代表另一个对象(目标对象, Target ),执行相关活动,也就是说,在实际应用中使用代理对象作为中介,代替目标对象。

在 Java 中,代理对象往往实现和目标对象一致的接口,并作为目标对象的代替,接收对象用户( Client )的调用,并将全部或部分调用转发给目标对象。

在这个过程中,实现代理接口和调用转发,是代理对象必须完成的两个重要任务,缺一不可。

在完成上述两个任务的同时,代理对象可以在调用转发前或者调用转发后执行一些功能,这些功能可以简单,如输出日志,实施访问控制等;也可以非常复杂,包括访问数据库,或者加载远程资源等。

也正是这些在调用前后执行的附加逻辑,体现了代理对象的价值。

Java 的 java.lang.reflect 包提供了一个 Proxy 类和一个 Interface InvocationHandler, 通过使用这个类和接口可以生成 JDK 动态代理类或动态代理对象。

5.1 使用 Proxy 和 InvocationHandler 创建动态代理

---------------------------------------------------------

Proxy 提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。

如果在程序中为一个或多个接口动态地生成实现类,就可以使用 Proxy 来创建动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用 Proxy 来创建动态代理实例。

Proxy 提供如下两个方法来创建动态代理类和动态代理实例:

    static Class<?>     getProxyClass(ClassLoader loader, Class<?>... interfaces):创建一个动态代理类所对应的 Class 对象,该代理类将实现 interfaces 所指定的所有接口。

    static Object         newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了 interfaces 指定的一系列接口,执行代理对象的每个方法时都会被替换执行 InvocationHandler 对象的 invoke() 方法

当程序使用反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口。动态代理对象就需要实现一个或多个接口里定义的所有方法,

但问题是:系统怎么知道如何实现这些方法? 这个时候就轮到 InvocationHandler 对象登场了------当执行动态代理对象里的方法时,实际上会替换成调用 InvocationHandler 对象的 invoke() 方法。

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

    //To create a proxy for some interface Foo:

    //创建一个 InvocationHandler 对象

    InvocationHandler handler = new MyInvocationHandler(...);

    //使用 Proxy 生成动态代理类 proxyClass

    Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);

    //获取 proxyClass 类中带有一个 InvocationHandler 参数的构造器,调用构造器的 newInstance() 方法创建动态实例

    Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).

                     newInstance(handler);

    ----------------------------------------------------------------------------------

    上面的代码也可简化成如下代码:

    //创建一个 InvocationHandler 对象

    InvocationHandler handler = new MyInvocationHandler(...);

    //使用 Proxy 直接生成一个动态代理对象

    Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),

                                          new Class<?>[] { Foo.class },

                                          handler);

    ----------------------------------------------------------------------------------

例子:

interface Person

{

    void walk();

    void sayHello(String name);

}

class MyInvokationHandler implements InvocationHandler

{

    public Object invoke(Object proxy, Method method, Object[] args)

    {

        System.out.println("----正在执行的方法:" + method);

        if (args != null)

        {

            System.out.println("下面是执行该方法时传入的实参为:");

            for (Object val : args)

            {

                System.out.println(val);

            }

        }

        else

        {

            System.out.println("调用该方法没有实参!");

        }

        return null;

    }

}

public class ProxyTest

{

    public static void main(String[] args)

        throws Exception

    {

        // 创建一个InvocationHandler对象

        InvocationHandler handler = new MyInvokationHandler();

        // 使用指定的InvocationHandler来生成一个动态代理对象

        Person p = (Person)Proxy.newProxyInstance(Person.class.getClassLoader()

            , new Class[]{Person.class}, handler);

        // 调用动态代理对象的walk()和sayHello()方法

        p.walk();

        p.sayHello("孙悟空");

    }

}

public interface InvocationHandler 接口: 定义该实现类需要实现 invoke() 方法, 调用代理对象是所有方法都会被替换成调用 invoke() 方法。

    Object invoke(Object proxy, //代表动态代理对象

              Method method,    //代表正在执行的方法

              Object[] args)    //代表调用目标方法时传入的实参。

       throws Throwable

    Returns:

    the value to return from the method invocation on the proxy instance. If the declared return type of the interface method is a primitive type,

    then the value returned by this method must be an instance of the corresponding primitive wrapper class; otherwise, it must be a type assignable to the declared return type.

    If the value returned by this method is null and the interface method's return type is primitive, then a NullPointerException will be thrown by the method invocation on the proxy instance.

    If the value returned by this method is otherwise not compatible with the interface method's declared return type as described above, a ClassCastException will be thrown by the method invocation on the proxy instance.

JDK 动态代理只能为接口创建动态代理    

实际上,在普通编程过程中,确实无须使用动态代理,但在编写框架或底层基础代码时,动态代理的作用非常大。

5.2 动态代理和 AOP

----------------------------------------------------------------------------

*

*

*

6 反射和泛型

-------------------------------------------------------------------------------------------------------------------------