运行时识别对象和类的信息的两种方式:
- RTTI(Run-Time Type Identification)
- “反射“机制
RTTI的含义就是运行时识别一个对象的类型,类型信息是由Class对象表示的,每个Java里面的类都对应一个Class对象(在编写并且编译后),这个对象被保存在这个类的同名class文件里。为了生成Class对象,运行这个程序的Java虚拟机将使用“类加载器”子系统。
Class对象是由JVM加载的,所有的类都是在对其第一次使用时,动态加载到JVM。当程序创建第一个对类的静态成员的引用,就会加载这个类。使用new关键字创建类的新对象也会被当做对类的静态成员的引用。因此,Java程序在它开始运行之前并非完全被加载,其各个部分是在必须时才被加载的。所以在使用该类时,类加载器首先会检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件(编译后Class对象保存在同名的class文件中),当这个类的字节码文件被加载时,它们必须接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java的安全机制检测),完全没问题后就会被动态加载到内存中,此时相当于Class对象被载入内存(.class字节码文件保存的就是Class对象),它就用来创建这个类的所有对象。
如果想要在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。
Class对象加载的时机
Class对象仅在需要的时候才被加载,static初始化是在类加载时进行的。
调用Class.forName(“全限定名”)将会导致类被加载,前提是该类从来没有被加载过。
获取Class对象引用的3种方法:
- Class cc=Class.fotName(“全限定名”);
- Class cc=object.getClass();
- FancyToy.class;//类字面常量
使用类字面常量生成对Class对象的引用相对前面两种方法更简单,更安全,因为它在编译期就会受到检查,并且由于无需调用forName()方法而更高效。使用.class来创建对Class对象的引用时,不会自动地初始化该Class对象。
使用类字面常量生成对Class对象的引用不仅可以应用于普通的类,还可以应用于接口、数组以及基本数据类型。对于基本数据类型对应的包装类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象。建议使用.class的形式,以保持与普通类的一致性。
boolean.class = Boolean.TYPE;
char.class = Character.TYPE;
byte.class = Byte.TYPE;
short.class = Short.TYPE;
int.class = Integer.TYPE;
long.class = Long.TYPE;
float.class = Float.TYPE;
double.class = Double.TYPE;
void.class = Void.TYPE;
为了使用类而做的准备工作实际包含3个步骤:
加载:由类加载器执行,查找此类字节码文件,并从这些字节码中创建一个Class对象
链接:在链接阶段将验证类中的字节码,为静态域分配空间,不包含实例成员变量,并且如果必需的话,将解析这个类创建的对其他类的所有引用
初始化:如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块
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");
//不触发类初始化
//static final值是编译期常量,那么这个值不需要对Initable类进行初始化就可以被读取
System.out.println(Initable.staticFinal);
//会触发类初始化
//如果只是将一个域设置为static final,还不足以确保这种行为,访问这个域将强制进行类的初始话
System.out.println(Initable.staticFinal2);
//会触发类初始化
//如果一个static域不是final的,那么在对它访问时,总是要求它在被读取之前,要先进行链接(为这个域分配空间)和初始化(初始化该存储空间)
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
泛化的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对象,通过使用泛型语法,可以让编译期强制执行额外的类型检查。
//编译无法通过
Class<Number> numberClass=Integer.class;
//Integer继承自Number 但Integer Class 不是Number Class对象的子类
为了在使用泛化的Class引用时放松限制,可以使用通配符,它是Java泛型的一部分。通配符就是“?”,表示“任何事物”。因此可以在普通Class引用中添加通配符,并产生相同的结果。
Class<?> intClass=int.class;
intClass=double.class;
这样的语句并没有什么问题,毕竟通配符指明所有类型都适用,那么为什么不直接使用Class还要使用Class
//编译通过
Class<? extends Number> clazz=Integer.class;
clazz=double.class;
clazz.Number.class;
向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误,稍后立即就会发现这一点。
类型转换/Class引用的转型语法
cast()方法接收参数对象,并将其转型为Class引用的类型。
class Building{}
class House extends Building{}
public class ClassCasts{
Building b=new House();
Class<House> houseType=House.class;
House h=houseType.cast(b);
h=(House)b;
}
关于instanceof 关键字,它返回一个boolean类型的值,意在告诉我们对象是不是某个特定的类型实例。如下,在强制转换前利用instanceof检测obj是不是Animal类型的实例对象,如果返回true再进行类型转换,这样可以避免抛出类型转换的异常(ClassCastException)
public void cast2(Object obj){
if(obj instanceof Animal){
Animal animal= (Animal) obj;
}
}
而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;
}
}
事实上instanceOf 与isInstance方法产生的结果是相同的。对于instanceOf是关键字只被用于对象引用变量,检查左边对象是不是右边类或接口的实例化。如果被测对象是null值,则测试结果总是false。一般形式:
//判断这个对象是不是这种类型
obj.instanceof(class)
而isInstance方法则是Class类的Native方法,其中obj是被测试的对象或者变量,如果obj是调用这个方法的class或接口的实例,则返回true。如果被检测的对象是null或者基本类型,那么返回值是false;一般形式如下:
//判断这个对象能不能被转化为这个类
class.inInstance(obj)
最后这里给出一个简单实例,验证isInstance方法与instanceof等价
class A{}
class B extends A {}
public class C {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof A " + (x instanceof A));
print("x instanceof B "+ (x instanceof B));
print("A.isInstance(x) "+ A.class.isInstance(x));
print("B.isInstance(x) " +
B.class.isInstance(x));
print("x.getClass() == A.class " +
(x.getClass() == A.class));
print("x.getClass() == B.class " +
(x.getClass() == B.class));
print("x.getClass().equals(A.class)) "+
(x.getClass().equals(A.class)));
print("x.getClass().equals(B.class)) " +
(x.getClass().equals(B.class)));
}
public static void main(String[] args) {
test(new A());
test(new B());
}
}
Testing x of type class com.zejian.A
x instanceof A true
x instanceof B false //父类不一定是子类的某个类型
A.isInstance(x) true
B.isInstance(x) false
x.getClass() == A.class true
x.getClass() == B.class false
x.getClass().equals(A.class)) true
x.getClass().equals(B.class)) false
Testing x of type class com.zejian.B
x instanceof A true
x instanceof B true
A.isInstance(x) true
B.isInstance(x) true
x.getClass() == A.class false
x.getClass() == B.class true
x.getClass().equals(A.class)) false
x.getClass().equals(B.class)) true
RTTI3种形式:
1传统的类型转换,由RTTI保证类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常
2代表对象类型的Class对象
3关键字instanceof,告诉我们对象是不是某个特定类型的实例。