Java反射入门

Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
用一句话总结就是反射可以实现在运行时可以知道任意一个类的属性和方法。

1 理解Class类和类类型

想要了解反射首先理解一下Class类,它是反射实现的基础。

1.1 类是对象吗?

思考一个问题:

在面向对象的世界里,万事万物皆是对象,而对象是类的一个实例,那么类是一个对象吗?

答案是肯定的,类是java.lang.Class类的实例对象,而Class是所有类的类(There is a class named Class)

1.2 如何表示Class的对象?

对于普通的对象,我们一般都会这样创建和表示:

Code code1 = new Code(); 

上面说了,所有的类都是Class的对象,那么如何表示呢,可不可以通过如下方式呢:

Class c = new Class();

但是我们查看Class的源码时,是这样写的:

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

可以看到构造器是私有的,只有JVM可以创建Class的对象,因此不可以像普通类一样new一个Class对象,虽然我们不能new一个Class对象,但是却可以通过已有的类得到一个Class对象,共有三种方式,如下:

  1. Class c1 = Code.class;
    这说明任何一个类都有一个隐含的静态成员变量class,这种方式是通过获取类的静态成员变量class得到的
  2. Class c2 = code1.getClass();
    code1是Code的一个对象,这种方式是通过一个类的对象的getClass()方法获得的
  3. Class c3 = Class.forName("com.trigl.reflect.Code");
    这种方法是Class类调用forName方法,通过一个类的全量限定名获得

这里,c1、c2、c3都是Class的对象,他们是完全一样的,而且有个学名,叫做Code的类类型(class type)。
这里就让人奇怪了,前面不是说Code是Class的对象吗,而c1、c2、c3也是Class的对象,那么Code和c1、c2、c3不就一样了吗?为什么还叫Code什么类类型?这里不要纠结于它们是否相同,只要理解类类型是干什么的就好了,顾名思义,类类型就是类的类型,也就是描述一个类是什么,都有哪些东西,所以我们可以通过类类型知道一个类的属性和方法,并且可以调用一个类的属性和方法,这就是反射的基础。

类类型是反射的基础!
类类型是反射的基础!
类类型是反射的基础!

加粗+斜体+说三遍,这次应该记住了吧!

我们创建一个对象是通过new关键字的,但是知道了类类型以后,可以通过类类型的newInstance()方法创建某个类的对象实例以及调用方法,如下:

Code code = (Code)c1.newInstance(); // 需要有无参的构造方法
code.print();

那么问题又来了,通过类类型创建类和通过new创建类有什么不同呢?事实上类类型创建类是动态加载类,下面讲一下什么是动态加载类。

2 动态加载类

程序执行分为编译器和运行期,编译时刻加载一个类就称为静态加载类,运行时刻加载类称为动态加载类,下面通过一个实例来讲解:

现在抛开IDE工具,用记事本手写类,这是为了方便我们利用cmd命令行手动编译和运行一个类,从而更好理解动态加载类和静态加载类的区别。

首先写Office.java

class Office
{
    public static void main(String[] args)
    {
        if ("Word".equals(args[0]))
        {   
            // 静态加载类,在编译时加载
            Word w = new Word();
            w.start();
        }
        if ("Excel".equals(args[0]))
        {
            Excel e = new Excel();
            e.start();
        }
    }
}

然后进入cmd编译Office.java,如图:

这里写图片描述

由于我们new的两个类Word和Excel没有编译,所以报错了,这就是静态加载类的缺点,即必须在编译时期就加载所有可能用到的类,而我们希望实现的是运行时用到哪个类就加载哪个类,下面通过动态加载类来加以改进。

改进以后的类:OfficeBetter.java

