java反射机制+继承设计技巧

来源:互联网 发布:初中课本同步软件 编辑:程序博客网 时间:2024/05/27 10:42

【0】README

0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java反射机制 ;最后还顺带提出了 继承设计的技巧;


【1】反射相关

1)反射定义:能够分析类能力的程序称为反射;
2)反射机制可以用来:

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

【2】Class 类

2.1) Java 运行时系统始终为 所有对象维护一个称为运行时的类型标志;虚拟机 利用运行时类型信息选择相应的方法执行;(干货——Java 运行时系统始终为 所有对象维护一个称为运行时的类型标志,对象在虚拟机中的运行时类信息保存在Class对象中)
2.2)Class: 可以通过专门的java类访问这些信息, 保存这些信息的类被称为 Class;Object.getClass() 将 返回一个Class 类型的实例;
2.3) 如同用一个Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性;最常用的 Class 方法是 getName方法(类名);(Class类的定义 的良好诠释)
这里写图片描述

2.4)还可以调用静态方法forName 获得类名对于的Class对象;

String className = "java.util.Date";Class c1 = Class.forName(className);

Hint)在启动时,main方法需要加载所有的类, 对于一个大型的应用程序来说, 这将会消耗很多时间, 用户会因此感到不耐烦;我们可以通过 Class.forName 手动地加载其他的类;
2.5)获得Class 类对象的第3种方法:
1)第一种方法:

Date d = new Date();Class c1 = d.getClass();String name = c1.getName();

2)第二种方法:

String className = "java.util.Date";Class c1 = Class.forName(className);

3)第三种方法:
如果T是 java类型, T.class 将代表匹配的类对象, 如

Class c1 = Date.class;Class c2 = int.class;Class c3 = Double[].class;

Attention)一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类;例如 int 不是类, 但 int.class 是一个Class类型的对象;(干货——一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类,如int不是类型,而int.class是一个Class类型的对象,那我们就可以人为 obj.class 是obj在jvm中运行时相关信息的封装对象)
Annotation)从 Java SE 5.0 开始, Class类已经参数化了,如, Class 的类型是 Employee.class;
Warning)由于历史原因, getName 应用于数组类型的时候会返回一个 很奇怪的名字:
Double[].class.getName() 返回 ”[Ljava.lang.Double“;
int[].class.getName() 返回 ”[I“;
2.6)虚拟机为每个类型管理一个 Class对象: 因此,可以用 == 运算符实现两个类对象的比较操作;
2.7)newInstance()方法: 可以用来快速创建一个类的实例;.getClass().getInstance(); 它将创建一个与 e 具有相同类型的实例, newInstance 调用默认构造器, 初始化新创建的对象,如果这个类没有默认的构造器,就会抛出一个异常;

Annotation)

  • A1)如果需要以以上这种方式向构造器提供参数的话, 那么就不要使用上面那条语句,而使用 构造器类中的newInstance 方法;
  • A2)(key)通过反射创建新类示例的两种方式及比较: http://blog.csdn.net/fenglibing/article/details/4531033 , 我的荔枝博文: http://blog.csdn.net/PacosonSWJTU/article/details/49886429

【3】捕获异常(未检查异常+已检查异常)

3.1)当程序运行过程中发生错误时, 就会抛出异常。抛出异常比终止程序要灵活得多, 这是因为可以提供一个捕获异常的处理器(handler)对异常情况进行处理;
3.2)异常有两种类型(注意区别):未检查异常和已检查异常;

  • 3.2.1)对于已检查异常: 编译器将会检查是否提供了处理器(try-cath or throws Exception);
  • 3.2.2)对于未检查异常: 例如访问null引用,都属于未检查异常,编译器不会查看是否为这些错误提供了处理器;

3.3)最简单的异常处理器

try catch块;

3.4)如果类名不存在, 则将跳过try 块中的剩余代码, 程序直接进入catch 子句, 这里,利用Throwable 类的 printStackTrace 方法打印出栈的轨迹;


