Thinking in Java Chapter 14
From Thinking in Java 4th Edition
RTTI(Run-Time Type Information),运行时类型信息,使得你可以在程序运行时发现和使用类型信息。对RTTI的需要,揭示了面向对象设计中许多有趣(并且复杂)的问题,同时也提出了如何组织程序的问题。
Java是如何让我们在运行时识别对象和类的信息的。主要有两种方式:
1. “传统的”RTTI,它假定我们在编译时已经知道了所有的类型
2. “反射”机制,它允许我们在运行时发现和使用类型信息
通常会建立一个具体对象(Circle, Square, or Triangle),把它向上转型成Shape(忽略对象的具体类型),并在后面的程序中使用匿名的Shape引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import java.util.*; abstract class Shape { void draw() { System.out.println( this + ".draw()" ); } abstract public String toString(); } class Circle extends Shape { public String toString() { return "Circle" ; } } class Square extends Shape { public String toString() { return "Square" ; } } class Triangle extends Shape { public String toString() { return "Triangle" ; } } public class Shapes { public static void main(String[] args){ List<Shape> shapeList = Arrays.asList( new Circle(), new Square(), new Triangle()); for (Shape shape : shapeList) shape.draw(); } } /* Output: Circle.draw() Square.draw() Triangle.draw() */ |
当从数组中取出元素时,这种容器——实际上它将所有的事物都当作Object持有——会自动将结果转换为Shap。这是RTTI最基本的使用形式,所有的类型转换都是在运行时进行正确检查的。这也是RTTI名字的含义:在运行时,识别一个对象的类型。
这个例子中,RTTI的转换并不彻底: Object被转型为Shape,而不是转型为Circle, Square or Triangle。这是因为目前我们从List<Shape>只知道保存的都是Shape,在编译时,将由容器和Java泛型来强制确保这一点;而在运行时,由类型转换操作来确保这一点。
接下来就是多态机制了,Shape对象实际执行什么样的代码,是由引用所指向的具体对象Circle, Square o Triangle而决定的。
使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除特列。
Class对象
要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。
类是程序的一部分,每个类都有一个Class对象。每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行在这个程序的Java虚拟机将使用被称为“类加载器”的子系统。
类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。
所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。
一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Examination of the way the class loader works. import static net.mindview.util.Print.*; class Candy { static { print( "Loading Candy" ); } } class Gum { static { print( "Loading Gum" ); } } class Cookie { static { print( "Loading Cookie" ); } } public class SweetShop { public static void main(String[] args){ print( "inside main" ); new Candy(); print( "After creating Candy" ); try { Class.forName( "Gum" ); } catch (ClassNotFoundException e){ print( "Couldn't find Gum" ); } print( "After Class.forName(\"Gum\")" ); new Cookie(); print( "After creating Cookie" ); } } /* Output: inside main Loading Candy After creating Candy Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie */ |
forName()是取得Class对象的引用的一种方法。它是由一个包含目标类的文本名的String作输入参数,返回的是一个Class对象的引用。对forName()的调用是为了它产生的“副作用”:如果类Gum还没有被加载,那么就加载它。在加载过程中,Gum的static子句就被执行。
无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。Class.forName()就是实现此功能的便捷途径,因为你不需要为了获得Class的引用而持有该类型的对象。
但是,如果你已经有了一个感兴趣的类型的对象,那就可以通过调用getClass()方法来获取Class引用(这个方法属于根类Object的一部分,它将返回表示该对象的实际类型的Class引用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | // Testing Class Class. package Leetcode.DrJava; import static net.mindview.util.Print.*; interface HasBatteries {} interface Waterproof {} interface Shoots {} class Toy { // Comment out the following default constructor // to see NoSuchMethodError from (*1*) Toy() {} Toy( int i) {} } class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots { FancyToy() { super ( 1 ); } } public class ToyTest { static void printInfo(Class cc){ print( "Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]" ); print( "Simple name: " + cc.getSimpleName()); print( "Canonical name: " + cc.getCanonicalName()); } public static void main(String[] args){ Class c = null ; // get the reference of a Class try { c = Class.forName( "Leetcode.DrJava.FancyToy" ); } catch (ClassNotFoundException e){ print( "Can't find FancyToy" ); System.exit( 1 ); } printInfo(c); // return the interfaces in "c" Class. for (Class face : c.getInterfaces()) printInfo(face); // return the superclass of the "c" class Class up = c.getSuperclass(); Object obj = null ; try { // Requires default constructor obj = up.newInstance(); } catch (InstantiationException e){ print( "Cannot instantiate" ); System.exit( 1 ); } catch (IllegalAccessException e){ print( "Can't access" ); System.exit( 1 ); } printInfo(obj.getClass()); } } /* Output: Class name: Leetcode.DrJava.FancyToy is interface? [false] Simple name: FancyToy Canonical name: Leetcode.DrJava.FancyToy Class name: Leetcode.DrJava.HasBatteries is interface? [true] Simple name: HasBatteries Canonical name: Leetcode.DrJava.HasBatteries Class name: Leetcode.DrJava.Waterproof is interface? [true] Simple name: Waterproof Canonical name: Leetcode.DrJava.Waterproof Class name: Leetcode.DrJava.Shoots is interface? [true] Simple name: Shoots Canonical name: Leetcode.DrJava.Shoots Class name: Leetcode.DrJava.Toy is interface? [false] Simple name: Toy Canonical name: Leetcode.DrJava.Toy */ |
1. 在main()中,用forName()方法在适当的try语句块中,创建了一个Class引用,并将其初始化为指向FancyToy的Class。(forName中的参数用的是全限定名,即包含包名)。
2. printInfo()中使用getName()来产生权限定的类名;运用getSimpleName()来产生不包含包名的类名;运用getCanonicalName()来产生全限定的类名。
(getName()返回的是虚拟机里面的class的表示, 而getCanonicalName()返回的是更容易理解的表示)
3. Class.getInterfaces()方法返回的是Class对象,它们表示感兴趣的Class对象中所包含的接口
4. 如果有一个Class的对象,还可以使用getSuperclass()方法查询其直接基类。
5. Class的newInstance()方法是实现“虚拟构造器”的一种途径:“我不知道你的具体类型,但无论如何要正确地创建你自己”。
使用newInstance()方法来创建的类,必须带有默认的构造器。之后我们会看到如何利用Java的反射API,用任意的构造器来动态地创建类的对象。
类型字面常量
Java提供了另外一种方法来生成对Class对象的引用,即使用类型字面常量。对以上程序可以这样写:
1 | FancyToy. class |
这样做更简单、安全,并且在编译时就会受到检查。
当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:
1. 加载,这是由类加载器执行的,检查字节码,并创建Class对象
2. 链接。验证类中的字节码,为静态域分配空间
3. 初始化。执行静态初始化器和静态初始化块。
初始化被延迟到了对静态方法或者非常数静态域进行首次引用时执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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" ); // Does not trigger initialization: System.out.println(Initable.staticFinal); // Does not trigger initialization: System.out.println(Initable.staticFinal2); // Does not trigger initialization: System.out.println(Initable2.staticNonFinal); Class initable3 = Class.forName( "Initable3" ); 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 */ |
1. 从对initable引用的创建中可以看到,仅使用.class语法来获得对类的引用不会引发初始化。
2. 但是从initable3引用的创建可以看出,Class.forName()立即就进行了初始化。
3. 如果一个static final值是“编译期常量”,就像Initable.staticFinal那样,那么这个值不需要对Initable类进行初始化就可以读取
4. 但如果一个域仅仅是static和final的,还不足以确保3的行为,例如对Initable.staticFinal2的访问将强制进行类的初始化,因为它不是编译期常量
5. 如果一个static域不是final的,那么对它访问时,就要求在访问前进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像对Initable2.statiNonFinal的访问。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)