本文主要参考博文:https://blog.csdn.net/javazejian/article/details/70768369【csdn博主:zejian_】,主要是对文章进行自己的整理以及一点理解。

文章主要讲的是的Class对象、获取Class对象的几种方法的差异,类的加载、类型转换、以及反射类的主要用法。

其余参考文章:
https://blog.csdn.net/justloveyou_/article/details/72466105 JVM类生命周期概述:加载时机与加载过程

关于Class类与Class对象

在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中,其部分源码如下:

public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
private static final int ANNOTATION= 0x00002000;
private static final int ENUM      = 0x00004000;
private static final int SYNTHETIC = 0x00001000;

private static native void registerNatives();
static {
registerNatives();
}

/*
* Private constructor. Only the Java Virtual Machine creates Class objects.(私有构造,只能由JVM创建该类)
* This constructor is not used and prevents the default constructor being
* generated.
*/
private Class(ClassLoader loader) {
// Initialize final field for classLoader.  The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null.
classLoader = loader;
}


Class类被创建后的对象就是Class对象,注意,Class对象表示的是自己手动编写类的类型信息,比如创建一个Shapes类,那么,JVM就会创建一个Shapes对应Class类的Class对象,该Class对象保存了Shapes类相关的类型信息。实际上在Java中每个类都有一个Class对象,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象并且这个Class对象会被保存在同名.class文件里(编译后的字节码文件保存的就是Class对象)


那为什么需要Class对象呢?

是这样的,当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器子系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。需要特别注意的是,手动编写的每个class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象。




Class对象总结(简介)

Class类也是类的一种,与class关键字是不一样的。

手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件),比如创建一个Shapes类,编译Shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中。

每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。

Class类只存私有构造函数,因此对应Class对象只能由JVM创建和加载

Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要。



Class对象的加载

首先我们要了解一下类的加载过程,了解各个步骤的区别:

类的加载过程:


类的生命周期:
【这些阶段不是一次性进行的,加载了类后不会立刻初始化】

加载就是:类加载过程的一个阶段:通过一个类的完全限定查找此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,将class文件已jvm识别的数据结构加载虚拟机内存中(存放在方法区),并生成一个该类的Class对象,作为方法区这个类的各种数据的访问入口;
连接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,并设置默认值,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。【赋值的特殊情况:如果类变量是final的【即static+final】,那么编译器在编译时就会为value生成ConstantValue属性,并在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值。也就是说准备阶段就有值了,而不是默认值
初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器【static{} 静态块中的语句】和初始化静态成员变量。
初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
实例化 : 就是根据类 创建对象的过程, 最简单的实例化就是 new 创建一个对象, 实例化 发生在 堆区



Class对象是由JVM加载的,那么其加载时机是?

实际上所有的类都是在对其第一次使用时动态加载到JVM中的,当程序创建第一个对类的静态成员引用时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件),注意,使用new操作符创建类的新实例对象也会被当作对类的静态成员的引用(构造函数也是类的静态方法),由此看来Java程序在它们开始运行之前并非被完全加载到内存的,其各个部分是按需加载,所以在使用该类时,类加载器首先会检查这个类的Class对象是否已被加载(类的实例对象创建时依据Class对象中类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找.class文件(编译后Class对象被保存在同名的.class文件中),在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良Java代码(这是java的安全机制检测),完全没有问题后就会被动态加载到内存中,此时相当于Class对象也就被载入内存了(毕竟.class字节码文件保存的就是Class对象)【类加载完成,此时已经初始化完成】,同时也就可以被用来创建这个类的所有实例对象。【实例初始化不一定要在类初始化结束之后才开始初始化】



什么情况下虚拟机需要开始初始化一个类呢?

这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化(而这一过程自然发生在加载、验证、准备之后)

1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:

使用new关键字实例化对象的时候;读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段即【编译期常量】除外)的时候;调用一个类的静态方法的时候。
【注:编译器常量是指既被static修饰,又被final修饰,同时代码中有明确的赋予已知的值。如果没有赋予已知的值不能算(比如使用随机数赋值,下面会提到),编译器常量是在编译器就确定好值的,平时直接调用的话不会引起初始化

