Java反射的基础使用

young 558 2021-11-16

java.lang.reflect的包中,提供了反射相关的类,常用的为Field:字段Method:方法Annotation:注解Constructor:构造器,我们平常最常见到的Class类则在java.lang目录下

Field,Method,Constructor都与Class相关,可以通过Class获取到

Class

常用获取方式

class常用的获取方式有3种

  1. 类型.class

  2. 对象的getClass方法()

  3. Class.forName(className),使用此方法需传入对象的全限定类名

    package org.example;
    
    public class Test{}
    
    // 类型.class
    Class<Test> clazz = Test.class;
    // 对象的getClass方法
    Test test = new Test();
    Class<? extends Test> clazz = test.getClass();
    // Class.forName方法,此方法会抛出ClassNotFoundException
    Class<Test> clazz = (Class<Test>)Class.forName("org.example.Test");
    

常用方法

  • isPrimitive 判断是否为基本类型

    String.class.isPrimitive(); // false
    int.class.isPrimitive();	// true
    Integer.class.isPrimitive();// false
    boolean.class.isPrimitive();// true
    Boolean.class.isPrimitive();// false
    short.class.isPrimitive();	// true
    Short.class.isPrimitive();	// false
    Void.class.isPrimitive();	// false
    void.class.isPrimitive();	// true
    
  • getName 获取class全限定类名

  • getSimpleName 获取类名

  • getSuperClass 获取父类,Object.class调用此方法返回null

    public static class A{}
    public static class B extends A{}
    
    
    Class<? super B> superclass = B.class.getSuperclass(); 
    System.out.println(superclass); // A
    Class<? super B> superclass1 = superclass.getSuperclass(); 
    System.out.println(superclass1);// java.lang.Object
    Class<? super B> superclass2 = superclass1.getSuperclass(); 
    System.out.println(superclass2);// null
    
  • isAssignableFrom(Class clazz) 判断当前类是否为指定类的父类

    System.out.println(A.class.isAssignableFrom(B.class)); // true
    System.out.println(Object.class.isAssignableFrom(B.class)); //true
    
  • getClassLoader 获取类加载器

  • isEnum 当前类是否是枚举类型

  • isInterface() 当前类是否为接口

  • isArray 是否是数组

  • getAnnotation(Class annotationClass) 获取类上的指定注解

  • newInstance 调用该类的无参构造函数进行对象实例化

    如果该类没有无参构造函数,只有含参构造函数,则次方法会抛出异常,此时需要使用Constructor构造器进行初始化

场景举例

Spring扫描包后,通过Class.forName方法获取Class对象

Constructor

当我们写一个类的时候,如果没有声明任何构造函数,java会帮我们生成一个无参构造函数,如果我们自己声明了构造函数,则Java不会帮我们生成无参构造,无论我们自己声明的构造函数是含参的还是无参的,所以在声明了含参构造的函数之后,我们还想使用无参构造函数,则需要手动声明

我们可以通过Class来获取类的构造函数

public class A{
    public A(){}
    public A(String name){}
    private A(Integer age){}
}
// 获取所有构造函数
Constructor<?>[] constructors = A.class.getConstructors();
System.out.println(constructors.length);
// 获取参数为String类型的构造函数
Constructor<A> constructor = A.class.getConstructor(String.class);
// 获取参数为Integer类型的构造函数
Constructor<A> constructor1 = A.class.getConstructor(Integer.class);

执行上述方法,我们可以看到constructors.length=2,并且会抛出异常Exception in thread "main" java.lang.NoSuchMethodException: A.<init>(java.lang.Integer)

找不到参数为Integer的构造方法,这是因为getConstructors方法和getConstructor方法只能拿到访问控制域为public的构造函数

如果需要获取所有访问控制域的构造函数,需要使用getDeclaredConstructorsgetDeclaredConstructor

将上面的代码进行修改