【4】利用反射分析类的能力

4.1)反射机制最重要的内容——检查类的结构;
4.2)在java.lang.reflect 包中有3个类 Field、Method 和 Constructor 分别用于描述类的域、方法 和 构造器;
4.3)这3个类都有一个 getName的方法, 用来返回项目名称

  • 4.3.1)Field类: Field类有一个 getType方法, 用来返回描述域所属类型的 Class对象;
  • 4.3.2)Method 和 Constructor 类: Method 和 Constructor 类有能够报告参数类型的方法,Method类还可以有一个报告返回类型的方法;
  • 4.3.3) 这3个类还有一个叫做 getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public 和 static这样的修饰符使用状况;
  • 4.3.4) 而且,还可以用 java.lang.reflect 包中的Modifier类的静态方法分析 getModifiers 返回的整型数值;如, 可以使用 Modifier 类中的 isPublic、isPrivate 或 isFinal 判断方法或构造器 是否是 public, private , final;我们需要做的工作就是调用 Modifier 类的相应方法, 并对返回的整型数值进行分析;

4.4) Class类中的 getField、getMethod 和 getConstructor 方法将分别返回类提供的public 域、方法和构造器数值,其中包括超类的公有成员;
4.5) Class 类的 getDeclareFields、getDeclareMethods、getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器;其中包括私有方法和受保护成员,但不包括超类成员


【5】在运行时使用反射分析对象

5.1)本节将进一步查看数据域的实际内容;
5.2)利用反射机制可以查看在编译时还不清楚的对象域;
5.3)查看对象域的关键方法是 Field类中的 get方法;如果 f 是一个 Field 类型的 对象, obj 是某个包含 f 域的类对象, f.get(obj) 将返回一个对象,其值为obj 域的当前值;
5.4)看个荔枝:

对以上代码的分析(Analysis):

  • A1)由于name 是一个 private, 所以get方法会抛出一个异常;
  • A2)只有利用get 方法才能得到可访问域的值;除非拥有访问权限,否则 java 安全机制只允许查看任意对象有哪些域, 而不允许读取他们的值;

5.5)引入setAccessible 方法(打印结果与5.4做比较,这个很有必要): 反射机制的默认行为受限于 Java的访问控制, 然而,如果一个 java 程序没有收到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用 Field、Method、Constructor对象 的 setAccessible 方法;如,f.setAccessible(true);

5.6)get方法 所遇到的问题 + 解决方法

  • 5.6.1)get遇到的问题: name域是一个 String,因此把它作为 Object 返回没有什么问题;但是,假定我们想要查看salary 域, 它属于double 类型, 而java中数值类型不是对象;
  • 5.6.2)解决方法:可以使用 Field 类中的 getDouble 方法,也可以调用get 方法, 此时, 反射机制将会自动地将这个域值打包到相应的对象包装器中, 这里将打包成 Double;
  • 5.6.3)当然, 可以获得就可以设置; 调用 f.set(obj, value) 可以将 obj 对象的 f域设置成新值;
    这里写图片描述

  • 5.6.4)如何编写一个可供任意类使用的通用 toString()方法;(P203,非常重要)


【6】使用反射编写泛型数组代码

6.1)java.lang.reflect 包中的Array 类允许动态地创建数组。例如, Array类中的copyOf 方法,用于扩充数组容量:

int[] a = new int[100];a = Arrays.copyOf(a, 2*a.length());

6.2)如何编写这样一个通用的方法?

  • 6.2.1)将Employee[] 数组转换为 Object[] 数组, 这让人感觉很有希望;
public static Object[] badCopyOf(Object[] a, int newLength){    Object[] newArray = new Object[newLength];    System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));    return newArray;}

  • 6.2.2)使用以上代码遇到的问题:一个对象数组不能转换为 雇员数组(Employee[]);