2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。



Class.forName方法

forName方法是Class类的一个static成员方法,记住所有的Class对象都源于这个Class类,因此Class类中定义的方法将适应所有Class对象。Class.forName()方法的调用将会返回一个对应类的Class对象,因此如果我们想获取一个类的运行时类型信息并加以使用时,可以调用Class.forName()方法获取Class对象的引用,这样做的好处是无需通过持有该类的实例对象引用而去获取Class对象
注意调用forName方法时需要捕获一个名称为ClassNotFoundException的异常,因为forName方法在编译器是无法检测到其传递的字符串对应的类是否存在的,只能在程序运行时进行检查,如果不存在就会抛出ClassNotFoundException异常。

例子:
package com.zejian;
class Gum {
static {   System.out.println("Loading Gum"); }
}

public class SweetShop {
public static void print(Object obj) {
System.out.println(obj);
}
public static void main(String[] args) {
try {
Class.forName("com.zejian.Gum");
} catch(ClassNotFoundException e) {
print("Couldn't find Gum");
}
}
}

输出结果:Loading Gum

从打印结果来看【static代码块是在初始化的时候运行的】,调用forName方法将会导致Gum类被加载(前提是Gum类从来没有被加载过)。



getClass()方法

getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用。
例子:
//通过实例对象获取Gum的Class对象
Gum gum = new Gum();
Class clazz2=gum.getClass();



Class字面常量

就是Class字面常量,如下:
//字面常量的方式获取Class对象
Class clazz = Gum.class;
这种方式相对前面两种方法更加简单,更安全。
1、因为它在编译器就会受到编译器的检查同时由于无需调用forName方法效率也会更高。
2、因为通过字面量的方法获取Class对象的引用不会自动初始化该类。下面介绍例子
3、更加有趣的是字面常量的获取Class对象引用方式不仅可以应用于普通的类,也可以应用用接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助。


基本数据类型还有对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下,一般情况下更倾向使用.class的形式,这样可以保持与普通类的形式统一。
例如:
byte.class = Byte.TYPE;
int.class = Integer.TYPE;
void.class = Void.TYPE;等等

我们获取字面常量的Class引用时,触发的应该是加载阶段Loading,因为在这个阶段Class对象已创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化
下面来看一个重要的例子:
import java.util.*;

class Initable {
//编译期静态常量
static final int staticFinal = 47;
//非编期静态常量
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}

class Initable2 {
//静态成员变量
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}

class Initable3 {
//静态成员变量
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}

public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
//字面常量获取方式获取Class对象
Class initable = Initable.class;
System.out.println("After creating Initable ref");
//不触发类初始化
System.out.println(Initable.staticFinal);
//会触发类初始化
System.out.println(Initable.staticFinal2);
//会触发类初始化
System.out.println(Initable2.staticNonFinal);
//forName方法获取Class对象
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}

执行结果:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
解析:
1、首先,通过字面常量获取方式获取Initable类的Class对象并没有触发Initable类的初始化,直接输出After creating Initable ref,而不会输出Initializing Initable;
2、同时发现调用Initable.staticFinal变量时也没有触发初始化,这是因为staticFinal属于编译期静态常量,在编译阶段通过常量传播优化的方式将Initable类的常量staticFinal存储到了一个称为NotInitialization类的常量池中,在以后对Initable类常量staticFinal的引用实际都转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在NotInitialization类的常量池获取,这也就是引用编译期静态常量不会触发Initable类初始化的重要原因。所以输出47,此时依旧没有初始化;
3、在之后调用了Initable.staticFinal2变量后就触发了Initable类的初始化,注意staticFinal2虽然被static和final修饰,但其值在编译期并不能确定因此staticFinal2并不是编译期常量,使用该变量必须先初始化Initable类。此时输出Initializing Initable,随后输出258随机数;
4、Initable2和Initable3类中都是静态成员变量并非编译期常量,引用都会触发初始化。所以输出Initializing Initable2、147
5、至于forName方法获取Class对象,肯定会触发初始化,所以输出了最后三个值


结论:关于Class.forname()、Object.getName()、.class字面常量的总结