Constructor<?>[] constructors = A.class.getDeclaredConstructors();
System.out.println(constructors.length);
Constructor<A> constructor = A.class.getDeclaredConstructor(String.class);
Constructor<A> constructor1 = A.class.getDeclaredConstructor(Integer.class);

此时constructors.length打印出来的值为3,并且也没有报错信息,说明此时我们已经获取到了所有的构造函数

接下来就可以使用获取到的构造函数创建实例了

// 无参构造函数
A a = A.class.getDeclaredConstructor().newInstance();
A a1 = A.class.getDeclaredConstructor(String.class).newInstance("1");
A a2 = A.class.getDeclaredConstructor(Integer.class).newInstance(1);

执行代码,此时控制台会抛出异常Exception in thread "main" java.lang.IllegalAccessException: Class TestMain can not access a member of class A with modifiers "private"

告诉我们不能调用private的构造函数,正常情况下,我们是不能调用非本类的私有方法的,但是用过反射,我们只需要关闭访问检查,便可以对这些本无法访问的方法或者属性进行调用或者访问

Constructor<A> declaredConstructor = A.class.getDeclaredConstructor(Integer.class);
declaredConstructor.setAccessible(true);
A a2 = declaredConstructor.newInstance(1);

setAccessible方法描述为值为true表示反射对象在使用时应禁止Java语言访问检查。 值为false表示反射对象应强制执行Java语言访问检查。

此时再去调用方法,即可创建对应的实例,给A类加上对应的属性,进行测试

@Data
public static class A{
    public A(){}
    public A(String name){this.name=name;}
    private A(Integer age){this.age=age;}

    private int age;
    private String name;
}

A a = A.class.getDeclaredConstructor().newInstance();
A a1 = A.class.getDeclaredConstructor(String.class).newInstance("1");
Constructor<A> declaredConstructor = A.class.getDeclaredConstructor(Integer.class);
declaredConstructor.setAccessible(true);
A a2 = declaredConstructor.newInstance(1);
System.out.println(a); // A(age=0, name=null)
System.out.println(a1);// A(age=0, name=1)
System.out.println(a2);// A(age=1, name=null)

从结果可以看到,我们通过反射调用构造函数进行对象实例化已经成功了

常用方法

  • newInstance 初始化对象
  • getAnnotation 获取构造函数上的注解
  • getParameterTypes 获取构造函数的参数类型数组

场景举例

Spring初始化对象加载到容器中时,通过Class获取到构造方法,并对其进行初始化

Field

通过Class对象,我们可以获取对象中的字段信息,获取属性的方法有4个

  1. getField(String name)
  2. getFields()
  3. getDeclaredField(String name)
  4. getDeclaredFields()

和上面的Constructor一样,不带Declared的方法,只能获取public修饰的字段,如需访问非public字段,应禁止访问权限检查。

获取指定字段

Field nameField = A.class.getField("name");

通过字段名称即可获取Field对象

如果指定的字段名称不存在,会抛出NoSuchFieldException异常

比如执行A.class.getDeclaredField("addr") ,则会抛出异常Exception in thread "main" java.lang.NoSuchFieldException: addr

获取全部字段

Field[] declaredFields = A.class.getDeclaredFields();
for (Field declaredField : declaredFields) {
    System.out.println(declaredField.getName());
}

这样可以获取到全部的字段,但是这个方法只能获取当前类的字段,也就说,如果A类存在父类,父类的字段是无法获取到的

@Data
public class B {
    private String address;
}

@Data
public class A extends B {
    private String name;
    private Integer age;
}

Field[] declaredFields = A.class.getDeclaredFields();
for (Field declaredField : declaredFields) {
    System.out.println(declaredField.getName());
}

从输出结果来看,只能拿到当前类的字段name和age,父类的字段address并不能获取,如需获取父类的字段,可调用A.class.getSuperClass(),通过父类的Class获取需要的字段

Class<A> clazz = A.class;
List<Field> fields = new LinkedList<>();
Class superClass = null;
do {
    fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
    superClass = clazz.getSuperclass();
}while (superClass!=null);

