11.1 类文件、类对象和元数据
第 1 章说过,类文件是编译 Java 源码文件(也可能是其他语言的源码文件)得到的中间格式,供 JVM 使用。类文件是二进制文件,目的不是供人类阅读。
运行时通过包含元数据的类对象表示类文件,而类对象表示的是从中创建类文件的 Java 类型。
11.1.1 类对象示例
在 Java 中,获取类对象有多种方式。其中最简单的方式是:
Class<?> myCl = getClass();
上述代码返回调用 getClass() 方法的实例对应的类对象。查看 Object 类的公开方法之后我们知道,Object 类中的 getClass() 方法是公开的,所以,可以获取任意对象 o 的类对象:
Class<?> c = o.getClass();
已知类型的类对象还可以写成“类字面量”:
// 类型名称后面加上“.class”,表示的是类字面量c = int.class; // 等同于Integer.TYPEc = String.class; // 等同于"a string".getClass()c = byte[].class; // 字节数组的类型
基本类型和 void 也能使用字面量表示类对象:
// 使用预先定义好的常量获取基本类型的类对象c = Void.TYPE; // 特殊的“没有返回值”类型c = Byte.TYPE; // 表示byte类型的类对象c = Integer.TYPE; // 表示int类型的类对象c = Double.TYPE; // Short、Character、Long、Float等类型也可以这么做
对于未知的类型,要使用更复杂的方法。
11.1.2 类对象和元数据
类对象包含指定类型的元数据,包括这个类中定义的方法、字段和构造方法等。开发者可以使用这些元数据审查类,就算加载类时对这个类一无所知也可以审查。
例如,可以找出类文件中所有的弃用方法(弃用方法使用 @Deprecated 注解标记):
Class<?> clz = getClassFromDisk();for (Method m : clz.getMethods()) {for (Annotation a : m.getAnnotations()) {if (a.annotationType() == Deprecated.class) {System.out.println(m.getName());}}}
我们还可以找出两个类文件的共同祖先类。下面这种简单的写法在使用同一个类加载程序加载两个类时才能使用:
public static Class<?> commonAncestor(Class<?> cl1, Class<?> cl2) {if (cl1 == null || cl2 == null) return null;if (cl1.equals(cl2)) return cl1;if (cl1.isPrimitive() || cl2.isPrimitive()) return null;List<Class<?>> ancestors = new ArrayList<>();Class<?> c = cl1;while (!c.equals(Object.class)) {if (c.equals(cl2)) return c;ancestors.add(c);c = c.getSuperclass();}c = cl2;while (!c.equals(Object.class)) {for (Class<?> k : ancestors) {if (c.equals(k)) return c;}c = c.getSuperclass();}return Object.class;}
类文件必须符合非常明确的布局才算合法,JVM 才能加载。类文件包含以下部分(按如下顺序):
魔法数(所有类文件都以
CA FE BA BE这四个十六进制的字节开始)使用的类文件标准版本
当前类的常量池
访问标志(
abstract、public等)当前类的名称
继承信息(例如超类的名称)
实现的接口
字段
方法
属性
类文件是简单的二进制格式,不过人类不可读。如果想了解其中的内容,要使用 javap(参见第 13 章)等工具。
类文件中最常使用的部分之一是常量池。常量池中包含类需要引用的所有方法、类、字段和常量(不管在不在当前类中)。常量池经过精心设计,字节码通过索引序号就能方便地引用其中的条目——这么做节省了字节码占用的空间。
不同的 Java 版本生成的类文件版本有所不同,不过,Java 向后兼容的规则之一是,新版 JVM(及其他工具)都能使用旧版类文件。
下面介绍在类加载过程中如何使用硬盘中的字节新建类对象。
