17.反射(reflection)【Java温故系列】

参考自–《Java核心技术卷1》

Java 的反射库提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中,它是 Java 组件的体系结构。

能够分析类能力的程序称为反射。反射机制的功能极其强大,它可以用来:

  • 在运行时分析类
  • 在运行时查看对象
  • 实现通用的数组操作代码
  • 利用 Method 对象

反射是一种功能强大且复杂的机制。


1 Class 类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个标识信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息找到相应的方法并执行下去。

可以通过专门的 Java 类访问这些信息(运行时类型),保存这些信息的类被称为 Class。

获取 Class 类对象的方法:

1)Object 类中的 getClass() 方法将会返回一个 Class 类型的实例。

Employee e = new Employee();   //创建一个雇员对象
Manager m = new Manager();    //创建一个经理对象

Class c1 = e.getClass();
Class c2 = m.getClass();

如同用一个 Employee 对象表示一个特定的雇员属性一样,一个 Class 对象将表示一个特定类的属性。最常用的 Class 方法是 getName。这个方法将返回类的名字,例如:

System.out.println(e.getClass().getName()); //输出 class cn.cbq.study.sample01.Employee
System.out.println(m.getClass().getName()); //输出 class cn.cbq.study.sample01.Manager

如果类在一个包里,包的名字也作为类名的一部分输出。

2)还可以调用静态方法 forName 获取类名对应的 Class 对象:

String className = "java.util.Random";
try {
	Class c = Class.forName(className);
	System.out.println(c.getName());   //输出 java.util.Random
} catch (ClassNotFoundException ex) {
	ex.printStackTrace();
}     

如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法。当然,这个方法只有在字符串为类名或接口名时才能够执行,否则,forName 方法将抛出异常(无论何时使用这个方法,都应该提供一个异常处理器)。

:在启动 Java 程序时,包含 main 方法的类被加载,它又会加载所有它需要的类。这些被加载的类又要加载它们各自需要的类,以次类推。对于一个大型的应用程序来说,这将消耗很多时间。可以使用这样一个技巧,给人一种启动速度比较快的幻觉:在 main 方法中通过调用 Class.forName 手工地加载其他的类,但需确保包含 main 方法的类没有显式地引用其他的类。

3)如果 T 是任意的 Java 类型(或 void 关键字),T.class 将代表匹配的类对象:

Class c1 = Random.class;  //要先 import java.util.*;否则 c1 = java.util.Random.class;
Class c2 = int.class;   // 输出 int
Class c3 = Double[].class;   //输出 class [Ljava.lang.Double;

注意,一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int 不是类,但 int.class 是一个 Class 类型的对象。

:Class 类实际上是一个泛型类。例如,Employee.class 的类型是 Class<Employee>。它将抽象的概念更加复杂化了,在大多数实际问题中,可以忽略类型参数,而使用原始的 Class 类。