获取Class对象引用的方式3种,通过继承自Object类的getClass方法,Class类的静态方法forName以及字面常量的方式”.class”。

其中实例类的getClass方法和Class类的静态方法forName都将会触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化。

初始化是类加载的最后一个阶段,也就是说完成这个阶段后类也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的Java程序代码或者字节码。



Class对象关于泛型的运用

在Java SE5引入泛型后,使用我们可以利用泛型来表示Class对象更具体的类型,即使在运行期间会被擦除,但编译期足以确保我们使用正确的对象类型。
例子:
public class ClazzDemo {

public static void main(String[] args){
//没有泛型
Class intClass = int.class;

//带泛型的Class对象
Class<Integer> integerClass = int.class;

integerClass = Integer.class;

//没有泛型的约束,可以随意赋值
intClass= double.class;

//编译期错误,无法编译通过
//integerClass = double.class
}
}


面对下述语句,确实可能令人困惑,但该语句确实是无法编译通过的。
//编译无法通过
Class<Number> numberClass=Integer.class;
我们或许会想Integer不就是Number的子类吗?然而事实并非这般简单,毕竟Integer的Class对象并非Number的Class对象的子类


当然我们可以利用通配符“?”来解决问题:
Class<?> intClass = int.class;
intClass = double.class;
这样的语句并没有什么问题,毕竟通配符指明所有类型都适用,那么为什么不直接使用Class还要使用Class<?>呢?这样做的好处是告诉编译器,我们是确实是采用任意类型的泛型,而非忘记使用泛型约束,因此Class<?>总是优于直接使用Class,至少前者在编译器检查时不会产生警告信息。当然我们还可以使用extends关键字告诉编译器接收某个类型的子类,如解决前面Number与Integer的问题:
//编译通过!
Class<? extends Number> clazz = Integer.class;
//赋予其他类型
clazz = double.class;
clazz = Number.class;
上述的代码是行得通的,extends关键字的作用是告诉编译器,只要是Number的子类都可以赋值。这点与前面直接使用Class<Number>是不一样的。实际上,应该时刻记住向Class引用添加泛型约束仅仅是为了提供编译期类型的检查从而避免将错误延续到运行时期。



instanceof 关键字与isInstance方法

关于instanceof 关键字,它返回一个boolean类型的值, instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例。比如类型转换前可以先进行判断再转换,这样可以避免抛出类型转换的异常(ClassCastException),注意:obj 必须为引用类型,不能是基本类型,instanceof 运算符只能用作对象的判断。

而isInstance方法则是Class类中的一个Native方法,也是用于判断对象类型的

public void cast2(Object obj){
//instanceof关键字
if(obj instanceof Animal){
Animal animal= (Animal) obj;
}

//isInstance方法
if(Animal.class.isInstance(obj)){
Animal animal= (Animal) obj;
}
}
两者用法基本一样,具体自己网上查。





反射技术

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。一直以来反射技术都是Java中的闪亮点,这也是目前大部分框架(如Spring/Mybatis等)得以实现的支柱。在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private),下面将对这几个重要类进行分别说明。

详情API请参考:http://www.matools.com/api/java8
例子请参考本文参考文章:https://blog.csdn.net/javazejian/article/details/70768369

Constructor类及其用法

Constructor类存在于反射包(java.lang.reflect)中,反映的是Class 对象所表示的类的构造方法。获取Constructor对象是通过Class类中的方法获取的
class中关于Constructor的方法如下:

关于Constructor类本身一些常用方法如下:

Field类

Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。同样的道理,我们可以通过Class类的提供的方法来获取代表字段信息的Field对象
class中关于Field 的方法如下:

当然关于Field类还有其他常用的方法如下
上述方法可能是较为常用的,事实上在设置值的方法上,Field类还提供了专门针对基本数据类型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法

Method类

Method 提供关于类或接口上单独某个方法(以及如何访问该方法)的信息,所反映的方法可能是类方法或实例方法(包括抽象方法)。
下面是Class类获取Method对象相关的方法:

Method类的invoke(Object obj,Object... args)第一个参数代表调用的对象,第二个参数传递的调用方法的参数。这样就完成了类方法的动态调用。

Method本身的方法: