Java> Java核心卷读书笔记 - 反射

反射简介

能够分析类能力的程序成为反射(reflective)。

反射可以用来干什么?
反射经常用于构建库或工具,主要包含以下能力:

  • 运行时分析类的能力;
  • 运行时查看对象,如编写一个toString查看对象属性;
  • 实现通用的数组操作代码;
  • 利用Method对象,类似于C/C++函数指针;

Class类

主要作用:维护Java对象类型信息 -- 程序运行时,Java运行时系统始终为所有对象维护一个被成为运行时的类型标识。该信息跟踪者每个对象所属的类。虚拟机利用运行时类型信息选择相应方法执行。
保存这些维护类型信息的类称为Class。

获取Class类对象

如何获取Class类对象?
有3种方法:

  1. 对象的getClass()方法;
  2. Class静态方法Class.forName();
  3. 类名.class;

示例:

// 方法1: 一个类的对象 -> Class类对象
Employee e;
Class c = e.getClass(); // 通过getClass(), 获取对象e的类型信息

// 方法2: 类名称: -> Class 类对象
String className = "java.util.Random"; // 类名(需要包含包的路径)
Class c2 = Class.forName(className) ;  // 通过Clas静态方法forName(),

// 方法3: 类名.class -> Class类对象
Class random = Random.class;
Class cint = int.class;
Class cdoubles = Double[].class;

Class类包含哪些类的信息?

  1. 类名;
  2. 创建类的实例;
  3. 比较类型信息;

读取类名

Class的getName方法可以读取类名,包也作为类名的一部分

System.out.println(e.getClass.getName() + " " + e.getName()); 

创建类的实例

Class对象的newInstance方法

// 根据不同获取Class对象方式, 分为三种方式, 不过都是调用Class对象的newInstance方法
// 方式一
Employee e = new Employee();
e.getClass().newInstance();

// 方式二
String s = "java.util.Random";
Object m = Class.forName(s).newInstance();

// 方式三
Object m = Integer.class.newInstance();

比较类型信息"=="

类似于Ojective-C的内省方法,可以用 “==”检查某个类对象是否为指定类对象。
如,

// 检查e的Class对象是否为Employee类对象
Employee e;
if(e.getClass() == Employee.class) {
      ..
}
// 检查e的父对象是否为Object类对象
Employee e;
Class cl = e.getClass();
Class supercl = cl.getSupperClass();

if (supercl == Object.class) {
      // process e or cl
}

捕获异常

如果类名对应类不存在,用forName()创建Class对象会抛出异常ClassNotFoundException。简单处理方式,如下

try {
      Claass c1 = Class.forName("TestNotExistClass");
      // do sth. with c1
}catch(Exception e) {
      e.printStackTrace();
}

反射分析类

  1. 字段类:Field,提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段;
  2. 方法类:Method;
  3. 构造器类:Constructor;
  4. 修饰符工具类:Modifiers;

Field 字段类型类, Method 方法类型类, Constructor 构造器类

java.lang.reflect包有三个类Field、Method、Constructor分别用于描述类的域(字段)、方法、构造器。
三个类都有getName()方法,返回项名称。
Field类有getType方法,返回域所属类型的Class对象。
Method类和Constructor类有能够报告参数类型的方法。
Method类getReturnType方法能报告函数返回值类型。

Class常用方法

Field[] getFields(); // 返回包含Field对象的数组,记录了类或其超类的公有域
Field[] getDeclaredFields();  // 返回包含Field对象的数组,记录了类的所有域(不包括超类)

Method getMethod(String name, Class<?>...  parameterTypes); // 返回指定名称 + 形参类型和数量的方法
Method[] getMethods(); // 返回包含Method对象的数组,记录了类或其超类的公有方法
Method[] getDeclaredMethods();  // 返回包含Method对象的数组,记录了类的所有方法(不包括超类)

Constructor[] getConstructors(); // 返回包含Constructor对象的数组,记录了类或其超类的公有构造器
Constructor[] getDeclareConstructor(); // 返回包含Constructor对象的数组,记录了类的所有构造器(不包括超类)