getName 方法在应用于数组类型的时候会返回比较奇怪的名字:

  • Double[].class.getName() 返回 “[Ljava.lang.Double”
  • int[].class.getName() 返回 “[I”

虚拟机为每个类型管理一个 Class 对象。可以使用 == 运算符实现两个类对象比较的操作:

if(e.getClass()==Employee.class)  ...

还有一个很有用的方法 newInstance() ,可以用来动态地创建一个类的实例:

Employee e = new Employee();
try {
	Employee es = e.getClass().newInstance();   //创建了一个新的Employee实例 es
} catch (InstantiationException ex) {
	ex.printStackTrace();
} catch (IllegalAccessException ex) {
	ex.printStackTrace();
}

创建了一个与 e 具有相同类类型的实例。 newInstance() 方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。若这个类没有默认的构造器(有其他带参数的构造器),就会抛出一个异常。

forNamenewInstance() 配合起来使用,就可以根据存储在字符串中的类名创建一个对象:

String className = "java.util.Random";
try {
	Object obj = Class.forName(s).newInstance();  //创建一个Random类对象
	Random ran = (Random)obj;
    int i = ran.nextInt();   //使用 Random 对象生成一个随机 int 类型数值
} catch (InstantiationException ex) {
	ex.printStackTrace();
} catch (IllegalAccessException ex) {
	ex.printStackTrace();
} catch (ClassNotFoundException ex) {
	ex.printStackTrace();
}

:如果需要以这种方式向希望按名称创建的类的构造器提供参数(调用带参数的构造函数),就需要使用 Constructor 类中的 newInstance() 方法。


2 利用反射分析类的能力(检查类的结构)

在 java.lang.reflect 包中有三个类 Field,Method 和 Constructor 分别用于描述类的域、方法和构造器。

这三个类都有一个叫 getName 的方法,用来返回项目的名称。Field 类有一个 getType 方法,用来返回描述域所属类型的 Class 对象。Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。这三个类还有一个叫 getModifies 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外,还可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析 getModifies 返回的整型数值。例如,可以使用 Modifier 类中的 isPublicisPrivateisFinal 判断方法或构造器是否是 public、private 或 final。另外还可以利用 Modifier.toString 方法将修饰符打印出来。

Class 类中的 getFieldsgetMethodsgetConstructors 方法将分别返回类提供的 public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclaredFieldsgetDeclaredMethodsgetDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

下面的案例通过输入 Java 类 分析 域,方法和构造器:

import java.util.*;
import java.lang.reflect.*;

public class ReflectionTest {

   public static void main(String[] args) {
        // read class name from command line args or user input
        String name;
        if (args.length > 0) {
           name = args[0];
        }else {
            Scanner in = new Scanner(System.in);
            System.out.println("Enter class name (例如:java.util.Date): ");
            name = in.next();
        }

        try {
            // print class name and superclass name (if != Object)
            Class cl = Class.forName(name);
            Class supercl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers + " ");
            System.out.print("class " + name);
            if (supercl != null && supercl != Object.class) 
                System.out.print(" extends " + supercl.getName());
            System.out.print("\n{\n");
            printConstructors(cl);
            System.out.println();
            printMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }

    /**
     * Prints all constructors of a class
     * @param cl a class
     */
    public static void printConstructors(Class cl) {
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Constructor c : constructors) {
            String name = c.getName();
            System.out.print("   ");
            String modifiers = Modifier.toString(c.getModifiers());
            if (modifiers.length() > 0) 
                System.out.print(modifiers + " ");
            System.out.print(name + "(");

            // print parameter types
            Class[] paramTypes = c.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) 
                    System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * Prints all methods of a class
     * @param cl a class
     */
    public static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();

        for (Method m : methods) {
            Class retType = m.getReturnType();
            String name = m.getName();

            System.out.print("   ");
            // print modifiers, return type and method name
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) 
                System.out.print(modifiers + " ");
            System.out.print(retType.getName() + " " + name + "(");

            // print parameter types
            Class[] paramTypes = m.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) 
                    System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * Prints all fields of a class
     * @param cl a class
     */
    public static void printFields(Class cl) {
        Field[] fields = cl.getDeclaredFields();

        for (Field f : fields) {
            Class type = f.getType();
            String name = f.getName();
            System.out.print("   ");
            String modifiers = Modifier.toString(f.getModifiers());
            if (modifiers.length() > 0) 
                System.out.print(modifiers + " ");
            System.out.println(type.getName() + " " + name + ";");
        }
    }
}

相关的API:

1)Class 类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUCRWdie-1590042929698)(C:\Users\86136\AppData\Roaming\Typora\typora-user-images\image-20200502155909356.png)]

2)Field、Method 和 Constructor 类:

在这里插入图片描述

3)Modifier 类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBhPpXJn-1590042929703)(C:\Users\86136\AppData\Roaming\Typora\typora-user-images\image-20200502160042760.png)]


3 在运行时使用反射分析对象

通过上面的内容,已经知道了如何查看任意对象的数据域名称和类型:

1.获取对应的 Class 对象

2.通过 Class 对象调用 getDeclaredFields

那么数据域的实际内容又怎么查看呢?

在编写程序时,如果知道想要查看的域名和类型,查看指定的域是一件很容易的事。利用反射机制可以查看在编译时还不清楚的对象域。

查看对象域的关键方法是 Field 类中的 get 方法。如果 f 是一个 Field 类型的对象,obj 是某个包含 f 域的类的对象,f.get(obj) 将返回一个对象,其值为 obj 域的当前值。如下:

//Employee
public class Employee{
	private String name;
    private double salary;
    ...
}
Employee e = new Employee("zs",1000);
Class c = e.getClass();
try {
	Field f = c.getDeclaredField("name");  //获取 e 对象的 name 域
	Object obj = f.get(e);
	System.out.println(obj);
} catch (NoSuchFieldException | IllegalAccessException ex) {
	ex.printStackTrace();
}
//若 name 是公有域,则成功获取  zs

实际上,这段代码存在一个问题:由于 name 域是一个私有域,所以 get 方法将会抛出异常。只有利用get 方法才能得到可访问域的值。 除非拥有访问权限,否则 Java 安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。

反射机制默认行为受限于 Java 的访问控制。然而,如果一个 Java 程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用 Field、Method 或 Constructor 对象的 setAccessible 方法:

f.setAccessible(true);
f.get(e);   //即使 name 域是私有域,也可以获得 name 域的值

setAccessible 方法是 AccessibleObject 类中的一个方法,它是 Field、Method 或 Constructor 类的公共超类。

get 方法还有一个需要解决的问题:name 域是一个 String ,因此把它作为 Object 返回没有什么问题;但是,若要查看 salary 域,它属于 double 类型,而 Java 的数值域不是对象,就无法直接查看(抛出异常)。