常用方法

  • getType 获取字段类型
  • getAnnotation 获取字段上的注解
  • getName 获取字段名称
  • get 获取字段的值
  • set 给字段赋值

获取字段值

调用get(Object obj)方法即可,Obj为对象实例,即表示获取obj的中该field的值

A a1 = new A();
a1.setName("1");
A a2 = new A();
a2.setName("2");
Field nameField = A.class.getDeclaredField("name");
nameField.setAccessible(true);
System.out.println(nameField.get(a1)); // 1
System.out.println(nameField.get(a2)); // 2

获取静态字段的值

静态字段只类有关,与实例无关,故获取静态字段的值时,可以将obj设置为null

@Data
public class A extends B {
    private static String STATIC_FIELD = "static";
    private String name;
    private Integer age;
}


Field staticField = A.class.getDeclaredField("STATIC_FIELD");
staticField.setAccessible(true);
System.out.println(staticField.get(null)); // static

给字段赋值

调用set(Object obj,Object value)方法即可

表示给obj实例的field赋值

Field staticField = A.class.getDeclaredField("age");
staticField.setAccessible(true);
A a1 = new A();
A a2 = new A();
staticField.set(a1,10);
staticField.set(a2,10);
System.out.println(a1); // A(name=null, age=10)
System.out.println(a2); // A(name=null, age=20)

如果赋值的value类型与field的type类型不符,会抛出IllegalArgumentException异常

Field staticField = A.class.getDeclaredField("age");
staticField.setAccessible(true);
A a = new A();
staticField.set(a,"young");

此时会抛出异常Exception in thread "main" java.lang.IllegalArgumentException: Can not set java.lang.Integer field com.example.jsch.A.age to java.lang.String

场景举例

  • BeanCopy
  • Spring基于字段的依赖注入

Method

通过Class对象,我们可以获取当前类中方法信息

  • getMethod
  • getMethods
  • getDeclaredMethod
  • getDeclaredMethods

因为方法重载的原因,获取指定的方法时,除了需要指定获取方法名称外,还要指定方法的参数,如果找不到对应的方法,会抛出NoSuchMethodException

常用方法

  • invoke 执行方法
  • getAnnotation 获取方法上的注解
  • getParameterTypes 获取方法参数类型列表
  • getParameterAnnotations 获取方法参数上的列表
  • getReturnType 获取返回值类型

invoke方法有返回值,当返回类型为void时,invoke返回null

A a = new A();
a.setName("young");
Method getNameMethod = A.class.getDeclaredMethod("getName");
Object value = getNameMethod.invoke(a);
System.out.println(value);	// young
System.out.println(a);		// A(name=young, age=null)

Method setNameMethod = A.class.getDeclaredMethod("setName", String.class);
System.out.println(setNameMethod.getReturnType());	// void
value = setNameMethod.invoke(a, "hahaha");			
System.out.println(value);	// null
System.out.println(a);		// A(name=hahaha, age=null)

场景举例

  • BeanCopy 在某些场景下,set方法或者get方法会对值进行一些处理操作

Annotation

Java内置注解

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
  • @Retention - 标识这个注解作用场景,源码期,编译期,运行期,如果想通过反射获取到该注解,应使使注解的作用场景为运行期。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

注解的作用范围

可通过注解上的@Target 注解的属性查看

  • TYPE 类、接口、枚举
  • FIELD 字段、枚举常量
  • METHOD 方法
  • PARAMETER 参数
  • CONSTRUCTOR 构造函数
  • LOCAL_VARIABLE 局部变量
  • ANNOTATION_TYPE 注解
  • PACKAGE

注解的生效时期

可通过注解上@Retention注解的属性查看

  • SOURCE 编译器
  • CLASS 编译之后,记录在class中,不会加载到JVM中
  • RUNTIME 运行期,会加载到JVM中,可以通过反射获取到