类型信息(Type Information)
运行类型信息(RTTI)允许在程序运行时找到并使用类型信息
有两种形式的类型信息:
- “传统”RTTI:要求在编译时有所有可用的类型
- 反射机制:只能在运行时获取并使用类信息
为什么需要RTTI?
举个例子,从数组中取出元素的时候,容器会根据泛型自动将结构转型为对应的类型。这是RTTI最基本的形式,因为所有的转型要在运行时检查以确保正确。RTTI的意义是:运行时,对象的类型被定义了。
Class对象
要理解RTTI在Java中是怎样运作的,首先要知道类型信息在运行时是怎样展示的。这些是通过Class对象完成的。实际上,Class对象被用来创建类中的所有“常规”对象。
每次创建并编译一个新类时,就创建一个Class对象,然后存储仅命名为.class的文件。JVM使用名为类加载器(class loader)的子系统来创建该类的对象。类加载器子系统由一系列类加载器组成,但是只有一个初始类加载器。初始类加载器装载受信任的类(trusted classes),包括主要从本地磁盘而来的Java API类。
首次使用类时,所有的类都会动态地加载到JVM。这发生在程序首次调用类的静态成员。因为构造方法也是静态的,所有使用new创建该类的新对象也算是首次调用类的静态成员
因此Java的程序不是一开始就完全加载了,而是只加载需要的部分。这和很多传统语言不同。Java是动态加载的。
类加载器首先要确认该类型的Class对象是否已经加载。如果没有,则默认类加载器寻找相应名称的.class文件。在类的字节加载后,会检查这些代码确保没有污染并且不包含坏的Java代码。一旦该类型的Class对象在内存中,则将其用来创建该类型的所有对象
Class.forName(“Gum”)
Class对象和其他普通的对象一样。可以通过Class的forName(String fileName)方法获得Class的句柄。
如果找不到试图加载的类,则Class.forName()失败,抛出ClassNotFoundException异常。
要想在运行时使用类型信息,则必须首先得到Class对象的句柄。使用Class.forName()是最方便的方法。但是如果已经有了需要类型的对象,则可以通过调用getClass()来获得Class句柄。Class中还有许多有趣的方法,具体可以参见JDK。
要注意:forName()中参数String要使用全路径名(即包含包名)
.class
还有第二个方法获得Class对象的句柄,即:.class,例如:FancyToy.class
这样不仅简单,安全(因为在编译时会检查),而且不用放在try块中,十分高效。
.class可以和通常类,接口,数组还有原始类型一起使用。此外,在包装类中有字段TYPE,该字段能生成对应包装类的Class句柄,见下:
A等同于 B
A | B |
---|---|
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 |
建议多使用.class
要注意使用.class创建Class的句柄并不能自动初始化Class对象。还要经过三步才能使用类:
- 加载:由类加载器执行,即找到字节码然后从这些字节码中创建Class对象。
- 链接(linking):在连接阶段验证类的字节码,为static字段分配存储空间,如果 有必要,解析所有由该类生成的其他类的句柄
- 初始化:如果有超类,则初始化该超类。执行静态初始化器和静态初始化块。
延迟初始化直到首次引用静态方法(构造方法是静态方法)或者非常量的静态字段
package JavaExer; //包名JavaExer
//: typeinfo/ClassInitialization.java
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 initable = Initable.class;
System.out.println("After creating Initable ref");
// 没有触发初始化,结果表现为没有输出了staticFinal的值但是没有输出 //“Initializing Initable”这句话
System.out.println(Initable.staticFinal);
// 触发初始化,明显结果就是输出了“Initializing initable”这句话
System.out.println(Initable.staticFinal2);
// 触发初始化
System.out.println(Initable2.staticNonFinal);
try {
Class initable3 = Class.forName("JavaExer.Initable3"); //要使用全类名
}catch(Exception e) {
System.out.println("something wrong!");
throw e;
}
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
使用.class不会立即初始化,使用Class.forName()会立马初始化。
如果static final值是“编译时的常量”,例如Initable.staticFinal,则Initable类不用初始化也能读取static final的值。但是使一个字段static和final,并不能保证可以不用初始化就能读取static final的值,因为这取决于是不是编译时的常量。
如果static字段不是final,则读取该字段总是需要连接和初始化。
Class泛型
Class<Integer> genericIntClass = int.class;
通过使用泛型语法,允许编译器强制进行类型检查
如果这么写
Class<Number> genericNumberClass = int.class;
乍一看好像很合理,因为Integer继承至Number。但是这样是行不通的,因为Integer Class不是Number Class的子类(继承和是否是子类有细微的区别)
但是可以这样写:
Class<?> intClass = int.class;
在Java SE5中,相比Class,更倾向于使用Class<?>,虽然两者的效果相同,而且Class不会产生编译器错误。使用Class<?>的好处是指明是有意识地使用未指明的类引用(non-specific class reference)。
创建一个受限于某个类型或某个子类型的Class句柄,可以将?与extends结合起来,所有可以用Class<? extends Number>替代Class,例如:
Class<? extends Number> = int.class;
为Class加入泛型是为了能在编译期进行类型检查,这样如果出现问题可以尽早的发现。
见下例:
package JavaExer;
import java.util.*;
class CountedInteger {
private static long counter;
private final long id = counter++;
public String toString() {
return Long.toString(id);
}
}
public class FilledList<T> {
private Class<T> type;
public FilledList(Class<T> type) {
this.type = type;
}
public List<T> create(int nElements) {
List<T> result = new ArrayList<T>();
try {
for(int i = 0; i < nElements; i++)
result.add(type.newInstance());
} catch(Exception e) {
throw new RuntimeException(e);
}
return result;
}
public static void main(String[] args) {
FilledList<CountedInteger> fl= new FilledList<CountedInteger>(CountedInteger.class);
System.out.println(fl.create(15));
}
}
注意使用.class的类要有默认构造方法,如果没有默认构造方法会抛出异常。
注意使用newInstance()会返回对象的类型
//: typeinfo/toys/GenericToyTest.java
// Testing class Class.
package typeinfo.toys;
public class GenericToyTest {
public static void main(String[] args) throws Exception {
Class<FancyToy> ftClass = FancyToy.class;
// Produces exact type:
FancyToy fancyToy = ftClass.newInstance();
Class<? super FancyToy> up = ftClass.getSuperclass();
// This won’t compile:
// Class<Toy> up2 = ftClass.getSuperclass();
// Only produces Object:
Object obj = up.newInstance();
}
} ///:~
在转型前检查
instanceof是第三种形式的RTTI,关键字instanceof用来判断一个对象是不是某个特定类型的实例,返回boolean,见下例:
if( x instanceof Dog)
((Dog)x).bark();
在向下转型时要注意使用instanceof,特别是没有其他信息来描述对象的类型的时候,否则是抛出ClassCastException异常
动态instanceof
Class.isInstance()方法能够动态测试对象的类型,见下例:
//: typeinfo/PetCount3.java
// Using isInstance()
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class PetCount3 {
static class PetCounter extends LinkedHashMap<Class<? extends Pet>,Integer> {
public PetCounter() {
super(MapData.map(LiteralPetCreator.allTypes, 0));
}
public void count(Pet pet) {
// Class.isInstance() eliminates instanceofs:
for(Map.Entry<Class<? extends Pet>,Integer> pair : entrySet()) if(pair.getKey().isInstance(pet))
put(pair.getKey(), pair.getValue() + 1);
}
public String toString() {
StringBuilder result = new StringBuilder("{");
for(Map.Entry<Class<? extends Pet>,Integer> pair : entrySet()){
result.append(pair.getKey().getSimpleName());
result.append("=");
result.append(pair.getValue());
result.append(", ");
}
result.delete(result.length()-2, result.length());
result.append("}");
return result.toString();
}
}
public static void main(String[] args) {
PetCounter petCount = new PetCounter();
for(Pet pet : Pets.createArray(20)) {
printnb(pet.getClass().getSimpleName() + " ");
petCount.count(pet);
}
print();
print(petCount);
}
}/* Output:
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse Pug Mouse Cymric {Pet=20, Dog=6, Cat=9, Rodent=5, Mutt=3, Pug=3, EgyptianMau=2, Manx=7, Cymric=5, Rat=2, Mouse=2, Hamster=1} *///:~
instanceof vs .Class相等
当查询类型信息时,instanceof和Class对象的直接比较有很重要的不同之处,看下例:
//: typeinfo/FamilyVsExactType.java
// The difference between instanceof and class
package typeinfo;
import static net.mindview.util.Print.*;
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof Base " + (x instanceof Base));
print("x instanceof Derived "+ (x instanceof Derived));
print("Base.isInstance(x) "+ Base.class.isInstance(x));
print("Derived.isInstance(x) " + Derived.class.isInstance(x));
print("x.getClass() == Base.class " + (x.getClass() == Base.class));
print("x.getClass() == Derived.class " + (x.getClass() == Derived.class));
print("x.getClass().equals(Base.class)) "+ (x.getClass().equals(Base.class)));
print("x.getClass().equals(Derived.class)) " + (x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
} /* Output:
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
*///:~
instanceof的意思是“是这个类,还是这个类的衍生类”。另外如果用==比较Class对象,就不考虑继承的情况,即“是或者不是这个类”
反射:运行时类的信息
如果不知道对象的具体类型,RTTI会告诉你。但是,有个条件限制:编译时必须知道类型信息,以便RTTI检测到类型。即编译器必须知道在使用的所有类。
反射可以检测出可用的方法并产出方法名。Java通过JavaBeans为组件化编程(component-based programming)提供框架。
在运行时获取类信息的另一动机是为了能够通过网络在远程平台创建和执行对象。这叫做Remote Method Invocation(RMI),其允许Java程序将对象散布到各个机器。有多种原因会导致这种情况。例如,在进行计算量很大的任务时,为了加快速度,可以将任务拆分,然后将拆分的任务放到空闲的机器上运行。
类Class支持反射概念,库java.lang.reflect包含类Field, Method和Constructor(其中每个类都实现了Member接口)。这些类型的对象在运行时被JVM创建,以表示未知类对应的成员。之后可以使用Constructors创建新对象,用get()和set()方法读取和修改和Field对象相关联的字段,使用invoke()方法调用和Method对象相关联的方法。此外还可以调用方法getFields(), getMethods(), getConstructors()获取表示字段,方法和构造器的对象数组。因此,匿名对象的类信息可以在运行时完全确定,编译时不需要知道任何信息。
要知道反射并没有什么神奇。当使用反射和未知类型的对象交互时,JVM会检查对象,判断该对象是否属于某个特定的类(和通常的RTTI相似)。在做任何事之前,必须装载Class对象。因此,某个特定类型的.class文件必须能被JVM读取,不论是在本地机器上还是通过网络。所以RTTI和反射的区别在于:通过RTTI,编译器在编译时打开和检查.class文件。换句话说,可以通过正常的方法调用对象中所有的方法;通过反射,.class文件在编译时不可用,其实在运行环境中打开和检查。
类方法提取器(class method extractor)
一般不需要直接使用反射工具,但需要创建更动态的代码时反射工具会很有用。反射支持其他Java特性,例如对象串行化(object serialization)和JavaBeans。但是有时用来动态提取类信息也很有用。
可以通过反射编写简单的工具动态展示整个接口,见下例:
package JavaExer;
import java.lang.reflect.*;
import java.util.regex.*;
public class ShowMethods {
private static String usage = "usage:\n" +
"ShowMethods qualified.class.name\n" +
"To show all methods in class or:\n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving ‘word’";
private static Pattern p = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
int lines = 0;
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if(args.length == 1) {
for(Method method : methods)
System.out.println( p.matcher(method.toString()).replaceAll(""));
for(Constructor ctor : ctors)
System.out.println(p.matcher(ctor.toString()).replaceAll(""));
lines = methods.length + ctors.length;
} else {
for(Method method : methods)
if(method.toString().indexOf(args[1]) != -1) {
System.out.println(
p.matcher(method.toString()).replaceAll(""));
lines++;
}
for(Constructor ctor : ctors)
if(ctor.toString().indexOf(args[1]) != -1) {
System.out.println(p.matcher( ctor.toString()).replaceAll(""));
lines++;
}
}
} catch(ClassNotFoundException e) {
System.out.println("No such class: " + e);
}
}
}
当你记不住类有什么方法而又不想在JDK文档中查找时,这个工具会很有用。
类名.class,getClass()和class.forName()的区别
- 类名.class:如果类还没有装入内存,JVM将使用类的类装载器将类装入内存,不对类做初始化处理,返回类的Class对象
- getClass():返回引用对象运行时真正所指的对象所属的类的Class对象
- Class.forName():装入类并做类的初始化
newInstance()和关键字new的区别
- newInstance()是一个方法,而new是一个关键字
- Class下的newInstance()的使用有局限,因为其生成对象只能调用无参的构造方法,而new关键字没有这个限制
- 从JVM的角度看,使用new创建时,类可以没有加载,但使用newInstance()要保证两点:1. 这个类已经加载;2.这个类已经连接。Class.forName()完成了以上两点,该方法会调用类加载器。
- newInstance()将new拆分成了两步,首先调用Class加载方法加载类,再实例化。这样在调用Class.forName()时可以更加灵活,是一种降低耦合的方法。
类加载器(Class loader)
类加载器就是用来加载Java类到Java虚拟机中。
Java虚拟机(JVM)使用Java类的方式为:Java源程序(.java文件)在进过Java编译器编译之后就被转换为Java字节代码(.class文件)。
类加载器负责读取Java字节代码,并转换为java.lang.Class类的一个实例。
java.lang.ClassLoader类的基本任务就是根据一个指定的类名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。
java中的类加载器大致分为两类,一类是系统提供的,一类则是由java应用开发人员编写的。
系统提供的类加载器主要有3类:
- 引导类加载器(Bootstrap):用来加载Java的核心库,用原生代码来实现的,不继承java.lang.ClassLoader
- 扩展类加载器(ExtClassLoader):用来加载Java的扩展库。类加载器会在扩展库目录查找并加载Java类
- 系统类加载器(AppClassLoader):根据Java应用的类路径(CLASSPATH)来加载Java类。Java应用的类一般都是由系统类加载器来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取系统加载器
除了系统的类加载器之外,还可以通过继承java.lang.ClassLoader类的方法实现自己的类加载器
接口和类型信息
关键字interface的一个重要目的就是允许程序员隔开组件,从而减低耦合。但是通过类型信息接口就不能保证做到解耦和,见下例:
//: typeinfo/interfacea/A.java
package typeinfo.interfacea;
public interface A {
void f();
}
class B implements A {
public void f() {}
public void g() {}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // Compile error
System.out.println(a.getClass().getName());
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
}
/* Output: B *///:~
使用RTTI通过造型为B,可以调用A没有的方法。
这样做是可行的但是一般并不希望客户程序员去这么做,这会导致客户程序员写出更高程度耦合的代码。
最简单的做法是在实现中使用包级访问,从而使包外的程序员无法访问到
//: typeinfo/packageaccess/HiddenC.java
package typeinfo.packageaccess;
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public class HiddenC {
public static A makeA() { return new C(); }
} ///:~