Class getClass(); // 获取对象或者类的Class对象
Class getSuperclass(); // 获取对象或者类的父类Class对象
Class getName(); // 获取对象或类的名称(字符串)
static Class forName(String); // 根据路径创建Class对象

TypeVariable[] getTypeParameters(); // 获取声明的变量类型的数组

int getModifiers(); // 获取对象或类的的Java语言修饰符(如public, static)使用情况

Field、Method、Constructor常用方法

Class getType(); (Field独有) // 获取域的类型
Class getReturnType(); (Method独有) // 获取返回值类型
Class[] getExceptionTypes(); (Constructor和Method类中) // 描述方法抛出的异常类型数组
Class[] getParameterTypes(); (Constructor和Method类中) // 描述参数类型的Class对象数组
Object get(Object obj); // 获取obj对象中Field对象表示的值, 调用对象是Field对象(即要获取的属性),obj对象是要查询的属性值所属类对象. 也就是说,返回obj.调用对象的Field值
void set(Object obj, Object newValue); // 设置obj.调用对象所属Field = newValue

Class getDeclaringClass(); // 返回用于描述类中定义的Constructor、Method或Field的类型
Class getName(); // 获取名称
int getModifiers(); // 获取修饰符使用情况
void setAccessible(boolean ); // 修改反射对象的访问标志 (为调试、持久存储和相似机制提供的功能)
boolean isAccessible(); // 查询反射对象可访问标志

Modifiers 修饰符工具类

Field、Method、Constructor三个类都有getModifiers方法,返回一个整型值,不同的位代表public static等修饰符使用情况。
可以利用java.lang.reflect包中的Modifier类的静态分析getModifiers返回的整型值,如使用Modifier类的isPublic、isPrivate、isFinal判断方法或构造器是否是public、private或final。
Modifiers.toString() 将修饰符打印出来。

获取Field, Method, Constructor
Class类的getFields、getMethods和getConstructors方法返回类的public 域、方法和构造器数组。(所有的public成员,包括超类的public成员)
Class类的getDeclareFields、getDeclareMethods、getDeclareConstructorsf方法返回类类的所有域、方法和构造器数组。(所有的public、private、protected成员,但不包括超类的成员)

Modifiers 常用方法

static String toString(int modifiers); // 读取modifilers中设置的修饰符使用情况的字符串表示
// 读取是否为函数名对应修饰符修饰
static boolean isAbstract(int modifiers); 
static boolean isFinal(int modifiers);
static boolean isInterface(int modifiers);
static boolean isNative(int modifiers);
static boolean isPrivate(int modifiers);
static boolean isProtected(int modifiers);
static boolean isPublic(int modifiers);
static boolean isStatic(int modifiers);
static boolean isStrict(int modifiers);
static boolean isSynchronized(int modifiers);
static boolean isVolatile(int modifiers);

运行时使用反射分析对象

查看数据域实际内容

先获得Class对象,再通过Class对象GetDeclaredField获得对应名称数据域,如果是私有访问权限,就利用Field/Method/Constructor的setAccessible方法修改访问权限控制属性。

修改访问权限

示例

Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989) ;
Class cl = harry.getClass(); // cl 代表Employee类对象
Field f = cl .getDeclaredField("name"); // 获取harry对象的name数据域
Object v = f.get (harry) ;
// 获取harry对象的name数据域的值,也就是说"Harry Hacker"字符串对象

存在问题:如果"name"域是private访问权限,那么后面的get方法就会抛出异常,需要先修改访问权限。

Field f = cl .getDeclaredField("name"); // 获取harry对象的name数据域
f.setAccessible(true);
Object v = f.get (harry) ;

查看任意对象的内部信息

ArrayList<Integer> squres = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
      squares.add(i);
}

System.out.println(new ObjectAnalyzer.toString(squares));

使用反射编写泛型数组

如何复制一个数组?为何要使用反射编写泛型数组?

例如实现Arrays.copyOf()方法。常规做法,编写一个通用的程序:

// Arrays.copyOf方法的使用
Employee[] a = new Employee[100];
// modify array a
// 创建新数组(存储),并复制老的元素到数组a (将Employee[] 类型转化成了Object[]类型)
a = Arrays.copyOf(a, 2*a.length); // 超出被复制数组长度部分用默认值0填充

