详解Java反射操作

在java开发过程中,经常使用一些框架如Spring、SpringMvc来简化开发操作,在web开发中,我们可以利用
SpringMvc快速的把请求参数和实体进行转换,在使用数据库操作的时候,可以使用Mybatis/hibernate等框架
实现实体与db的转换操作,这其中就是利用了java的反射机制实现的操作,所以java进阶之旅一定会有反射一
席之地,下面让我们详细学习一下Java反射相关的类与方法

反射适合用在哪

首先我们先思考一个问题,反射适合使用在哪里呢?从功能上看,反射似乎无所不能,几乎所有的类,所有的属性、方法、构造我们都能使用,但是我们细细思考一下,在实际开发中,并不是所有场景都需要使用反射获取属性或者方法进行操作,反而更多的使用实例.xxx方式操作,而当这些操作重复次数较多的时候,我们往往会考虑优化代码,减少代码冗余,提高复用,比如实体构建赋值等操作,这个时候往往是我们最需要复用的地方,所以我们可以大体认为反射主要使用在实体操作过程中。而在一般操作数据的过程中,我们的实体一般都是知道并且依赖于对应的数据类型的,比如:

1.根据类型new的方式创建对象

2.根据类型定义变量,类型可能是基本类型也可能是引用类型、类或者接口

3.将对应类型的对象传递给方法

4.根据类型访问对象的属性,调用方法等操作

以上这些操作都是数据操作的过程中最常见也是最难复用优化的地方,而如果这里的操作使用反射则可以实现动态的操作不同的类型的实例,通过调用反射入口类Class,获取对应的属性、构造、方法完成对应的操作,所以接下来我们先从Class入口开始学习

获取Class

学习过类和继承实现原理的时候,我们都知道,每个已经加载的类在内存中都有一份对应的类信息,而每个对象都有所属类的引用。其中类信息存放的类即为java.lang.Class。而所有类的基类Object中就有一个本地方法可以快速获取到Class对象

public final native Class<?> getClass()

可以看到返回的类型为Class<?>,即为当前类的信息,但是由于基类的子类并不明确,所以具体的类型这里使用范型的方式返回

而getClass方法不仅仅可以使用在类中,针对于接口也可以使用,当然接口没有具体的实现,所以不可能是接口的实例.getClass的方式获取,这个时候我们就需要调用内置的.class属性快速获取类型:

Class<Comparable> cls = Comparable.class;

而在java中有一些特殊的类型,例如基本类型,还有void类型,这种类型无法直接创建实例,如果要获取class,与接口的方式相同,如下:

//这里可以看出来,基本类型的class即为对应的包装类型
Class<Integer> intCls = int.class;
Class<Byte> byteCls = byte.class;
Class<Character> charCls = char.class;
Class<Double> doubleCls = double.class;
//void也是一种特殊的类型,返回的也是对应的包装类Void
Class<Void> voidCls = void.class;

而数组和枚举作为java中的特殊实现类,获取的class类型也是较为特殊的,数组具有维度特性,所以获取的class类型同样具有维度,即一维数组有一个,二维数组有两个,每个维度都有对应的不同类型,而枚举的class则是其中定义的每一个子集,如下:

String[] strArr = new String[10];
int[][] twoDimArr = new int[3][2];
int[] oneDimArr = new int[10];
Class<? extends String[]> strArrCls = strArr.getClass();
Class<? extends int[][]> twoDimArrCls = twoDimArr.getClass();
Class<? extends int[]> oneDimArrCls = oneDimArr.getClass();

而通过上述方式获取Class对象以后,我们就可以了解到关于类型的很多信息,并且基于这些信息可以获取我们想要的详细信息,大致可以分为以下几类,包括名称信息、字段信息、方法信息、创建对象的方法、构造信息与类型信息等,接下来我们就分别学习这几类信息相关的内容

Class名称信息

我们在开发过程中,获取到Class以后,往往需要获取对应的类名进行操作,比如比较类名,排除类名等操作,而在Class类中,提供了以下几个方法获取类名称相关信息:

public String getName()
public String getSimpleName()
public String getCanonicalName()
public Package getPackage()

这里可以看出分别能获取四种不同的类名称,那么获取的具体内容有什么不同呢?我们先看一张罗列的表格信息:


class名称列表.png

可以看出来各个不同类型的Class通过四种方法获取的类名称信息完全不同,那么为什么会出现这么大的区别呢?这里我们需要注意的是:

getName()

