Java 反射


反射库能用来编写能够动态操控代码的程序。

反射机制可以用来:

  • 在运行时分析类的能力。
  • 在运行时检查对象。
  • 实现泛型数组操控代码。
  • 利用 Method 对象。

Class 类

在程序运行时,java 始终会为所有对象维护一个运行时类型标识。在多态发生时可以利用这个信息确定执行哪个方法。

不过,可以利用一个特殊的 java 类访问这些信息,这个类就是 Class 类。在我们调用 getClass () 时返回的就是一个 Class 类。

Class 类最常用的方法时 getName(),这个方法会返回 String 类型的类的名字。如果类在一个包里,包名也将作为类名的一部分。
相对的,可以用 Class 类型的 forName()方法将字符串类型的类的名字转化为 Class 类型。
如果字符串不是一个类名或接口名,则会抛出一个检查性异常 checked exception,因此,在调用这个方法时,必须提供一个异常处理器。

获得 Class 类的第三种方法非常便捷, 类名.class 代表该类所匹配的 Class 对象。
需要注意的是,基本类型,比如 int.class 也会返回一个 Class 对象。

Class 类事实上是一个泛型类,比如 Integer.class 的类型为 Class<Integer> 。

每个类型有一个唯一的 Class 对象,因此可以直接用 == 进行比较。

可以用 getConstructor () 方法构造某个 Class 类的对象,然后用 newInstance () 方法构造一个实例(如果该类没有无参构造器,则会抛出一个异常)。

getConstructor () 返回类型为 java.lang.reflect.Constructor<T>。

类似

public static void main(String[] args) throws
NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    var test=Test.class.getConstructor().newInstance()//其中Test有无参构造器
    }

声明异常入门

当程序运行遇到错误时,程序会抛出一个异常,此时可以给出一个处理器(handler)捕获这个异常并做出处理。

异常分为检查性异常和非检查性异常。对于检查性异常,编译器会检查是否程序员已经知道了这些可能异常并给出对应的处理器。

对于非检查性异常,比如 null 和数组越界,编译器则更希望程序员避免这些问题而非给出一个处理器。

最简单的异常处理的策略是,在方法名后给出 throws 子句,任何调用该方法的方法也要给出 throws 声明,如果出现异常,main 方法会终止并给出一个堆栈轨迹,让你找到是哪个方法抛出的异常。

类似

Exception in thread "main" java.lang.NoSuchMethodException: java.lang.Integer.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3617)
	at java.base/java.lang.Class.getConstructor(Class.java:2303)
	at Method.main(Method.java:20)

资源

类通常会有一些关联的数据文件,在 java 中,它们被称为资源。

比如一个 Student 类的照片数据。

Class 类提供了一个有用的服务用于查找文件:

  1. 获得拥有资源的对象的 Class 对象。
  2. 使用 getResource (String) 方法得到一个 URL。
  3. 或者,使用 getResourceAsStream (String) 方法得到一个 InputStream。

这里的 String 都是 Path,可以是绝对路径,也可以是相对路径。

path 不以 ‘/’ 开头时,默认是从此类所在的包下取资源;
path 以 ‘/’ 开头时,则是从项目的根目录下获取资源。(在 IDEA 中为项目根目录,在命令行中为当前位置,在 jar 包中为根目录)

这个查找方法的最大优点是可以查找 jar 包中的资源(资源一般都直接放在 jar 文件中)。

对于资源的内容,没有标准的方法解释,每个程序有自己的方式解释其资源文件。

利用反射分析类的能力

java.lang.reflect 包中有三个类 Field、Method、Constructor 分别描述类的字段、方法和构造器。

可以用 Class.getField()等得到某个类的 Field 等。
getField()都需要一个 String 类型的参数代表字段名。
getMethod()需要提供方法名和 Class 类型的参数表。
getConstructor()方法需要一列 Class 类型的参数表。

也可以用 getFields () 等得到某个类的字段 / 方法 / 构造器数组,其中包括从父类继承来的字段和方法。

这三个类都有一个 getName()方法,用于返回名称。
Field 类有一个 getType()方法,用于返回字段类型的一个对象。
这三个类都有一个 getModifiers()方法,用 0/1 位描述使用的修饰符(比如 public 等)

可以用 Modifier.isPublic()等方法处理 getModifiers()的返回值判断是否为 public 类型。
也可以用 Modifier.toString()方法输出所有修饰符。