// 自定义copyOf方法 -- 存在问题的自定义方法
public static Object[] badCopyOf(Object[] a, int newLength) {
      if(newLength <= 0) return new int[0];
      Object[] newArray = new Object[newLength];
      System.arraycopy(a, 0, newArray, Math.min(a.length, newLength));
      return newArray;
}

程序根据原有数组Employee[] a,新建一个指定长度新数组Object[] newArray,再将数组a的值全部复制到newArray中。
看起来很美好,然而存在一个严重问题:新建的newArray是Object[]类型,即使强制转化成Employee[]类型,也不可能真正转化成Employee[],除非从一开始就是Employee[]中间可以临时转化成Object[]。而用户是不能直接根据Employee所包含的方法使用Object对象的,即使是强制转换也不行。

改进:将a的类型信息也一并传入复制函数。根据参数创建指定类型数组,可以利用Array.newInstance()
Object newArray = Array.newInstance(componentType, newLength);
componentType: 要创建数组类型信息,通过Class对象的getComponentType()方法获得;
newLength: 要创建数组长度,通过Array.getLength(a)获得;

复制一个新的同类型的数组的步骤:

  1. 获得数组a的类对象(Class cl = a.getClass());
  2. 确认a是数组(Class的isArray方法);
  3. 获得数组a的类型信息(cl.getComponentType());
  4. 获得数组a的长度(Array.getLength(a));
  5. 创建指定类型和长度的数组(Array.newInstance(componentType, newLength));
  6. 复制旧数组元素到新数组(System.arraycopy());

实现copyOf代码:

public static Object copyOf(Object a, int newLength) {
      Class cl = a.getClass();
      if(!cl.isArray() return null; // 确认不是数组,就返回空
      Class componentType = cl.getComponentType();
      int length = Array.getLength(a); // 前提是a实际是一个数组
      Object newArray = Array.newArray(componentType, newLength);
      System.arrarycopy(a, 0, newArray, 0, Math.min(length, newLength));
      return newArray;
}

调用任意方法

C/C++有指针执行任意函数,java没有方法指针,不过提供了接口,另外还可以通过反射机制调用任意方法。
要调用的方法是invoke,原型:

/**
* 第一个参数obj是隐式参数(静态方法隐式参数可忽略),其余对象提供了显式参数(没有显示参数就用null)
* invoke的参数和返回值必须是Object类型,1)设计风格复杂,类似于C;2)会经过多次类型转化,导致错过编译器类型检查,出错可能性较大(如提供invoke错误参数)
*/
public Object invoke(Object obj, Object...args)
public Object invoke(Object implicitParameter,Object[] explicitParamenters)

使用示例:

String n = (String)m1.invoke(harry);

如果返回的是基本类型,invoke方法会自动返回其包装器类型。

Method m1 = Employee.class.getMethod("getName"); // 无参数方法getName()
Method m2 = Employee.class.getMethod("raiseSalary", double.class); // 包含一个形参 raiseSalary(double)
String name = (String)m1.invoke(e1); // 返回Employee对象e1.getName() 结果
m2.invoke(e2, 10.0); //调用e2.raiseSalary(double),为e2增加薪资

Method m3 = Math.class.getMethod("max", int.class, int.class); // m3代表方法Math.max(int, int), 注意函数名和参数类型、数量都要对应上
int max = (int)m3.invoke(null, 20, 40); // 静态方法invoke第一个参数传入null,后面的参数与取得实际m3所需要形参类型和数量对应上
System.out.println("max number = " + max);

小结

反射要点

  • 获得对应Class对象
  • 通过Class对象调用getDeclaredFields获得对应Field
  • 利用反射查看编译时不清楚的对象域

建议
仅在必要时,才使用Method对象的回调功能(invoke),一般情况下最好使用接口即lambda表达式来调用方法。因为接口回调速度比使用Method对象更快,更容易维护。

posted @ 2020-12-11 01:43  明明1109  阅读(104)  评论(0编辑  收藏  举报