Java反射和注解

133 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

Java反射和注解

注意: 本章节涉及到JVM相关底层原理,难度会有一些大。

反射就是把Java类中的各个成分映射成一个个的Java对象。即在运行状态中,对于任意一个类,都能够知道这个类所有的属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性。这种动态获取信息及动态调用对象方法的功能叫Java的反射机制。

简而言之,我们可以通过反射机制,获取到类的一些属性,包括类里面有哪些字段,有哪些方法,继承自哪个类,甚至还能获取到泛型!它的权限非常高,慎重使用!

Java类加载机制

在学习Java的反射机制之前,我们需要先了解一下类的加载机制,一个类是如何被加载和使用的:

图片.png

在Java程序启动时,JVM会将一部分类(class文件)先加载(并不是所有的类都会在一开始加载),通过ClassLoader将类加载,在加载过程中,会将类的信息提取出来(存放在元空间中,JDK1.8之前存放在永久代),同时也会生成一个Class对象存放在内存(堆内存),注意此Class对象只会存在一个,与加载的类唯一对应!

思考: 既然说和与加载的类唯一对应,那如果我们手动创建一个与JDK包名一样,同时类名也保持一致,那么JVM会加载这个类吗?

package java.lang;

public class String {    //JDK提供的String类也是
    public static void main(String[] args) {
        System.out.println("我姓,我叫");
    }
}

我们发现,会出现以下报错:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)

但是我们明明在自己写的String类中定义了main方法啊,为什么会找不到此方法呢?实际上这是ClassLoader的双亲委派机制在保护Java程序的正常运行:

图片.png

实际上我们的类最开始是由BootstarpClassLoader进行加载,BootstarpClassLoader用于加载JDK提供的类,而我们自己编写的类实际上是AppClassLoader,只有BootstarpClassLoader都没有加载的类,才会让AppClassLoader来加载,因此我们自己编写的同名包同名类不会被加载,而实际要去启动的是真正的String类,也就自然找不到main方法了!

public class Main {
    public static void main(String[] args) {
        System.out.println(Main.class.getClassLoader());   //查看当前类的类加载器
        System.out.println(Main.class.getClassLoader().getParent());  //父加载器
        System.out.println(Main.class.getClassLoader().getParent().getParent());  //爷爷加载器
        System.out.println(String.class.getClassLoader());   //String类的加载器
    }
}

由于BootstarpClassLoader是C++编写的,我们在Java中是获取不到的。

Class对象

通过前面,我们了解了类的加载,同时会提取一个类的信息生成Class对象存放在内存中,而反射机制其实就是利用这些存放的类信息,来获取类的信息和操作类。那么如何获取到每个类对应的Class对象呢,我们可以通过以下方式:

public static void main(String[] args) throws ClassNotFoundException {
    Class<String> clazz = String.class;   //使用class关键字,通过类名获取
    Class<?> clazz2 = Class.forName("java.lang.String");   //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
    Class<?> clazz3 = new String("cpdd").getClass();  //通过实例对象获取
}

注意Class类也是一个泛型类,只有第一种方法,能够直接获取到对应类型的Class对象,而以下两种方法使用了?通配符作为返回值,但是实际上都和第一个返回的是同一个对象:

Class<String> clazz = String.class;   //使用class关键字,通过类名获取
Class<?> clazz2 = Class.forName("java.lang.String");   //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
Class<?> clazz3 = new String("cpdd").getClass();

System.out.println(clazz == clazz2);
System.out.println(clazz == clazz3);

通过比较,验证了我们一开始的结论,在JVM中每个类始终只存在一个Class对象,无论通过什么方法获取,都是一样的。现在我们再来看看这个问题:

public static void main(String[] args) {
    Class<?> clazz = int.class;   //基本数据类型有Class对象吗?
    System.out.println(clazz);
}

迷了,不是每个类才有Class对象吗,基本数据类型又不是类,这也行吗?实际上,基本数据类型也有对应的Class对象(反射操作可能需要用到),而且我们不仅可以通过class关键字获取,其实本质上是定义在对应的包装类中的:

/**
 * The {@code Class} instance representing the primitive type
 * {@code int}.
 *
 * @since   JDK1.1
 */
@SuppressWarnings("unchecked")
public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

/*
 * Return the Virtual Machine's Class object for the named
 * primitive type
 */
static native Class<?> getPrimitiveClass(String name);   //C++实现,并非Java定义

每个包装类中(包括Void),都有一个获取原始类型Class方法,注意,getPrimitiveClass获取的是原始类型,并不是包装类型,只是可以使用包装类来表示。

public static void main(String[] args) {
    Class<?> clazz = int.class;
    System.out.println(Integer.TYPE == int.class);
}

通过对比,我们发现实际上包装类型都有一个TYPE,其实也就是基本类型的Class,那么包装类的Class和基本类的Class一样吗?

public static void main(String[] args) {
    System.out.println(Integer.TYPE == Integer.class);
}

我们发现,包装类型的Class对象并不是基本类型Class对象。数组类型也是一种类型,只是编程不可见,因此我们可以直接获取数组的Class对象:

public static void main(String[] args) {
    Class<String[]> clazz = String[].class;
    System.out.println(clazz.getName());  //获取类名称(得到的是包名+类名的完整名称)
    System.out.println(clazz.getSimpleName());
    System.out.println(clazz.getTypeName());
    System.out.println(clazz.getClassLoader());   //获取它的类加载器
    System.out.println(clazz.cast(new Integer("10")));   //强制类型转换
}

再谈instanceof

正常情况下,我们使用instanceof进行类型比较:

public static void main(String[] args) {
    String str = "";
    System.out.println(str instanceof String);
}

它可以判断一个对象是否为此接口或是类的实现或是子类,而现在我们有了更多的方式去判断类型:

public static void main(String[] args) {
    String str = "";
    System.out.println(str.getClass() == String.class);   //直接判断是否为这个类型
}

如果需要判断是否为子类或是接口/抽象类的实现,我们可以使用asSubClass()方法:

public static void main(String[] args) {
    Integer i = 10;
    i.getClass().asSubclass(Number.class);   //当Integer不是Number的子类时,会产生异常
}

获取父类信息

通过getSuperclass()方法,我们可以获取到父类的Class对象:

public static void main(String[] args) {
    Integer i = 10;
    System.out.println(i.getClass().getSuperclass());
}

也可以通过getGenericSuperclass()获取父类的原始类型的Type:

public static void main(String[] args) {
    Integer i = 10;
    Type type = i.getClass().getGenericSuperclass();
    System.out.println(type);
    System.out.println(type instanceof Class);
}

我们发现Type实际上是Class类的父接口,但是获取到的Type的实现并不一定是Class。

同理,我们也可以像上面这样获取父接口:

public static void main(String[] args) {
    Integer i = 10;
    for (Class<?> anInterface : i.getClass().getInterfaces()) {
        System.out.println(anInterface.getName());
    }
  
    for (Type genericInterface : i.getClass().getGenericInterfaces()) {
        System.out.println(genericInterface.getTypeName());
    }
}