getName()方法获取的是标准的类名称信息,即Java内部真正的类型对应的名称信息(JVM真实的类名称信息),这里我们可以看出来数组的getName()的结果为[I,这里需要解释一下,[表示的是数组,并且和维度有关系,如果是二维数组,那么这里就会是[[,而后面的I则是int类型在JAVA中真实的类型的简写,八大基本类型对应的类简写名称如下所示:

基本类型 真实类名称简写
boolean Z
byte B
char C
double D
float F
int I
long J
short S

而这里还有一点需要注意的是,如果是引用类型对象的数组,Class的真实类名结尾还会有一个分号;

getSimpleName()

getSimpleName()方法是jdk实现的用来快速获取当前类的真实类名的路径缩写,即不带包名的Class名称

getCanonicalName()

getCanonicalName()方法获取到的即为java中的完整伪类名,即包名+getSimpleName()名称的完整名称,此方法获取的Class名称比较友好

getPackage()

getPackage()则是java中默认实现的可以用来快速获取当前Class所在包名的方法,此方法仅仅返回类路径的前置(包名),不包含类名

Class字段信息

获取完Class的名称信息以后,我们开始关注如何通过Class类获取类属性信息。我们知道在类中定义的静态和实例变量都被称为字段,在Class类中则是使用Field表示,位于java.lang.reflect包下,而在Class类中有如下方法可以获取Field信息:

public Field[] getFields()
public Field[] getDeclaredFields()
public Field getField(String name)
public Field getDeclaredField(String name)

可以看出来主要是分为两类,一类返回的是字段数组,一类返回具体的字段,分别看下这两类方法的作用:

getFields()/getDeclaredFields()

getFields()/getDeclaredFields()方法都是返回的当前Class中所有的字段,其中包括来自父类的,但是这两个方法在使用的时候有一定的区别,getFields()方法只能返回非私有的字段,而getDeclaredFields()则是返回所有的字段,包括私有的字段,当然还需要借助setAccessible方法才能实现

getField(String name)/getDeclaredField(String name)

getField(String name)/getDeclaredField(String name)这两个方法都是通过字段名来获取对应的字段信息,通过命名我们不难发现,和上一组类似,getField(String name)方法只能从所有的非私有的字段中查找当前名称的字段信息,getDeclaredField(String name)则是从所有的字段中查找对应名称的字段信息

操作Field的常见方法

通过上述的四个方法我们可以轻松获取到Class中对应的字段信息,接下来我们只要对这些信息进行操作处理,即可完成我们要做的操作,而常用的操作Field的方法如下:

public String getName()
public boolean isAccessible()
public void setAccessible(boolean flag)
public Object get(Object obj)
public void set(Object obj, Object value)

getName()

getName()方法通过命名即可看出来,此方法可以获取到当前Field的字段名

isAccessible()

isAccessible()方法我们在上述getDeclaredFields()方法的时候曾经介绍过,如果需要获取私有字段,需要setAccessible方法支持,此方法则是可以获取是否获取到setAccessible方法的支持,即是否支持获取私有字段

setAccessible(boolean flag)

setAccessible(boolean flag)则是通过设置boolean值确认当前反射获取Field的操作中,是否检查私有字段,设置为true,则不检查,反射可以获取到私有Field,设置为false则是检查私有字段,反射不可获取私有Field

get(Object obj)/set(Object obj, Object value)

get(Object obj)/set(Object obj, Object value)方法我们都不陌生,这一对方法则是能对当前Field设置对应的值/获取对应的值

其他方法

除了上述常见的方法以外,开发过程中可能还会使用到一些其他操作Field的方法,搭配使用,可以实现更灵活的字段操作,方法如下:

//返回当前字段的修饰符--public、private等
public int getModifiers()
//返回当前字段的类型--String等
public Class<?> getType()
//通过当前方法获取/赋值基础类型的字段值
public void setBoolean(Object obj, boolean z)
public boolean getBoolean(Object obj)
public void setDouble(Object obj, double d)
public double getDouble(Object obj)
//获取当前字段上的注解,使用jpa或者mybatis-plus等框架的时候,会添加在字段上一些注解
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
public Annotation[] getDeclaredAnnotations()

Class方法信息

获取完Field字段信息以后,往往我们还需要进行方法的操作,比如调用xx方法实现部分功能,这个时候就需要获取方法信息,而在Class中提供了很多操作方法信息的方法,常见的如下:

//获取所有的非私有方法,包括父类的非私有方法
public Method[] getMethods()
//获取所有方法,包括私有方法和父类的非私有方法
public Method[] getDeclaredMethods()
//从当前Class的所有public方法中查找对应名称,并且参数列表相同的方法(包括父类非私有方法)
//如果查找不到会抛出NoSuchMethodException异常
public Method getMethod(String name, Class<?>... parameterTypes)
//从当前Class的所有方法包括父类的非私有方法中查找对应名称并且参数列表相同的方法,如果查找不到会抛出NoSuchMethodException异常
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)

而当我们通过上述方法获取到Method对象后,即可操作此对象完成Method方法调用等,而Method信息包含如下内容:

//获取当前Method的名称
public String getName()
//是否忽略检查机制,允许调用私有的Method,如果设置为true,则忽略检查,允许调用私有方法,设置false则使用检查机制,不允许操作私有方法
public void setAccessible(boolean flag)
//调用指定obj实例对象的当前方法,并且依据参数调用正确的方法
public Object invoke(Object obj, Object... args) throws
IllegalAccessException, Illegal-ArgumentException, InvocationTargetException

反射创建实例与构造方法

当我们拿到了字段信息和方法信息以后,这儿时候我们基本已经可以操作这些完成很多常见的功能了,但是除此之外,日常开发中还会遇到构造实例的频繁操作,如果能反射创建实例就好了,所以Class中提供了构建实例的方法,并且提供了操作Class的构造方法,如下:

//获取当前Class的所有public构造方法列表
public Constructor<?>[] getConstructors()
//获取当前Class中所有构造方法的列表,包含private的构造
public Constructor<?>[] getDeclaredConstructors()
//根据参数列表查找当前符合的非私有构造方法,不满足的情况抛出NoSuchMethodException异常
public Constructor<T> getConstructor(Class<?>... parameterTypes)
//根据参数列表查找当前所有构造中符合的方法,不满足的情况抛出NoSuchMethodException异常
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

通过调用获取的构造方法可以完成类实例的加载构建,同样的我们也可以使用如下方法快速构建类实例:

public T newInstance() throws InstantiationException, IllegalAccessException

举个简单的例子:

Map<String,Integer> map = HashMap.class.newInstance();//通过Class<?>.newInstance构建类实例
map.put("hello", 123);

Class的类型信息与声明信息

通过上述的学习,我们已经了解到Class类是个神奇的存在,既可以获取名称,也可以操作字段和方法,那么我们不禁疑惑,Class代表的类型到底是普通的类,还是内部类,还是基础类型或者数组呢?其实Class是一个类型的集合,每一个Class的类型取决于原类型,在Class方法中也提供了如下方法辅助判断Class的类型信息,如下:

//Class类型是否为数组
public native boolean isArray() 
////Class类型是否为基本数据类型--包装类
public native boolean isPrimitive() 
//Class类型是否为接口
public native boolean isInterface()
//Class类型是否为枚举
public boolean isEnum() 
//Class类型是否为注解类型
public boolean isAnnotation() 
//Class类型是否为匿名内部类
public boolean isAnonymousClass() 
//Class类型是否为成员类,定义在方法外的Class
public boolean isMemberClass()
//Class类型是否为本地类,即定义在方法内的Class,非匿名内部类
public boolean isLocalClass() 

除了Class本身的类型信息,我们也可以根据以下方法获取Class的声明信息、父类、接口等信息,方法如下:

//获取当前类的修饰符
public native int getModifiers()
//获取当前类的父类型信息
public native Class<? super T> getSuperclass()
//获取当前类实现的所有的接口信息
public native Class<?>[] getInterfaces();
//获取当前类申明的注解信息数组
public Annotation[] getDeclaredAnnotations()
//获取当前类中所有的注解信息
public Annotation[] getAnnotations()
//根据注解的完整类名,查找到对应的注解信息
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)

数组与枚举的反射

在使用反射过程中,除了日常的操作以外,有些时候我们还需要针对数组和枚举类型的Class做一些反射操作,而数组类型的操作往往需要借助java.lang.reflect包下的Array类操作完成,主要方法如下:

//创建元素类型、元素长度指定的数组
public static Object newInstance(Class<?> componentType, int length)
//创建多维度的数组,dimensions可连续传递多个,分别代表不同维度
public static Object newInstance(Class<?> componentType, int... dimensions)
//获取指定数组的对应索引的值
public static native Object get(Object array, int index)
//赋值给指定数组的对应索引下的值
public static native void set(Object array, int index, Object value)
//获取数组长度
public static native int getLength(Object array)

需要注意的是,在Array类中,数组使用Object而不是Object[]表示,这是为了方便处理多种类型的数组而设计的,因为在java中int[]、String[]等数组都不可以与Object[]相互转化,但是却可以转为Object,例如:

int[] intArr = (int[])Array.newInstance(int.class, 10);
String[] strArr = (String[])Array.newInstance(String.class, 10);

除了数组类型,在开发中,尤其是遇到固定常量类型的时候,往往选择使用枚举类来实现操作,但是在反射中,当我们需要查找枚举类型的时候,Class类提供了如下方法获取我们枚举类中定义的所有的常量,从而可以实现枚举相关的反射操作

//获取当前枚举类型Class的所有定义的枚举常量
public T[] getEnumConstants()

推荐阅读更多精彩内容

  • 1. 简介 定义:Java语言中 一种动态(运行时)访问、检测 & 修改它本身的能力 作用:动态(运行时)获取类的...
    lihan2734阅读 298评论 0 0
  • 类加载机制 1 什么是反射 Java反射机制是在运行状态中对于任意一个类,都能知道这个类的所以属性和方法;对于任何...
    凯玲之恋阅读 7,308评论 2 20
  • 其他更多java基础文章:java基础学习(目录) 深入理解Class对象 RRIT及Class对象的概念 RRI...
    Hiwayz阅读 680评论 0 18
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李头阅读 2,388评论 2 11
  • 刚准备日更,儿子打电话回来,明天要学考,要回家拿身份证,和以往一样,我和孩他爸立刻动身。 从家到学校大概二十几分钟...
    舒心的话阅读 1,246评论 22 55