概述
一、简介
从某种意义上来说,Java有两种对象:实例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的。实例对象实际上就是通过Class对象来创建的。Java使用Class对象执行其RTTI(运行时类型识别,Run-Time Type Identification),多态是基于RTTI实现的。
每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象。基本类型(boolean, byte, char, short, int, long, float, double)有Class对象;数组有Class对象;关键字void也有Class对象(void.class)
System.out.println(int.class.getName());
System.out.println(char.class.getName());
System.out.println(short.class.getName());
System.out.println(long.class.getName());
System.out.println(byte.class.getName());
System.out.println(float.class.getName());
System.out.println(double.class.getName());
System.out.println(boolean.class.getName());
System.out.println(void.class.getName());
System.out.println(char[].class.getName());//[C
System.out.println(char[][].class.getName());//[[C
Class类没有公共的构造方法,Class对象是在类加载的时候由JVM以及通过调用类加载器中的 defineClass 方法自动构造的。因此不能显式地声明一个Class对象。
一个类被加载到内存并供我们使用,需要经历如下三个阶段:
- 加载:这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法取得运行时数据接口,根据字节码在Java堆中生成一个代表这个类的java.lang.Class对象
- 链接:在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值)。并且如果必需的话,将常量池中的符号引用转化为直接引用
- 初始化:到了此阶段,才真正开始执行类中定义的Java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化
所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此,Java程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的
在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找**.class**文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象
二、获得Class对象的三种方式
- Class.forName(“类的全限定名”)
- 实例对象.getClass()
- 类名.class (类字面常量)
实例1:Class.forName 和getClass()的使用
package com.cry;
class Dog {
static {
System.out.println("Loading Dog");
}
}
class Cat {
static {
System.out.println("Loading Cat");
}
}
public class Test {
public static void main(String[] args){
System.out.println("inside main");
new Dog();
System.out.println("after creating Dog");
try {
Class cat=Class.forName("com.cry.Cat");
} catch (ClassNotFoundException e) {
System.out.println("Couldn't find Cat");
}
System.out.println("finish main");
}
}
运行结果:
inside main
Loading Dog
after creating Dog
Loading Cat
finish main
备注:
(1)静态语句块、静态成员、静态方法都是在类第一次被加载时执行的
(2)从输出中可以看到,Class对象仅在需要的时候才被加载
Class.forName方法是Class类的一个静态成员。forName在执行的过程中发现如果类Dog还没有被加载,那么JVM就会调用类加载器去加载Dog类,并返回加载后的Class对象。Class对象和其他对象一样,我们可以获取并操作它的引用。在类加载的过程中,Dog类的静态语句块会被执行。如果Class .forName找不到你要加载的类,它会抛出ClassNotFoundException异常。
Class.forName的好处就在于,不需要为了获得Class引用而持有该类型的对象,只要通过全限定名就可以返回该类型的一个Class引用。如果你已经有了该类型的对象,那么我们就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它返回的是表示该对象的实际类型的Class引用
⚠️:基本数据类型的Class对象和包装类的Class对象是不一样的
Class c1 = Integer.class;
Class c2 = int.class;
System.out.println(c1);
System.out.println(c2);
System.out.println(c1 == c2);
运行结果:
class java.lang.Integer
int
false
但是,在包装类型中有一个字段TYPE,TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示,左右两边相互等价:
用 .class 来创建对Class对象的引用时,不会自动地初始化该Class对象(这点和Class.forName方法不同)。类对象的初始化阶段被延迟到了对静态方法或静态成员首次引用时才执行
class Dog {
static final String s1 = "Dog_s1";
static String s2 = "Dog_s2";
static {
System.out.println("Loading Dog");
}
}
class Cat {
static String s1 = "Cat_s1";
static {
System.out.println("Loading Cat");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("----Star Dog----");
Class dog = Dog.class;
System.out.println("------");
System.out.println(Dog.s1);
System.out.println("------");
System.out.println(Dog.s2);
System.out.println("---start Cat---");
Class cat = Class.forName("com.cry.Cat");
System.out.println("-------");
System.out.println(Cat.s1);
System.out.println("finish main");
}
}
运行结果:
----Star Dog----
------
Dog_s1
------
Loading Dog
Dog_s2
---start Cat---
Loading Cat
-------
Cat_s1
finish main
从上面结果可以看到,如果仅使用.class语法来获得对类的Class引用是不会引发初始化的。但是如果使用Class.forName来产生引用,就会立即进行了初始化,就像Cat所看到的
如果一个字段被static final修饰,我们称为”编译时常量“,就像Dog的s1字段那样。那么在调用这个字段的时候是不会对Dog类进行初始化的。因为被static和final修饰的字段,在编译期就把结果放入了常量池中了。但是,如果只是将一个域设置为static 或 final的,还不足以确保这种行为,就如调用Dog的s2字段后,会强制Dog进行类的初始化,因为s2字段不是一个编译时常量
通过javap -c -v对Dog的字节码进行反汇编:
{
static final java.lang.String s1;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String Dog_s1
static java.lang.String s2;
flags: ACC_STATIC
com.cry.Dog();
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/cry/Dog;
static {};
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc #2 // String Dog_s2
2: putstatic #3 // Field s2:Ljava/lang/String;
5: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #5 // String Loading Dog
10: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
LineNumberTable:
line 6: 0
line 9: 5
line 10: 13
}
从上面可以看出,s1在编译后被ConstantValue属性修饰 ConstantValue: String Dog_s1,表示即同时被final和static修饰。而s2并没有被ConstantValue修饰,因为它不是一个编译时常量。在static{}中表示类的初始化操作,在操作中我们看到只有s2字段进行了赋值,而却没有s1的踪影,因此调用s1字段是不会触发类的初始化的
三、总结
一旦类被加载到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个Java堆地址上的Class引用。JVM不会创建两个相同类型的Class对象
package com.cry;
class Cat {
static {
System.out.println("Loading Cat");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("inside main");
Class c1 = Cat.class;
Class c2= Class.forName("com.cry.Cat");
Class c3=new Cat().getClass();
Class c4 =new Cat().getClass();
System.out.println(c1==c2);
System.out.println(c2==c3);
System.out.println("finish main");
}
}
运行结果:
inside main
-------
Loading Cat
true
true
finish main
其实,对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性。也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。所以在Java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性
四、泛型Class引用
Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,允许你对Class引用所指向的Class对象的类型进行限定,也就是说你可以对Class对象使用泛型语法。通过泛型语法,可以让编译器强制指向额外的类型检查
public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type,AnnotatedElement {}
Class<Integer> c1 = int.class;
c1=Integer.class;
//c1=Double.class; 编译报错
虽然,int.class和Integer.class指向的不是同一个Class对象引用,但是它们基本类型和包装类的关系,int可以自动包装为Integer,所以编译器可以编译通过
泛型中的类型可以持有其子类的引用吗?(不行)
Class<Number> c1 = Integer.class; //编译报错
虽然Integer继承自Number,但是编译器无法编译通过
为了使用泛化的Class引用放松限制,我们还可以使用通配符,它是Java泛型的一部分。通配符的符合是”?“,表示“任何事物“
Class<? extends Number> c1 = Integer.class;
c1 = Number.class;
c1 = Double.class;
// c1=String.class; 报错,不属于Number类和其子类
通配符 ? 不仅可以与extend结合,而且还可以与super关键字相结合,表示被限定为某种类型,或该类型的任何父类型
Class<? super Integer> c1 = Integer.class;
c1 = Number.class;
c1 = Object.class;
c1=Integer.class.getSuperclass();
向Class引用添加泛型语法的原因,仅仅是为了提供编译期类型检查
五、Class类的方法
方法名 | 说明 |
---|---|
forName() | (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类;(2)为了产生Class引用,forName()立即就进行了初始化。 |
Object-getClass() | 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用 |
getName() | 取全限定的类名(包括包名),即类的完整名字 |
getSimpleName() | 获取类名(不包括包名) |
getCanonicalName() | 获取全限定的类名(包括包名) |
isInterface() | 判断Class对象是否是表示一个接口 |
getInterfaces() | 返回Class对象数组,表示Class对象所引用的类所实现的所有接口 |
getSupercalss() | 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构 |
newInstance() | 返回一个Oject对象,是实现“虚拟构造器”的一种途径。使用该方法创建的类,必须带有无参的构造器 |
getFields() | 获得某个类的所有的公共(public)的字段,包括继承自父类的所有公共字段。 类似的还有getMethods和getConstructors |
getDeclaredFields | 获得某个类的自己声明的字段,即包括public、private和proteced,默认但是不包括父类声明的任何字段。类似的还有getDeclaredMethods和getDeclaredConstructors |
import java.lang.reflect.Field;
interface I1 {
}
interface I2 {
}
class Cell{
public int mCellPublic;
}
class Animal extends Cell{
private int mAnimalPrivate;
protected int mAnimalProtected;
int mAnimalDefault;
public int mAnimalPublic;
private static int sAnimalPrivate;
protected static int sAnimalProtected;
static int sAnimalDefault;
public static int sAnimalPublic;
}
class Dog extends Animal implements I1, I2 {
private int mDogPrivate;
public int mDogPublic;
protected int mDogProtected;
private int mDogDefault;
private static int sDogPrivate;
protected static int sDogProtected;
static int sDogDefault;
public static int sDogPublic;
}
public class Test {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<Dog> dog = Dog.class;
//类名打印
System.out.println(dog.getName()); //com.cry.Dog
System.out.println(dog.getSimpleName()); //Dog
System.out.println(dog.getCanonicalName());//com.cry.Dog
//接口
System.out.println(dog.isInterface()); //false
for (Class iI : dog.getInterfaces()) {
System.out.println(iI);
}
/*
interface com.cry.I1
interface com.cry.I2
*/
//父类
System.out.println(dog.getSuperclass());//class com.cry.Animal
//创建对象
Dog d = dog.newInstance();
//字段
for (Field f : dog.getFields()) {
System.out.println(f.getName());
}
/*
mDogPublic
sDogPublic
mAnimalPublic
sAnimalPublic
mCellPublic //父类的父类的公共字段也打印出来了
*/
System.out.println("---------");
for (Field f : dog.getDeclaredFields()) {
System.out.println(f.getName());
}
/** 只有自己类声明的字段
mDogPrivate
mDogPublic
mDogProtected
mDogDefault
sDogPrivate
sDogProtected
sDogDefault
sDogPublic
*/
}
}
getName、getCanonicalName与getSimpleName的区别:
- getSimpleName:只获取类名
- getName:类的全限定名,JVM中Class的表示,可以用于动态加载Class对象,例如Class.forName
- getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或 log打印。大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了
public class Test {
private class inner{
}
public static void main(String[] args) throws ClassNotFoundException {
//普通类
System.out.println(Test.class.getSimpleName()); //Test
System.out.println(Test.class.getName()); //com.cry.Test
System.out.println(Test.class.getCanonicalName()); //com.cry.Test
//内部类
System.out.println(inner.class.getSimpleName()); //inner
System.out.println(inner.class.getName()); //com.cry.Test$inner
System.out.println(inner.class.getCanonicalName()); //com.cry.Test.inner
//数组
System.out.println(args.getClass().getSimpleName()); //String[]
System.out.println(args.getClass().getName()); //[Ljava.lang.String;
System.out.println(args.getClass().getCanonicalName()); //java.lang.String[]
//我们不能用getCanonicalName去加载类对象,必须用getName
//Class.forName(inner.class.getCanonicalName()); 报错
Class.forName(inner.class.getName());
}
}
最后
以上就是彪壮夏天为你收集整理的Java中Class类 和 class对象(运行时的类型信息)的全部内容,希望文章能够帮你解决Java中Class类 和 class对象(运行时的类型信息)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复