阅读 71

java 反射基础大全

1、前言

指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。反射的功能非常强大,也非常灵活,解决事情的角度来看,有以下作用

  • 调用隐藏api
  • 获取注解信息
  • 切面编程

但反射也有缺点,在能不使用反射完成时,尽量不要使用,原因有以下几点:

  1. 性能问题: Java反射机制中包含了一些动态类型,所以Java虚拟机不能够对这些动态代码进行优化。现在的机器一般都很好了,当反射的次数较少,性能基本没有什么影响
  2. 安全限制: 使用反射通常需要程序的运行没有安全方面的限制。
  3. 程序健壮性:反射允许代码执行一些通常不被允许的操作,所以使用反射有可能会导致意想不到的后果。

从技术的角度来看,反射涉及到哪些方面呢?

  1. 反射的相关基础类 Class、Field、Method、Constructor
  2. 泛型以及泛型的反射
  3. 数组的反射
  4. 注解

2、基础api

对于反射,很重要的一个类就是Class;Class是jvm在需要加载类信息时,创建的类信息对象;java中每个类都在jvm中对应一个Class对象;Class对象不能自己创建,且在同一个jvm中,一个类对应唯一的类对象;

注意: 反射依据名字来查找时,可能找不到,这时均会抛出异常

2.1 Class操作

这个对象很关键,是运行时反射的入口(java注解器,是源码阶段处理,也有一套反射获取的代码,我称为静态反射);

class的常规操作,包括获取、生成实例、获取属性方法等

获取Class对象有三种方式

  • 代码可以引用到类时,

      Class<?> cls = Test.class;
    复制代码
  • 代码中可以生成类对象时

      Class<?> cla = test.getClass();
    复制代码
  • 代码中没有类信息,可以用类的全限定名查找,有可能找不到,会抛出异常

      Class<?> lll = Class.forName("com.csu.liutao.Test");
    复制代码

生成Class对应类对象

	Test tt = (Test) cls.newInstance();
复制代码

生成类对象也可以使用Constructor来处理;包括对属性的操作、方法的操作分别放在后面介绍

2.2 属性操作

属性的操作分为:获取、获取属性名字-值,修改属性值;属性的对象由Class对象来获取操作

获取当前类或者其父类、祖父类等的public属性

 Field[] fields = cls.getFields();
 Field field = cls.getField("name");
复制代码

仅仅获取当前类声明的所有属性

Field[] fields = cls.getDeclaredFields();
Field field = cls.getDeclaredField("name");
复制代码

获取/设置属性值

String name = field.getName();
String value = (String) field.get(test);
field.set(test, "xinde");
复制代码

有一点要明白;属性的名字,是类加载后就定下来的,与具体对象无关,但属性的值,是创建对象后初始化的,依赖于具体实例;上面代码中test就代表相应的实列

有没有人实操过发现,哎,我私有的方法竟然不能获取属性值,修改属性值;那是因为private的访问受到了限制,通过下面代码,可以不受此限制

field.setAccessible(true);
复制代码

还有人的说了,静态属性呢,不依赖于实例,test传null即可

2.3 方法操作

方法对象同样由Class来处理;常见操作:获取方法,执行方法;获取方法由下面两种

  • 获取所有的Public方法,包括继承的类中public方法

      Method[] methods = cls.getMethods();
      Method method = cls.getMethod("hello", new Class[]{String.class});
    复制代码
  • 仅仅获取本类中的声明方法,不限是否private

      methods = cls.getDeclaredMethods();
      Method method = cls.getDeclaredMethod("hello", new Class[]{String.class});
    复制代码

根据名字查询时,第一个参数是方法名字,第二个参数是参数class数组,无参时不传;

执行方法,同样要设置权限放开,且静态方法时,invoke的第一个参数传空,后面参数即为方法参数,无时可不传

method.setAccessible(true);
method.invoke(test, "getDeclaredMethod invoke hello method");
复制代码

在注解中,也会获取方法参数信息,返回值信息

返回值信息

	Method.getReturnType(); // 类信息,不包括泛型
	Method.getGenericReturnType(); // 泛型信息