要想解决这个问题,可以使用 Field 类中的 getDouble 方法,此时,反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成 Double。

当然,可以获得就可以设置。调用 f.set(obj,value) 将 obj 对象的 f 域设置成新值 value。

 f.set(e,"lisa");  //将上述的e对象的name域设置为"lisa"

在这里插入图片描述

4 使用反射编写泛型数组

java.lang.reflect 包中的 Array 类允许动态地创建数组。例如 Array 类的 copyOf 方法:

Employee[] e = new Employee[100];
...
//当数组e已经被填满后
e = Arrays.copyOf(a,2*a.length);

显然,copyOf 方法是一个通用的方法,那么它返回的数组必须为 Object 数组,那么它是如何实现的呢?

首先,我们知道,Employee[] 可以临时地转换为 Object[] 数组,然后将它转换回来也是可以的;但一个一开始就是 Object[] 的数组却无法转换成 Employee[] 数组(包括其他类型的数组)。为了编写通用的 copyOf 函数,就需要能够创建与原数组类型相同的新数组。

为此,就需要 java.lang.reflect 包中的 Array 类的一些方法。其中最关键的是 Array 类中的静态方法 newInstance ,它能构造新数组。在调用它是必须提供两个参数:原数组的元素类型,新数组的长度。

Object newArray = Array.newInstance(componentType,newLength);

可以通过 Array.getLength(obj) 获得当前数组 obj 的长度;而要获取原数组的元素类型以设置新数组的元素类型,就需要进行以下工作:

  • 首先获取原数组的类对象
  • 确认它是一个数组
  • 使用 Class 类(只能定义表示数组的类对象)的 getComponentType 方法确定数组对应的类型。

copyOf 方法的实现如下:

public static Object copyOf(Object obj,int newlength){
	Class c1 = obj.getClass();
    if(!c1.isArray())
        return null;
    Class componentType = c1.getComponentType();
    int length = Array.getLength(obj);
    Object newArray = Array.newInstance(componentType,newLength);
    //public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);原数组,原数组起始位置,目标数组,目标数组开始位置,要copy的数组(原数组的)长度
    System.arraycopy(obj,0,newArray,0,Math.min(length,newLength));
    return newArray;
}

copyOf 方法可以用来扩展任意类型的数组,而不仅仅是对象数组:

int[] a = {1,2,3,4,5};
a = (int[])copyOf(a,10);

整型数组类型 int[] 可以被转换成 Object,但不能转换成对象数组。

在这里插入图片描述


5 调用任意方法

反射机制允许 Java 程序员调用任意方法。

在 Method 类中有一个 invoke 方法,它允许调用包装在当前 Method 对象中的方法,invoke 方法的签名是:

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

第一个参数是隐式参数,其余参数是对象提供的显式参数。

对于静态方法,第一个参数可以被省略,即可以将它设置为 null。

例如,假设 m1 代表 Employee 类的 getName 方法,下面的语句显示了如何调用这个方法:

Employee e = new Employee("zs",1000);
Method m1 = null;
Method m2 = null;
try {
	m1 = Employee.class.getMethod("getName");
    m2 = Employee.class.getMethod("getSalary");
	String name = (String) m1.invoke(e);
    double s = (Double)m2.invoke(e);
	System.out.println(name);
} catch (Exception ex) {
	ex.printStackTrace();
}

如果返回类型是基本类型,invoke 方法会返回其包装器类型。例如,假设有 m2 表示 Employee 的 getSalary 方法,那么实际返回的对象实际上是一个 Double ,必须相应地完成类型转换可以使用自动拆箱将它转换为一个 double.

getMethod 方法的签名是:

Method getMethod(String name,Class... parameterTypes)

parameterTypes 是传入参数的类型。

调用静态方法时:

Method m = null;
try {
	m = Math.class.getMethod("sqrt",double.class);
	double x = (Double)m.invoke(null,4);
	System.out.println(x);   //输出 2.0
} catch (Exception ex) {
	ex.printStackTrace();
}

invoke 的参数和返回值都必须为 Object 类型的,这意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码错误的机会,从而只有在测试阶段才能发现这些错误。不但如此,使用反射获得方法指针的代码要比直接调用方法明显慢一些。

hod(String name,Class... parameterTypes)

parameterTypes 是传入参数的类型。

调用静态方法时:

Method m = null;
try {
	m = Math.class.getMethod("sqrt",double.class);
	double x = (Double)m.invoke(null,4);
	System.out.println(x);   //输出 2.0
} catch (Exception ex) {
	ex.printStackTrace();
}

invoke 的参数和返回值都必须为 Object 类型的,这意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码错误的机会,从而只有在测试阶段才能发现这些错误。不但如此,使用反射获得方法指针的代码要比直接调用方法明显慢一些。

建议仅在有必要的时候才使用 Method 对象,最好使用接口以及 lambda 表达式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值