Class 类
[class对象通常存放在方法区]
在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。不过,可以使用一个特殊的Java类访问这些信息。保存这些信息的类名为Class,这个名字有些让人困惑。
Object类中的getclass()方法将会返回一个class类型的实例。
Employee e;
Class cl=e.getClass();
就像 Employee对象描述一个特定员工的属性一样,(lass对象会描述一个特定类的属性。可能最常用的Class方法就是getName。这个方法将返回类的名字。例如,下面这条语句:System.out.println(e.getClass().getName()+""+ e.getName());如果e是一个员工,则会输出:Employee Harry Hacker如果e是经理,则会输出:Manager Harry Hacker如果类在一个包里,包的名字也作为类名的一部分:var generator = new Random();Class cl= generator.getClass();String name =cl.getName();//name is set to "iava.util.Random'还可以使用静态方法forName获得类名对应的class 对象String className ="java.util.Random";Class cl=Class.forName(className);如果类名保存在一个字符串中,这个字符串会在运行时变化,就可以使用这个方法。如果cassName是一个类名或接口名,这个方法可以正常执行。否则,forName方法将抛出一个检查型异常(checked exception)。无论何时使用这个方法,都应该提供一个异常处理器exception handler)。关于如何提供异常处理器,请参看下一节。
有三种方法可以获得class对象的实例
- Class.forName(""),传入类名的全限定符
- 每一个对象都有getClass()方法,例如a.getClass()
- 直接使用类名.class,例如MyClass.Class
提示:在启动时,包含main方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类,以此类推。对于一个大型的应用程序来说,这将会花费很长时间,用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的假象。不过,要确保包含 main方法的类没有显式地引用其他的类。首先,显示一个启动画面;然后,通过调用Class.forName手工地强制加载其他类。获得class类对象的第三种方法是一个很方便的快捷方式。如果T是任意的Java类型200Java核心技术卷基础知识或 void 关键字),T.class 将代表匹配的类对象。例如:Class cl1=Random.class;// if you import java.util.*;Class cl2=int.class:Class cl3 = Double[].class;请注意,一个lass对象实际上表示的是一个类型,这可能是类,也可能不是类。例如int不是类,但int.class 是一个(lass 类型的对象。注释:Class类实际上是一个泛型类。例如,Employee.class的类型是Class
注释:有一个已经废弃的(lass.toInstance方法,它也可以用无参数构造器构造一个实例。不过,如果构造器抛出一个检查型异常,这个异常将不做任何检查重新抛出。这违反了编译时异常检查的原则。与之不同,(onstructor.newInstance会把所有构造器异常包装到一个InvocationTargetException中。C++注释:newInstance方法相当于C++中的虚拟构造器概念。不过,C++中的虚拟构造器不是一个语言特性,而是需要一个专业库支持的习惯用法。(lass 类类似于C++中的type info类,getClass方法则等价于typeid运算符。不过,Java的Class比type info功能更全面。C++的 type info只能给出表示类型名的一个字符串,而不能创建那个类型的新对象。第5章继死201srl java.lang.class 10static Class forName(String className)返回一个 Class 对象,表示名为 className 的类。Constructor getConstructor(Class...parameterTypes)1.1生成一个对象,描述有指定参数类型的构造器。参见5.7.7节更多地了解如何提供参数类型。java.lang,reflect .Constructor 110bject newInstance(0bject... params)将params传递到构造器,来构造这个构造器声明类的一个新实例。参见5.7.7节更多地了解如何提供参数。java.lang.Throwable 1.0void printStackTrace()将 Throwable 对象和堆栈轨迹打印到标准错误流。
利用反射分析类的能力
下面简要介绍反射机制最重要的内容--检查类的结构。在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的字段、方法和构造器。这三个类都有一个叫做 getName的方法,用来返回字段、方法或构造器的名称.Java核心技术 卷204基础知识Field类有一个getType方法,用来返回描述字段类型的一个对象,这个对象的类型同样是Class。Method和Constructor类有报告参数类型的方法,Method类还有一个报告返回类型的方法。这三个类都有一个名为getModifiers的方法,它将返回一个整数,用不同的 0/1位描述所使用的修饰符,如public和static。另外,还可以利用java.lang.reflect包中Modifier类的静态方法分析 getModifiers返回的这个整数。例如,可以使用Modifier类中的isPublic、isPrivate 或isFinal 判断方法或构造器是public、private还是final。我们需要做的就是在 getModifiers 返回的整数上调用Modifier类中适当的方法,另外,还可以利用Modifier.tostring方法将修饰符打印出来。Class类中的getFields、getMethods和getConstructors方法将分别返回这个类支持的公共字段、方法和构造器的数组,其中包括超类的公共成员。class类的getDeclareFieldsgetDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部字段、方法和构造器的数组,其中包括私有成员、包成员和受保护成员,但不包括超类的成员。
使用反射在运行时分析对象
从前面一节中,我们已经知道如何查看任意对象数据字段字段的名字和类型:获得对应的 Class 对象。在这个Class对象上调用getDeclaredFields。本节将进一步查看字段的具体内容。当然,在编写程序时,如果知道想要查看的字段名和类型,查看对象中指定字段的内容是一件很容易的事情。而利用反射机制可以查看在编译如isAbstract方法就是检查modifiers值中对应修饰符abstract的二进制位。--译者注日第5章 继死209时还不知道的对象字段。要做到这一点,关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过 getDeclaredFields 得到的对象),obj是某个包含f字段的类的对象,f.get(obj)将返回一个对象,其值为oj的当前字段值。这样说起来显得有点抽象,下面来看一个程序。var harry=new Employee("Harry Hacker",50000,10,1,1989);Class cl=harry.getClass();//the class obiect representing EmployeeField f= cl.getDeclaredField("name");// the name field of the Employee classObject v=f.get(harry);// the value of the name field of the harry object, i.e.//the String object "Harry Hacker'当然,不仅可以获得值,也可以设置值。调用f.set(obj,value)将把对象obj的f表示的字段设置为新值。实际上,这段代码存在一个问题。由于name是一个私有字段,所以get和set方法会抛出一个IllegalAccessException。只能对可以访问的字段使用get和set方法。Java安全机制允许查看一个对象有哪些字段,但是除非拥有访问权限,否则不允许读写那些字段的值。反射机制的默认行为受限于Java的访问控制。不过,可以调用Field、Method或Constructor对象的setAccessible方法覆盖Java的访问控制。例如f.setAccessible(true);//now OK to call f.get(harry)setAccessible方法是Accessible0bject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试、持久存储和类似机制提供的。本节稍后将利用它编写一个通用的 toString 方法。如果不允许访问,setAccessible调用会抛出一个异常。访问可以被模块系统(见卷I的第9章)或安全管理器(卷Ⅱ的第10章)拒绝。安全管理器并不常用。不过,在Java9中,由于Java API是模块化的,每个程序都包含模块。
使用反射编写泛型数组代码
调用任意方法和构造器
在C和C++中,可以通过一个函数指针执行任意函数。从表面上看,Java 没有提供方法指针,也就是说,Java没有提供途径将一个方法的存储地址传给另外一个方法,以便第二个方法以后调用。事实上,Java的设计者曾说过:方法指针是很危险的,而且很容易出错。他们认为 Java 的接口(interface)和lambda表达式(将在下一章讨论)是一种更好的解决方案。不过,反射机制允许你调用任意的方法。回想一下,可以用Field类的 get方法查看一个对象的字段。与之类似,Method类有一个invoke 方法,允许你调用包装在当前Method对象中的方法。invoke方法的签名是:0bject invoke(0bject obj, 0bject... args)第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,第一个参数可以忽略,即可以将它设置为null。例如,假设用m表示Employee类的getName方法,下面这条语句显示了如何调用这个方法:String n=(String)ml.invoke(harry);如果返回类型是基本类型,invoke方法会返回其包装器类型。例如,假设m2表示Employee类的 getsalary方法,那么返回的对象实际上是一个Double,必须相应地完成强制类型转换。可以使用自动拆箱将它转换为一个double:double s=(Double)m2.invoke(harry);如何得到 Method对象呢?当然,可以调用 getDeclareMethods方法,然后搜索返回的 Method对象数组,直到发现想要的方法为止。也可以调用Class类的etMethod方法得到想要的方法
它与 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);可以使用类似的方法调用任意的构造器。将构造器的参数类型提供给assgetConstructor方法,并把参数值提供给Constructor.newInstance方法:Class cl=Random.class;//or any other class with a constructor that//accepts a long parameterConstructor cons=cl.getConstructor(long.class);0bject obj= cons.newInstance(42L);到此为止,我们已经了解了使用 Method对象的规则。下面来看如何具体使用。程序清单5-18中的程序会打印一个数学函数(如 Math.sqrt或Math.sin)的值的表格。打印的结果如下所示:
public static native double java.lang.Math.sqrt(double)1.00001.00002.00001.41421.73213.00002.00004.00002.23615.00002.44956.00007.00002.64582.82848.00003.00009.000010.00003.1623
当然,打印表格的代码与具体要打印表格的数学函数无关。
double dx=(to-from)/(n-1);for(double x=from;x<=to;x+= dx)double y=(Double)f.invoke(null,x);System.out.printf("%10.4f%10.4f%n",x,y);
在这里,f是一个Method类型的对象。由于正在调用的方法是一个静态方法,所以invoke的第一个参数是 null。要打印 Math.sqrt 函数的值表格,需要将f设置为:Math.class.getMethod("sqrt",double.class)这是Math类的一个方法,名为sqrt,有一个double类型的参数。
这个例子清楚地表明,利用method对象可以实现C语言中函数指针(或C#中的委托)所能完成的所有操作。同C中一样,这种编程风格不是很简便,而且总是很容易出错。如果在调用方法的时候提供了错误的参数会发生什么?invoke方法将会抛出一个异常。另外,invoke的参数和返回值必须是0biect类型。这就意味着必须来回进行多次强制类型转换。这样一来,编译器会丧失检查代码的机会,以至于等到测试阶段才会发现错误,而这个时候查找和修正错误会麻烦得多。不仅如此,使用反射获得方法指针的代码要比直接调用方法的代码慢得多。
有鉴于此,建议仅在绝对必要的时候才在你自己的程序中使用Method对象。通常更好的做法是使用接口以及Java8引人的lambda表达式(第6章中介绍)。特别要强调:我们建议Java开发者不要使用回调函数的Method对象。可以使用回调的接口,这样不仅代码的执行速度更快,也更易于维护。