复制代码

请求参数信息:

	Method.getParameterTypes()); // 类信息
	Method.getGenericParameterTypes(); // 泛型信息
    Method.getParameters() // 参数信息,包括类型信息(Parameter.getType())、泛型信息(Parameter.getParameterizedType())
复制代码

2.4 Constructor构造器对象

构造器是一个特殊的方法;常规操作:获取、生成实例;获取分为2种方式,获取过程中只需要传入参数列别的class数组

  • 获取本类中public构造器

      Constructor[] constructors = cls.getConstructors();
      Constructor constructor = cls.getConstructor(new Class[]{int.class, long.class});
    复制代码
  • 获取本类中声明的所有构造器

      constructors = cls.getDeclaredConstructors();
      constructor = cls.getDeclaredConstructor(new Class[]{int.class, long.class});
    复制代码

执行,如果获取的不一定是public,需要开启权限限制;newInstance方法只需要传入构造参数即可

	constructor.setAccessible(true);
    constructor.newInstance(5, 10L);
复制代码

3、泛型与反射

如果没有泛型,反射要简单很多;但现在的情况是,不仅有泛型,而且用的很是很广的;那我们先看泛型是啥吧

3.1、泛型

泛型就是参数化类型:适用于多种数据类型执行相同的代码;泛型中的类型在使用时指定;泛型归根到底就是“模版”;

在java中,在运行时才可知泛型类型,而且泛型只是一个语法糖,编译后的class文件只有一个而且是没有泛型参数的(泛型参数 变化成 上边界,默认Object),也可以说是类型擦除;泛型类继承实现中也会产生桥接方法;

public class Temp<T> {
    T t;
    
    public <U> U convert(T t) {
        return (U) t;
    }
    
    public <U> void set(U u) {
        t = (T) u;
    }
}
复制代码

这段代码;就展示了泛型常见的用法,泛型类、泛型方法

泛型除了用一个标志来表达外,还可以有通配符的形式: ?, ? extend, ? super

  • ? :任意类型;
  • ? extend:某个类的子类
  • ? super:某个类的超类

类型擦除

其实理解这个也很简单,就是比如上面我写的代码,编译后;泛型信息肯定也会存储,而实际java代码可以大致看成是下面的;泛型参数被替换为上界,这里是Object,如果是? extend String, 那会替换String

public class Temp {
    Object t;
}
复制代码

泛型使用时需要注意以下限制:

  1. 不能使用基本类型:由于类型擦除,所以只能是Object以及其子类
  2. 不能实例化,主要是为了防止类型转换错误
  3. 不允许定义静态变量,静态变量初始化在创建实例之前,而创建实例时不知道泛型的类型,所以不行
  4. 不是协变的,通配符是协变的,数组是协变的,这还是类型擦除的原因;泛型的参数是继承关系,那么这两个泛型类也是继承关系,这就协变的
  5. instanceof 不支持具体泛型,可以使用泛型通配符;也是因为擦除
  6. 不能通过new建立具体的类,通配符可以;和擦除有关,new对象时,泛型参数必须可知
  7. 不允许定义泛型异常类或者catch异常(throws可以)
  8. 不允许方法重载

这些限制,基本是由于泛型擦除、类创建对象的生命周期的原因;

3.2 泛型与集合

通配符和集合结合时,存在添加和获取的限制:

  1. ? extends:不能添加,只能获取
  2. ? super:能够添加类以及其子类,获取的是Object

3.3 泛型与反射

这里主要是说明如何获取泛型信息; 泛型信息有四种:

  1. TypeVariable类型:

     T object; // T即为此类型
    复制代码
  2. ParameterizedType类型:

     ArrayList<T> list;// ArrayList<T> 即为此类型
    复制代码

    可以通过类方法获取,擦除后类类型,以及泛型类型

     getActualTypeArguments()  // 泛型参数类型,即T
     getRawType() // 擦除后类型,即ArrayList
    复制代码
  3. GenericArrayType类型

     T[] array;// T[] 即为此类型
    
    类中方法可以获取T类型,方法如下
    
     getGenericComponentType()
    复制代码
  4. WildcardType类型

     ? extends Number // 这种类型
    通过类中方法,可以获取上下界:
    
     Type[] getUpperBounds();
     Type[] getLowerBounds();
    复制代码

