Java语言中的Class类

基本概念

在Object类中定义了以下的方法,此方法将被所有子类继承

public final Class getClass()

这个方法的返回值类型是一个Class类,此类是Java反射的源头,是实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class、interface、enum、annotation、primitive type、void)的有关信息。

  • Class本身也是一个类

  • Class对象只能由系统建立对象

  • 一个加载的类在JVM中只会有一个Class实例

  • 一个Class对象对应的是一个加载到JVM中的一个.class文件

  • 每个类的实例都会记得自己是由哪个Class实例所生成

  • 通过Class可以完整地得到一个类中的所有被加载结构

  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象。

Java中拥有Class对象的类型

在Java语言中,一切皆是对象。而对象主要分为两种,一种是普通类创建的实例对象,一种是Class类对象。每个类运行时的类型信息就是通过Class对象表示的,这个对象包含了与类有关的信息。其实Java中的实例对象就是通过Class对象来创建的,Java使用Class对象执行其RTTI(运行时类型识别,Run-Time Type Identification),多态是基于RTTI实现的。

那么在Java中哪些类型可以有Class对象呢?

  • class:外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类

  • interface:接口

  • []:数组

  • enum:枚举

  • annotation:注解@interface

  • primitive type:基本数据类型

  • void

我们用代码演示一下,这些类型的Class对象都是什么?

Class c1 = Object.class; //
Class c2 = Comparable.class; // 接口
Class c3 = String[].class; // 一维数组
Class c4 = int[][].class; // 二维数组
Class c5 = Override.class; // 注解
Class c6 = ElementType.class; // 枚举
Class c7 = Integer.class; // 基本数据类型(包装类)
Class c10 = int.class; // 基本数据类型
Class c8 = void.class; // 空类型
Class c9 = Class.class; // Class
​
System.out.println(c1); // class java.lang.Object
System.out.println(c2); // interface java.lang.Comparable
System.out.println(c3); // class [Ljava.lang.String;
System.out.println(c4); // class [[I
System.out.println(c5); // interface java.lang.Override
System.out.println(c6); // class java.lang.annotation.ElementType
System.out.println(c7); // class java.lang.Integer
System.out.println(c10);// int
System.out.println(c8); // void
System.out.println(c9); // class java.lang.Class
int[] a = new int[10];
int[] b = new int[100];
​
/*
    对于数组,只要元素类型与维度一样,就是同一个Class对象
 */
System.out.println(a.getClass()); //class [I
System.out.println(b.getClass()); //class [I
​
System.out.println(b.getClass().hashCode()); //1735600054
System.out.println(a.getClass().hashCode()); //1735600054

每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,更准确的来说,是被保存在一个同名的.class文件中。当程序中需要使用到这个类的时候(包括需要这个类的对象或引用这个类的静态变量)就通过类加载器将类加到内存中来。

Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。

获取Class对象的方式有以下三种

  1. Class.forName(“类的全限定名”)

  2. 实例对象.getClass()

  3. 类名.class (类字面常量)

Class.forName 和 实例对象.getClass()

Class.forName方法是Class类的一个静态成员。forName在执行的过程中发现如果传入的参数类还没有被加载,那么JVM就会调用类加载器去加载这个参数类,并返回加载后的Class对象。Class对象和其他对象一样,我们可以获取并操作它的引用。在类加载的过程中,这个参数类的静态语句块会被执行。如果Class .forName找不到你要加载的类,它会抛出ClassNotFoundException异常。

静态语句块(static)在类第一次被加载的时候被执行,Class对象仅在需要的时候才会被加载。

Class.forName的好处就在于,不需要为了获得Class引用而持有该类型的对象,只要通过全限定名就可以返回该类型的一个Class引用。如果你已经有了该类型的对象,那么我们就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它返回的是表示该对象的实际类型的Class引用。

public class TestClass {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("反射.A");
        B b = new B();
​
        Class b2 = b.getClass();
    }
}
class A{
    static {
        System.out.println("My name is A");
    }
}
class B{
    static {
        System.out.println("My name is B");
    }
}
​
/*
运行结果:
    My name is A
    My name is B
*/

可以看出,A和B这两个类都有一个静态语句块,所以当使用Class.forName和new关键字在执行以后,A和B分别会加载到内存中,当这两个类加载到内存以后,其内部的静态语句块便会被执行。与此同时,当用new关键字创建对象后,对应的实际创建这个对象的类已经装载到内存中了,所以执行getClass()方法的时候,就不会再去执行类加载的操作了,而是直接从java堆中返回该类型的Class引用。

类字面常量

Java还提供了另一种方法来生成对Class对象的引用。即使用类字面常量,就像这样:Cat.class,这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且根除了对forName()方法的调用,所有也更高效。类字面量不仅可以应用于普通的类,也可以应用于接口、数组及基本数据类型。

用.class来创建对Class对象的引用时,不会自动地初始化该Class对象(这点和Class.forName方法不同)。类对象的初始化阶段被延迟到了对静态方法或者非常数静态域首次引用时才执行。

public class TestClass {
    public static void main(String[] args) throws ClassNotFoundException {
        /*
            首先,调用class字面常量获取Class对象
         */
        Class b = B.class;
        Class a = A.class;
        // 此时发现并没有输出A和B这两个类的静态语句块的内容
        // 说明此时并不会自动初始化该Class对象
​
        System.out.println(A.a);
        System.out.println(B.s);
        
        /*
           My name is A
            0
            My name is B
            lalala
        */
    }
}
class A{
    static  int a = 0;
    static {
        System.out.println("My name is A");
    }
}
class B{
    static String s = "lalala";
    static {
        System.out.println("My name is B");
    }
}

 

发现当我们调用A和B的静态成员的时候,这两个类对象被初始化了,并且是先输出了静态语句块的内容,然后才在主程序中输出了两个静态成员变量的值。接下来我们将A类中的a变量改为常量static final int a = 0;,在主程序再次调用时发现A中的静态语句块并没被打印,说明引用常数静态成员并不会使类对象初始化。

也可以看出,如果仅使用.class语法来获得对类的Class引用是不会引发初始化的。但是如果使用Class.forName来产生引用,就会立即进行了初始化。

如果一个字段被static final修饰,我们称为”编译时常量“,就像A类的a字段那样,那么在调用这个字段的时候是不会对A类进行初始化的。因为被static和final修饰的字段,在编译期就把结果放入了常量池中了。但是,如果只是将一个域设置为static 或final的,还不足以确保这种行为,就如调用B的s字段后,会强制B进行类的初始化,因为s字段不是一个编译时常量。

一旦类被加载了到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个Java堆地址上的Class引用。JVM不会创建两个相同类型的Class对象。其实对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。所以在Java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性。

 

参考博客地址:

https://blog.csdn.net/dufufd/article/details/80537638

https://blog.csdn.net/BraveLoser/article/details/82500474

posted @ 2020-03-30 22:35  有心有梦  阅读(1498)  评论(0编辑  收藏  举报