Java学习笔记:反射

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

  • 在运行时分析类的能力。
  • 在运行时查看对象, 例如, 编写一个toString 方法供所有类使用。
  • 实现通用的数组操作代码。
  • 利用Method 对象, 这个对象很像C/C++中的函数指针。
1.Class类

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

    然而, 可以通过专门的Java 类访问这些信息。保存这些信息的类被称为Class, 这个名字很容易让人混淆。

得到Class对象的三个方法:

  • Object 类中的getClass( ) 方法将会返回一个Class 类型的实例。
/* Employee是已经创建好的一个类*/
Employee e;
...
Class cl = e.getClass();
System.out.println(e.getClass.getName());

  • 还可以调用静态方法forName获得类名对应的Class对象。如果类名保存在字符串中并且可以在运行中改变,就可以使用这个方法。当然,这个方法只有在className是类名或接口名时才能够执行,否则forName将会抛出一个checked exception(已检查异常)。无论何时使用这个方法都应该提供一个异常处理器。
  • 第三种方法很简单。如果T是任意的Java类型或void关键字,T.class将代表匹配的类对象。例如
Class cl1=Random.class; //如果import了java.util.*
Class cl2=int.Class;
Class cl3=Double.class

虚拟机为每个类型管理一个Class对象。请注意, 一个Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。例如,int 不是类, 但int.class 是一个Class 类型的对象。如果类在一个包里,包的名字也作为类名的一部分。

2.利用反射分析类的能力

下面简要地介绍一下反射机制最重要的内容——检查类的结构。

在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Field类有一个getType方法,用来返回域所属类型的Class对象。Method 和Constructor 类有能够报告参数类型的方法, Method 类还有一个可以报告返回类型的方法。这<个类还有一个叫做getModifiers 的方法, 它将返回一个整型数值, 用不同的位开关描述public 和static 这样的修饰符使用状况。另外, 还可以利用java.lang.refleCt 包中的Modifiei•类的静态方法分析getModifiers 返回的整型数值。例如, 可以使用Modifier 类中的isPublic、isPrivate 或isFinal判断方法或构造器是否是public、private 或final。我们需要做的全部工作就是调用Modifier类的相应方法, 并对返回的整型数值进行分析, 另外, 还可以利用Modifier.toString 方法将修饰符打印出来。

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

程序:显示一个类的全部信息(包括类中所有的方法和构造器的签名以及全部的域名,以java.lang.Double为例)

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

public class Main {

    public static void main(String[] args) {
        String classname = "java.lang.Double";//要分析的类
        try
        {
            //输出类名和超类名
            Class cl = Class.forName(classname);
            Class supercl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers + " ");
            System.out.println("class" + classname);
            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);
    }
    /**
     * 输出一个类的全部构造器
     * */
    public static void printConstructors(Class cl)
    {
        Constructor[] constructors = cl.getConstructors();
        for (Constructor c : constructors)
        {
            String name = c.getName();
            System.out.println("  ");
            String modifiers = Modifier.toString(c.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers+" ");
            System.out.print(name+"(");
            //打印参数类型
            Class[] paraTypes = c.getParameterTypes();
            for (int j = 0; j < paraTypes.length; j++)
            {
                if (j > 0) System.out.print(", ");
                System.out.print(paraTypes[j].getName());
            }
            System.out.println(");");
        }
    }
    /**
     * 输出一个类的所有方法
     */
    public static void printMethods(Class cl)
    {
        Method[] methods = cl.getDeclaredMethods();
        for (Method m:methods)
        {
            Class retType = m.getReturnType();
            String name = m.getName();
            System.out.println("  ");
            // 输出modifiers、返回类型和方法名
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers+" ");
            System.out.print(retType.getName()+" "+name+"(");
            // 输出参数类型
            Class[] paraTypes= m.getParameterTypes();
            for (int j = 0; j < paraTypes.length; j++)
            {
                if (j>0) System.out.print(", ");
                System.out.print(paraTypes[j].getName());
            }
            System.out.println(");");
        }
    }
    /**
     * 输出一个类的所有域
     */
    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+" ");
        }
    }
}
3.在运行时使用反射分析对象

前面一节中我们已经知道了如何查看任意对象的数据域名称和类型:

  • 获得对应的Class对象。
  • 通过Class对象调用getDeclaredFields。

本节将进一步查看数据域的实际内容。当然,在编写程序时,如果知道想要查看的域名和类型,查看指定的域是一件很容易的事,而利用反射机制可以查看在编译时还不清楚的对象域。

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

     Employee john = new Employee("John Porter",12345,2015);
     Class cl = john.getClass();
     Field f = cl.getDeclaredField("name");
     Object v = f.get(john);  //john这个对象的name域的值,即"John Porter"。
     System.out.println(v); 

如此会输出john这个对象的name域的值,即“John Porter”。实际上我们运行这段代码的时候会发现是有问题的。

  • 第一个问题是,name是一个私有(private)域,所以get方法将会抛出一个IllegalAccessException。只有利用get方法才能得到可访问域的值。除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。
    反射机制的默认行为受限于Java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用Field、Method或Constructor对象的setAccessible方法。例如
    f.setAccessible(true); //现在就可以正常调用Object v = f.get(john)了。
    setAccessible是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为了调试、持久存储和相似机制而提供的。稍后将利用它编写一个通用的toString方法。
  • 还有一个问题。name是一个String域,因此把它作为Object返回没有什么问题。但是假定我们想要查看salary域,它是一个double类型,而Java中数值类型并不是对象。要想解决这个问题,可以使用Field域中的getDouble方法;也可以调用get方法,此时,反射机制将会自动将这个域值打包到相应的对象包装器中,这里将打包成Double。

既然可以获得域值,就可以设置。调用f.set(obj,value)就可以将obj对象的f域设置成value值。简单的小试验:

import java.lang.reflect.*;
import java.lang.IllegalAccessException;
public class Main {

    public static void main(String[] args) {
        try
        {
            Employee john = new Employee("John Porter",12345,2015);
            Class cl = john.getClass();
            Field f = cl.getDeclaredField("name");
            f.setAccessible(true);
            Object v = f.get(john);
            System.out.println(v);
            f.set(john,"John Poter");
            v = f.get(john);
            System.out.println(v);
        }
        catch (NoSuchFieldException | IllegalAccessException e){e.printStackTrace();}
    }
}
class Employee{
    private String name;
    private int salary;
    private int work_year;
    public Employee(String name,int salary,int work_year){
        this.name = name;
        this.salary = salary;
        this.work_year = work_year;
    }
    //....
}
4.使用反射可以编写泛型数组代码,如扩展已填满的泛型数组

java.lang.reflect 包中的Array 类允许动态地创建数组。例如, 将这个特性应用到Array类中的copyOf方法实现中, 应该记得这个方法可以用于扩展已经填满的数组。

Employee[] a = new Employee[100]:
//数组满了
a = Arrays.copyOf(a, 2 * a.length);
如何编写这样一个通用的方法呢? 正好能够将Employee[ ] 数组转换为Object[ ] 数组。下面是一个错误的尝试:
        public static Object[] badCopyOf(Object[] a , int newlength){
            Object[] newArray = new Object[newlength ];
            System.arraycopy(a,0,newArray,Math.min(a.length,newlength));
            return;newArray;
        }

在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组( Object[ ]) 类型,这是由于使用下面这行代码创建的数组: new Object[newlength]        。

一个对象数组不能转换成雇员数组( Employee[ ] )。如果这样做, 则在运行时Java 将会产生ClassCastException 异常。前面已经看到,Java 数组会记住每个元素的类型, 即创建数组时new 表达式中使用的元素类型。将一个Employee[ ] 临时地转换成Object[ ] 数组, 然后再把它转换回来是可以的,但一从开始就是Objectt ] 的数组却永远不能转换成Employe ] 数组。为了编写这类通用的数组代码, 需要能够创建与原数组类型相同的新数组。为此, 需要java,lang.reflect 包中Array 类的一些方法。其中最关键的是Array 类中的静态方法newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。

为了能够实际地运行,需要获得新数组的长度和元素类型。

可以通过调用Array.getLength(a) 获得数组的长度, 也可以通过Array 类的静态getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作:

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

下面是这个逻辑的代码:

    public static Object goodCopyOf(Object[] a, int newlength) {
        Class cl = a.getClass();
        if (!cl.isArray()) return null;
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType,newlength);
        System.arraycopy(a,0,newArray,0,Math.min(length,newlength));
    }

请注意,这个CopyOf 方法可以用来扩展任意类型的数组, 而不仅是对象数组。为了能够实现上述操作,应该将goodCopyOf 的返回参数声明为Object 类型,而不要声明为对象型数组(Object[])。整型数组类型 int[]可以被转换成Object, 但不能转换成对象数组Object[]。

5.使用反射调用任何方法

先回忆一下利用Field 类的get 方法查看对象域的过程,f.get(Object) 。与之类似, 在Method 类中有一个invoke 方法, 它允许调用包装在当前Method 对象中的方法。invoke 方法的签名是:   Object invoke(Object obj , Object... args),第一个参数是隐式参数,其余的对象提供了显式参数。

对于静态方法,第一个参数可以被忽略,即可以将它设置为null。例如,假设用m1表示Employee类的getName方法,下面这条语句显示了如何调用这个方法:

String n = (String)m1.invoke(john)

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

double s = (Double)m2.invoke(john)

如何得到Method对象?当然可以通过调用getDeclaredMethods方法,然后对返回的Method对象数组进行查找,知道发现想要的为止。也可以通过调用Class类中的getMethod方法得到想要的方法。它与getField方法类似。getField方法根据表示域名的字符串,返回一个Field对象。然而,有可能存在若干个相同名字的方法,因此要格外小心,以确保能够准确地得到想要的那个方法。有鉴于此,还必须提供想要的方法的参数类型。getMethod的签名是: Method getMethod(String name,Class... parameterTypes) 。 例如,下面说明了如何获取Employee类的getName方法和raiseSalary方法的方法指针。

        Method m1 = Employee.class.getMethod("getName");
        Method m2 = Employee.class.getMethod("raiseSalary",double.class);
下面的一段代码是一个打印诸如Math.sqrt这样的数学函数值表的程序:
public class Main {
    public static void main(String[] args) throws Exception{
        Method square = Main.class.getMethod("square",double.class);
        Method sqrt = Math.class.getMethod("sqrt", double.class);
        printTable(1,10,10,square);
        printTable(1, 10, 10, sqrt);
    }

    /**
     * 返回数的平方
     * @param x 一个数
     * @return x 平方后的数字
     */
    public static double square(double x){
        return x*x;
    }

    /**
     * 打印一个表,包含一个方法的x、y的值
     * @param from,to  x的边界值
     * @param n 表的行数
     * @param f 一个有一个double变量和double返回值的方法
     */
    public static void printTable(double from, double to, int n ,Method f){
        //打印方法作为表头
        System.out.println(f);
        double dx = (to - from ) / ( n - 1);
        for (double x = from ;x <= to; x += dx){
            try {
                double y = (Double)f.invoke(null,x);
                System.out.printf("%10.4f | %10.4f%n",x,y);
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}

上述程序清楚地表明, 可以使用method 对象实现C ( 或C# 中的委派)语言中函数指针的所有操作。同C 一样,这种程序设计风格并不太简便,出错的可能性也比较大。如果在调用方法的时候提供了一个错误的参数,那么invoke 方法将会抛出一个异常。

另外, invoke 的参数和返回值必须是Object 类型的。这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。因此, 等到测试阶段才会发现这些错误, 找到并改正它们将会更加困难。不仅如此, 使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

有鉴于此, 建议仅在必要的时候才使用Method 对象,而最好使用接口以及Java SE 8 中的lambda 表达式(第6 章中介绍)。特别要重申: 建议Java 开发者不要使用Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。


那么看了半天反射机制在开发中到底有啥用啊?项目写多了也就懂了~可以先看看这个--- 反射机制有什么用


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值