通过下列方法获取上述类型中某一种:

Class.getTypeParameters() // 类中泛型参数
Class.getComponentType() // 对象数组中泛型参数

Method.getGenericParameterTypes() // 所有参数泛型
Method.getGenericReturnType() // 返回值泛型

Field.getGenericType() // 变量泛型信息
复制代码

3.4 数组与反射

前面已经说了一些在反射中,数组的一些特性,这里只说说,如何利用反射创建,增加和获取;没有其它操作了,完成操作的类为Array;

创建

Object array = Array.newInstance(String.class, 10);
复制代码

添加

Array.set(array, 5, "hello");
复制代码

获取

String str = (String)Array.get(array, 5);
复制代码

4、 注解与反射

这里是关于运行时注解的;也就是注解时机:@Retention(RetentionPolicy.RUNTIME)

4.1 注解简介

注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
}
复制代码

其实就是定义一种接口;对上面的解释如下

  1. @interface表示这是一个注解接口
  2. @Document表示注释被包含在javadoc中
  3. @Retention表示注释保留的生命周期可选的RetentionPolicy参数包括:
    • SOURCE:注解将被编译器丢弃
    • CLASS:注解在class文件中可用,但会被VM丢弃
    • RUNTIME:VM将在运行期间保留注解,因此可以通过反射机制读取注解的信息。
  4. @Target 该注解可以用于什么地方,可能的ElementType参数有:
    • CONSTRUCTOR:构造器的声明
    • FIELD:域声明(包括enum实例)
    • LOCAL_VARIABLE:局部变量声明
    • METHOD:方法声明
    • PACKAGE:包声明
    • PARAMETER:参数声明
    • TYPE:类、接口(包括注解类型)或enum声明

这些注解,是java已经提供的,叫做元注解;我们自定义,基本Target、Retention这两个就够了;

其它一些常见元注解

  • @Override,表示当前的方法定义将覆盖超类中的方法。
  • @Deprecated,使用了注解为它的元素编译器将发出警告,因为注解@Deprecated是不赞成使用的代码,被弃用的代码。
  • @SuppressWarnings,关闭不当编译器警告信息。

4.2 注解获取

使用时,我见到的、使用的,也就三种注解:成员注解、方法注解、方法参数注解;其它的不太清除,有兴趣的自己去了解下;

另外,在这里作者未发现方法带有Declared、未带有的,有什么区别,或者场景比较生僻;有知道可以留言

4.2.1 成员注解

这个样子的注解

	@IField("ITest-private-field")
    private String first;
复制代码

获取方式通过下面代码可以拿到注解类的对象,就可以获取注解的信息了;代码中试验过这两种方式,获取结果一致;

	Field.getAnnotation(Class)
    Field.getAnnotations()

    Field.getDeclaredAnnotation(Class)
    Field.getDeclaredAnnotations()
复制代码

4.2.2 方法参数注解

这个样子的

public void setThird(@IParam("parent_param") String third) {
    this.third = third;
}
复制代码

通过下面方法可以获取参数注解数组:一维数组大小为参数个数,相应的二维为参数中注解个数

method.getParameterAnnotations()
复制代码

4.2.3 方法注解

这个样子的注解

@IMethod("ITest-public-method")
public String getThird() {
    return third;
}
复制代码

通过下面方法可以获取方法注解;同样行为一致

Method.getAnnotation(Class);
Method.getDeclaredAnnotation(Class);

Method.getAnnotations();
Method.getDeclaredAnnotations();
复制代码

4、小结

这篇文章知识点比较琐碎,且内容也是属于死记硬背的;这个写下来,也是根据平时用的一些+一些源码如何使用+ide测试验证;希望大家在看源码中,把这些知识点串联起来,增加越来越多的运用技巧

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!