以上方法只能访问公共字段、方法、构造器。但我们可以用 getDeclaredField (String name)/getDeclaredFields () 等访问私有的或受保护的字段、方法、构造器。

除了以上方法以外,Class 类还有一个 getPackageName () 方法返回包名。对于数组类型,返回元素类型的包名;对于基本类型,返回 “java.lang”。

对于 Field、Method、Constructor 对象,分别有一些方法用于分析字段、方法、构造器的细节。
比如,可以用 Method.getReturnType()方法用 Class 类型返回返回值类型。可以用 Method.getParameterTypes()返回参数表的数组。

利用反射在运行时分析对象

对于某个类 Obj 的某个字段 f,可以用 f.get (Obj obj) 得到 obj 对象在这个字段上的值。当然不仅能获得值,也能设置值,可以用 f.set (Obj obj,xxx value) 设置某个字段上的值。

为了不破坏封装性,get 和 set 只能对有权限访问的字段使用(在类内部 private 修饰的字段也能用 get 和 set 方法,因为 private 对自己的类是可见的),在使用时也要检查 IllegalAccessException。

不过,可以调用 Field/Method/Constructor 对象的 setAccessible 方法覆盖 java 的访问控制,比如

f.setAccessible(true);

当然,这会破坏封装性,但 java 的修饰符本就不是为了绝对安全设计的,而是对用户常规使用 Java 的一种约束。

但这种访问仍然可能被模块系统或安全管理器所拒绝并报错。

通过设置访问权限,我们可以得到一个可以用于任何对象的 toString () 方法。

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

如何实现一个动态大小的数组。类似函数 Arrays.copyOf (xxx [] array,int size),它的返回类型为 xxx []。

static public Object[] firstCopy(Object[] obj,int newLength){
    var newArray=new Object[newLength];
    System.arraycopy(obj,0,newArray,0,Math.min(obj.length,newLength));
    return newArray;
}

如果使用一般的写法,我们只能返回 Object []。因为我们创建时使用的是 new Object [],我们也无法将其强转回我们想要的类型。(如果我们将 Integer [] 转化为 Object [],再强转回来是可行的)

此时需要使用 java.lang.reflect 包中的 Array 类。

Array.newInstance (Class type,int length) 可以返回一个 type 类型的数组。

Class.getComponentType () 可以得到一个数组类型的元素类型。

给出以下代码,

static public Object[] Copyof(Object[] obj,int newLength){
    if(!obj.getClass().isArray())
        return null;
    Class cl=obj.getClass().getComponentType();
    Object[] newArray=(Object[]) Array.newInstance(cl,newLength);//该函数的返回类型为Object,但它实际上是个cl类型的数组
    System.arraycopy(obj,0,newArray,0,Array.getLength(obj));
    return newArray;
}

java 中,数组也是对象,可以将以上代码改为,以下代码还有一个优点,可以兼容基本类型(int [] 可以转化为 Object,但不能转化为 Object [])

static public Object Copyof(Object obj,int newLength){
    if(!obj.getClass().isArray())
        return null;
    Class cl=obj.getClass().getComponentType();
    Object newArray=Array.newInstance(cl,newLength);
    System.arraycopy(obj,0,newArray,0,Array.getLength(obj));
    return newArray;
}

java 会在需要时自动进行向上转型和向下转型,这些转型显然不会报错。

可以在自己的代码中使用 copyOf () 实现数组大小的变化。

Integer[] a={1,2,3,4,5};
Integer[] b=(Integer[])Copyof(a,10);

调用任意方法和构造器

对于任意 Method 类的对象,可以调用,Method.invoke(Obj obj,arg1,arg2…),其中第一个参数是隐式参数,对于静态方法,第一个对象可以设置为 null。
如果返回类型是基本类型,invoke 会将其包装为对象。

可以用 getMethod () 的得到一个想要的方法,需要提供方法名和 Class 类型的参数表。

可以类似地调用 Constructor 对象,使用 getConstructor () 方法得到一个想要的构造器,使用 Constructor.newInstance (arg1,arg2…) 方法得到一个新的对象。

invoke 的参数和返回值为 Object 类型,这意味着我们需要多次进行强转,编译器无法在编译阶段找到错误,如果提供了错误的参数,invoke 会在运行时抛出一个异常。不仅如此,使用反射调用方法比正常调用要慢得多。

有鉴于此,只在存在绝对需要的情况下才会在代码中使用 Method 对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值