Attention)

  • A1) Java 数组会记住每个元素的类型, 即创建数组时 new 表达式中使用的元素类型。将一个 Employee[] 临时转换为 Object[] 数组, 然后再把它转换回来是可以的, 但一个从开始就是Object[] 的数组却永远不会转换成Employee[] 数组。所以, 为了能够编写这类通用的数组代码, 需要能够创建与 原数组类型相同的新数组;
  • A2)所以,我们需要java.lang.reflect 包中Array类的一些方法。 其中最关键的是 Array 类中的静态方法 newInstance, 它能够构造新数组。在调用它时 必须提供两个参数, 一个是数组的元素类型, 一个是数组的长度;
Object newArray = Array.newInstance(componentType, newLength);

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

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

这里写图片描述


【7】调用任意方法

7.1)在Method类中有一个invoke 方法, 它允许调用包装在当前 Method 对象中的方法, invoke方法的签名是: Object invoke(Object obj, Object… args), 如 String n = (String)m1.invoke(harry);
7.2)如何得到 Method 对象呢?
7.2.1)通过 getDeclaredMethods 方法 , 然后对返回的Method对象数组进行查找, 直到发现想要的方法为止;
7.2.2)通过调用 Class 类中的getMethod 方法得到想要的方法;它与 getField 方法类似, getField 方法根据表示域名的字符串, 返回一个 Field对象;然而,有可能存在着若干个相同名字的方法, 因此要格外小心, 以确保准确地得到想要的方法;所以, 我们需要提供想要方法的参数类型;
7.3)getMethod的方法签名是: Method getMethod(String name, Class… parameterTypes) ;
7.4)看个荔枝(如何获得 Employee类的 getName 方法 和 raiseSalary 方法的方法指针):

Method m1 = Employee.class.getMethod("getName");Method m2 = Employee.class.getMethod("getSalary", double.class);

7.5)再看个荔枝
这里, f是一个Method类型的对象,由于正在调用的 方法是一个静态方法,所以 invokle 的第一个参数是null;
为了将 Math.sqrt 函数表格化, 需要将f 设置为: Math.class.getMethod(“sqrt”, double.class);这是 Math类中的一个方法, 通过参数向它提供了一个函数名 sqrt 和 一个double类型参数

对以上代码的分析(Analysis):

  • A1) invoke 的参数和返回值必须是 Object类型的, 这就意味着必须进行多次的类型转换;
  • A2)使用反射获得方法指针的代码要比仅仅直接调用方法明显 慢一些;

Attention)

  • A1) 建议仅在必要的时候才使用 Method对象,而最好使用接口和内部类;
  • A2)特别重申: 建议 Java 开发者不要使用 Method 对象的回调功能, 使用接口进行回调会使得代码的执行速度更快, 更易于维护;

【8】继承设计的技巧

8.1)将公共操作和域放在超类;
8.2)不要使用受保护的域:

  • 8.2.1)第一:子类集合是无限制的, 任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域, 从而破坏了封装性;
  • 8.2.2)第二:在java程序设计语言中,在同一个包中的所有类都可以访问 protected域, 而不管它是否为这个类的子类;

8.3)使用继承实现 “is-a” 关系;
8.4)除非所有继承的方法都有意义, 否则不要使用继承;
8.5)在覆盖方法时, 不要改变预期的行为;
8.6)使用多态,而非类型信息:无论什么时候 , 碰到这种代码,都应该使用 多态性;

if(x is of type1)    action1(x);else if(x is of type2 )    action2(x);

8.7)不要过多地使用反射:

  • 8.7.1)反射机制使得人们可以通过在运行时查看与核方法,让个人编写出更具有通用性的程序;这种功能对于编写系统程序来说极为实用, 但是通常不适用于编写应用程序;
  • 8.7.2)反射是很脆弱的, 即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常;
0 0
原创粉丝点击