Java编程思想 - Java反射机制(一)Class对象、类加载器、泛化的Class引用、cast()转型方法

  1. RTTI含义:运行时类型信息
  2. Class对象和类加载器
    Class对象:包含了与类有关的信息,每一个类都有一个Class对象
    类加载器:类加载器是Java虚拟机(JVM)的一个子系统,所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创 建一个对类的静态引用时,就会加载这个类。因此证明了构造器也是类的静态方法,用new操作费创建类的新对象也会被当做对类的静态成员引用。类加载时,类加载器首先检查类的Class对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找.class文件。static初始化是在类加载时进行的。
  3. 获取类的Class对象的方式
    (1)Class 对象名 = Class.forName(“包名.类名”); 需要检查异常(用try-catch包围或throws抛出异常),并会初始化类
    (2)Class 对象名 = 类名.class; 无需检查异常,不会进行初始化,比类名.forName方式更高效
    (3)Class 对象名 = 类名.getClass();
    (4)通过ClassLoader加载,ClassLoader.getSystemClassLoader().loadClass(“包名.类名”); 没有对类进行初始化,只是把类加载到了虚拟机中
  4. 使用类时准备工作三个步骤
    (1)加载:由类加载器执行,查找字节码(通常在classpath所指定的路径中查找),并从这些字节码中创建一个Class对象、
    (2)链接:在链接阶段将验证类中的字节码,为静态域分配存储空间,如果必需的话,将解析这个类创建对其它类的所有引用。
    (3)初始化:如果该类具有超类,则对其进行初始化,执行静态初始化器(如构造方法)和静态初始化块。
    使用.class方式获取类的Class对象时。初始化操作被延迟到对静态方法(构造器也是隐式地是静态的)或非常数静态域(需要运行时才能确定的静态域)进行首次调用时才执行
import java.util.Random;

class Initable {
	static final int staticFinal = 47;
	static final int staicFinal2 = Test1.rand.nextInt(1000);
	static {
		System.out.println("Initializing Initable");
	}
}

class Initable2 {
	static int staticFinal = 147;
	static {
		System.out.println("Initializing Initable2");
	}
}

class Initable3 {
	static final int staticFinal = 74;
	static {
		System.out.println("Initializing Initable3");
	}
}

public class Test1 {
	public static Random rand = new Random(47);
	public static void main(String[] args) {
		Class initable = Initable.class;
		System.out.println("----1----");
		System.out.println(Initable.staticFinal);
		System.out.println(Initable.staicFinal2);
		
		System.out.println("----2----");
		System.out.println(Initable2.staticFinal);
		
		System.out.println("----3----");
		try {
			Class initable3 = Class.forName("test1.test1.Initable3");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		System.out.println(Initable3.staticFinal);
	}
}

运行结果:
Java编程思想 - Java反射机制(一)Class对象、类加载器、泛化的Class引用、cast()转型方法

可以看出,初始化有效的实现了尽可能的“惰性”。仅使用.class获得对类的引用不会引发初始化。而用Class.forName()就立即进行了初始化。(由于源码中默认设置了是否初始化为true,所以会进行初始化操作)

  1. 泛化的Class引用
      Class引用总是指向某个Class对象,可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。
      使用泛型语法,是通过允许你对Class引用所指的Class对象进行限定而实现的。通过使用泛型语法,可以让编译器强制执行额外的类型检查。   
      向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查。
public class GenericClassReferences {
	public static void main(String[] args) {
		Class intClass = int.class;
		Class<Integer> genericIntClass = int.class;
		genericIntClass = intClass;
		intClass = double.class;
		//genericIntClass = intClasss;  不能将double的Class引用赋值给使用了Integer泛型的Class引用;
	}
}
  1. 加入继承关系的泛化Class引用
 Class<Number> genericNumberClass = int.class;

    虽然Numbe是Integer的父类,但是这行代码是错误的,因为Number类的Class对象不是Integer类的Class对象的父类,我的理解是这两个实质是Class类的两个不同对象,表示的是两个不同的类。
    我们需要使用通配符“?”来表示泛型。“?”表示任何事物。Class<?>和Class是等价的,但前者编译器不会有警告信息。它配合extends或super关键字表示一个范围,分别是:
    (1)Class<? extends 类名> //继承了指定类的类,即指定类型的子类。
    (2)Class<? super 类名> //指定类型的父类
    如果使用了泛型的Class对象,在调用newInstance()方法时,当泛型为指定的确切类型时,会返回该指定类型的对象,而不是Object类型;当泛型为一个范围时,如果上面说的<? super 类名>,返回的是Object类型,而不是确切的类型。我的理解是不确定是哪一个父类类型,可能是父类也可能是祖父类,有含糊性。

  1. cast()转型方法
    cast()方法接受参数对象,并将其转型为Class引用的类型。与普通的类型转换相比,新的转型语法对于无法使用普通转型的情况显得非常有用,在编写泛型代码时,如果存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。
class Building{}
class House extends Building{}

public class ClassCast {
	public static void main(String[] args) {
		Building build = new Building();
		Class<House> houseType = House.class;  //获取Class对象,用于确定需要转换成什么类型
		House house = houseType.cast(build);   //通过houseType的cast(Object b)方法将传入的build对象转换为Hose类型的对象
		
		house = (House)build;  //普通的转型方法
	}
}

源码拓展:

  1. 调用 Class.forName(String className)方法获得Class对象时,会执行类初始化的原因。
    Class.forName(String className);这个方法的源码是
    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

    其中,forName0方法是调用了private static native Class<?> forName0(String name, boolean initialize,ClassLoader loader,Class<?> caller)本地方法,其中第二个参数即为设置是否对类进行初始化的参数。 当调用Class.forName方法时,源码中默认设置为true,所以加载类时会进行初始化。
    当需要设置不初始化时,可以调用forName(String name, boolean initialize,ClassLoader loader)来设置参数,true表示执行初始化。

    Spring框架中的IOC的实现使用的是ClassLoader(没有对类进行初始化,只是把类加载到了虚拟机中)。
    而在我们使用JDBC时通常是使用Class.forName()方法来加载数据库连接驱动。这是因为在JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己。(Class.forName(“com.mysql.jdbc.Driver”))

public class Driver extends NonRegisteringDriver implements java.sql.Driver {  
    // ~ Static fields/initializers  
    // ---------------------------------------------  
  
    //  
    // Register ourselves with the DriverManager  
    //  
    static {  
        try {  
            java.sql.DriverManager.registerDriver(new Driver());  
        } catch (SQLException E) {  
            throw new RuntimeException("Can't register driver!");  
        }  
    }  
  
    // ~ Constructors  
    // -----------------------------------------------------------  
  
    /** 
     * Construct a new driver and register it with DriverManager 
     *  
     * @throws SQLException 
     *             if a database error occurs. 
     */  
    public Driver() throws SQLException {  
        // Required for Class.forName().newInstance()  
    }  
}

我们看到Driver注册到DriverManager中的操作写在了静态代码块中,这就是为什么在写JDBC时使用Class.forName()的原因了。