class OfficeBetter
{   
    public static void main(String[] args)
    {
            try
        {
            // 动态加载类,在运行时加载
            Class c = Class.forName(args[0]);
            // 通过类类型,创建该类对象
            OfficeAble oa = (OfficeAble)c.newInstance();
            oa.start();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }   
}

这里动态加载了名为args[0]的类,而args[0]是在运行期输入给main方法的第一个参数,如果你输入Word那么就会加载Word.java,这时候就需要在与OfficeBetter.java相同路径下面创建Word.java;同理,如果你输入Excel就需要加载Excel.java了。
其中OfficeAble是一个接口,上面动态加载的类如Word、Excel就是实现了OfficeAble,体现了多态的思想,这种动态加载和多态的思想可以使具体功能和代码解耦,也就是随时想添加某个功能(如Word和Excel都不要了,我要PPT)都能动态添加,而不改动原来的代码。

其中OfficeAble接口如下:

interface OfficeAble
{
    public void start();
}

Word类:

class Word implements OfficeAble
{
    public void start()
    {
        System.out.println("word...starts...");
    }
}

按顺序编译、运行上面的类:

这里写图片描述

3 获取类的信息

一个类都有哪些东西呢?答案非常简单:属性和方法,这一节我们就学习如何通过类类型得到类的基本信息。

3.1 获取类的成员方法信息

首先想一想成员方法中都包括什么:返回值类型+方法名+参数类型
在Java中,类的成员方法也是一个对象,它是java.lang.reflect.Method的一个对象,所以我们通过java.lang.reflect.Method里面封装的方法来获取这些信息.

3.1.1 单独获取某一个方法

获取方法
单独获取某一个方法是通过Class类的以下方法获得的:

  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 得到该类所有的方法,不包括父类的
  • public Method getMethod(String name, Class<?>... parameterTypes) // 得到该类所有的public方法,包括父类的

两个参数分别是方法名和方法参数类的类类型列表。
例如类A有如下一个方法:

public void print(String a, int b) {
    // code body
}

现在知道A有一个对象a,那么就可以通过:

Class c = a.getClass();
Method method = c.getDeclaredMethod("print", String.class, int.class);

来获取这个方法。

如何调用获取到的方法
那得到方法以后如何调用这个方法呢,也就是像调用普通对象方法那样实现方法中的代码呢?通过Method类的以下方法实现:

public Object invoke(Object obj, Object… args)

两个参数分别是这个方法所属的对象和这个方法需要的参数,还是用上面的例子来说明,通过:

method.invoke(a, "hello", 10);

和通过普通调用:

a.print("hello", 10);

效果完全一样,这就是方法的反射,invoke()方法可以反过来将其对象作为参数来调用方法,完全跟正常情况反了过来。

3.1.2 获取类中所有成员方法的信息

如果想要获得类中所有而非单独某个成员方法的信息,可以通过以下几步来实现:

  1. 已知一个对象,获取其类的类类型

    Class c = obj.getClass();
  2. 获取该类的所有方法,放在一个数组中

    Method[] methods = c.getDeclaredMethods();
  3. 遍历方法数组,获得某个方法method

    for (Method method : methods)
  4. 得到方法返回值类型的类类型

    Class returnType = method.getReturnType();
  5. 得到方法返回值类型的名称

    String returnTypeName = returnType.getName();
  6. 得到方法的名称

    String methodName = method.getName();
  7. 得到所有参数类型的类类型数组

    Class[] paramTypes = method.getParameterTypes();
  8. 遍历参数类的类类型数组,得到某个参数的类类型class1

    for (Class class1 : paramTypes)
  9. 得到该参数的类型名

    String paramName = class1.getName();

3.2 获取类的成员变量信息

想一想成员变量中都包括什么:成员变量类型+成员变量名
类的成员变量也是一个对象,它是java.lang.reflect.Field的一个对象,所以我们通过java.lang.reflect.Field里面封装的方法来获取这些信息。

3.2.1 单独获取某个成员变量

通过Class类的以下方法实现:

  • public Field getDeclaredField(String name) // 获得该类自身声明的所有变量,不包括其父类的变量
  • public Field getField(String name) // 获得该类自所有的public成员变量,包括其父类变量

参数是成员变量的名字。
例如一个类A有如下成员变量:

private int n;

如果A有一个对象a,那么就可以这样得到其成员变量:

Class c = a.getClass();
Field field = c.getDeclaredField("n");

3.2.2 获取所有的成员变量

同样,如果想要获取所有成员变量的信息,可以通过以下几步:

  1. 已知一个对象,获取其类的类类型

    Class c = obj.getClass();
  2. 获取该类的所有成员变量,放在一个数组中

    Field[] fields = c.getDeclaredFields(); 
  3. 遍历变量数组,获得某个成员变量field

    for (Field field : fields)
  4. 得到成员变量类型的类类型

    Class fieldType = field.getType();
  5. 得到成员变量的类型名

    String typeName = fieldType.getName();
  6. 得到成员变量的名称

    String fieldName = field.getName();

3.3 获取类的构造函数

最后再想一想构造函数中都包括什么:构造函数参数
同上,类的成构造函数也是一个对象,它是java.lang.reflect.Constructor的一个对象,所以我们通过java.lang.reflect.Constructor里面封装的方法来获取这些信息。

3.3.1 单独获取某个构造函数

通过Class类的以下方法实现:

  • public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) // 获得该类所以public构造器,包括父类
  • public Constructor<T> getConstructor(Class<?>... parameterTypes) // 获得该类所以public构造器,包括父类

这个参数为构造函数参数类的类类型列表。
例如类A有如下一个构造函数:

public A(String a, int b) {
    // code body
}

那么就可以通过:

Constructor constructor = a.getDeclaredConstructor(String.class, int.class);

来获取这个构造函数。

3.3.2 获取所有的构造函数

可以通过以下步骤实现:

  1. 已知一个对象,获取其类的类类型

    Class c = obj.getClass();
  2. 获取该类的所有构造函数,放在一个数组中

    Constructor[] constructors = c.getDeclaredConstructors();
  3. 遍历构造函数数组,获得某个构造函数constructor

    for (Constructor constructor : constructors)
  4. 得到构造函数参数类型的类类型数组

    Class[] paramTypes = constructor.getParameterTypes();
  5. 遍历参数类的类类型数组,得到某个参数的类类型class1

    for (Class class1 : paramTypes)
  6. 得到该参数的类型名

    String paramName = class1.getName();

4 通过反射了解集合泛型的本质

首先下结论:

Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译到了运行期就无效了。

下面通过一个实例来验证:

package com.trigl.reflect;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * 集合泛型的本质
 * @description
 * @author Trigl
 * @date 2016年4月2日上午2:54:11
 */
public class GenericEssence {
    public static void main(String[] args) {
        List list1 = new ArrayList(); // 没有泛型 
        List<String> list2 = new ArrayList<String>(); // 有泛型


        /*
         * 1.首先观察正常添加元素方式,在编译器检查泛型,
         * 这个时候如果list2添加int类型会报错
         */
        list2.add("hello");
//      list2.add(20); // 报错!list2有泛型限制,只能添加String,添加int报错
        System.out.println("list2的长度是:" + list2.size()); // 此时list2长度为1


        /*
         * 2.然后通过反射添加元素方式,在运行期动态加载类,首先得到list1和list2
         * 的类类型相同,然后再通过方法反射绕过编译器来调用add方法,看能否插入int
         * 型的元素
         */
        Class c1 = list1.getClass();
        Class c2 = list2.getClass();
        System.out.println(c1 == c2); // 结果:true,说明类类型完全相同

        // 验证:我们可以通过方法的反射来给list2添加元素,这样可以绕过编译检查
        try {
            Method m = c2.getMethod("add", Object.class); // 通过方法反射得到add方法
            m.invoke(list2, 20); // 给list2添加一个int型的,上面显示在编译器是会报错的
            System.out.println("list2的长度是:" + list2.size()); // 结果:2,说明list2长度增加了,并没有泛型检查
        } catch (Exception e) {
            e.printStackTrace();
        }

        /*
         * 综上可以看出,在编译器的时候,泛型会限制集合内元素类型保持一致,但是编译器结束进入
         * 运行期以后,泛型就不再起作用了,即使是不同类型的元素也可以插入集合。
         */
    }
}   

输出结果

list2的长度是:1
true
list2的长度是:2

OVER

  • 4
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
评论

打赏作者

Trigl

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值