类型信息(1):为什么需要RTTI、Class对象(上)

     运行时类型信息使得你可以在程序运行时发现和使用类型信息。它使你从只能在编译期执行面向对象类型的操作的禁锢中解脱出来,并且可以使用某些非常强大的程序。对RTTI的需要,揭示了面向对象设计中许多有趣(并且复杂)的问题,同时也提出了如何组织程序的问题。

    接下来将讨论java是如何让我们在运行时识别对象和类的信息的。主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。

一、为什么需要RTTI

    下面看一下我们已经很熟悉了的一个例子,它使用了多态的类层次结构。最通用的类型(泛型)是基类Shape,而派生出的具体类有Circle、Square和Triangle。

    这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程中基本的目的是:让代码只操纵对基类(这里是Shape)的引用。这样,如果要添加一个新类(比如从Shape派生的Rhomboid)来扩展程序,就不会影响到原来的代码。在这个例子的Shape接口中动态绑定了draw()方法,目的就是让客户端程序员使用泛化的Shape引用来调用draw()。draw()在所有派生类里都会被覆盖,并且由于它是被动态绑定的,所以即使是通过泛化的Shape引用来调用,也能产生正确的行为。这就是多态。

    因此,通常会创建一个具体对象(Circle、Square或者Triangle),把它向上转型成Shape(忽略对象的具体类型),并在后面的程序中使用匿名(不知道具体类型)的Shape引用。

    你可以像下面这样对Shape层次结构编码:

import java.util.Arrays;
import java.util.List;

abstract class Shape {
	void draw() {
		System.out.println(this + ".draw()");
	}

	abstract public String toString();
}

class Circle extends Shape {
	@Override
	public String toString() {
		return "Circle";
	}
}

class Square extends Shape {
	@Override
	public String toString() {
		return "Square";
	}
}

class Triangle extends Shape {
	@Override
	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();
		}
	}
}

    基类中包含draw()方法,它通过传递this参数给System.out.println(),间接地使用toString()打印类标识符(注意,toString()被声明为abstract,以此强制继承者覆写该方法,并可以防止对无格式的Shape的实例化)。如果某个对象出现在字符串表达式中(涉及“+”和字符串对象的表达式),toString()方法就会被自动调用,以生成表示该对象的String。每个派生类都要覆盖(从Object继承来的)toString()方法,这样draw()在不同的情况下就打印出不同的消息(多态)。

    在这个例子中,当把Shape对象放入List<Shape>的数组时会向上转型。但在向上转型为Shape的时候也丢失了Shape对象的具体类型。对于数组而言,它们只是Shape对象。

    当从数组中取出元素时,这种容器--实际上它将所有的事物都当成Object持有--会自动将结果转型回Shape。这是RTTI最基本的使用形式,因为在java中,所有的类型转换都是在运行时进行检查的。这也是RTTI名字的含义:在运行时,识别一个对象的类型。

    在这个例子中,RTTI类型转换并不彻底:Object被转型为Shape,而不是转型为Circle、Square或者Triangle。这是因为目前我们只知道这个List<Shape>保存的都是Shape。在编译时,将由容器和java的泛型系统来强制确保这一点;而在运行时,由类型转换操作来确保这一点。

    接下来就是多态机制的事情了,Shape对象实际执行什么样的代码,是由引用所指向的具体对象Circle、Square或者Triangle而决定的。通常,也正是这样要求的;你希望大部分代码尽可能少地了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(这个例子中是Shape)。这样代码会更容易写,更容易读,且更便于维护;设计也更容易实现、理解和改变。所以“多态”是面向对象编程的基本目标。

    但是,假如你碰到了一个特殊的编程——如果能够知道某个泛化引用的确切类型,就可以使用最简单的方式去解决它,那么此时该怎么办呢?例如,假设我们允许用户将某一具体类型的几何形状全都变成某种特殊的颜色,以突出显示它们。通过这种方法,用户就能找出屏幕上所有被突出显示的三角形。或者,可能要用某个方法来旋转列出的所有图形,但想跳过圆形,因为对圆形进行旋转没有意义。使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除特例。

二、class对象

    要理解RTTI在java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class对象就是用来创建类的所有的“常规”对象的。java使用Class对象来执行其RTTI,即使你正在执行的是类似转型这样的操作。Class类还拥有大量的使用RTTI的其他方式。

    类是程序的一部分,每个类都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的java虚拟机(JVM)将使用被称为“类加载器”的子系统。

    类加载器子系统实际上可以包含一条加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是所谓的可信类,包括java API类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者在网络中下载类),那么你有一种方式可以挂接额外的类加载器。

    所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。

    因此,java程序在它开始运行之前并非完全加载,其各个部分是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

    类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码)。这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码(这是java中用于安全防范目的的措施之一)。

    一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。下面的示范程序可以证明这一点:

class Candy {
	static {
		System.out.println("Loading Candy");
	}
}

class Gum {
	static {
		System.out.println("Loading Gum");
	}
}

class Cookie {
	static {
		System.out.println("Loading Cookie");
	}
}

public class SweetShop {
	public static void main(String[] args) {
		System.out.println("inside main");
		new Candy();
		System.out.println("After creating Candy");
		try {
			Class.forName("Gum");
		} catch (ClassNotFoundException e) {
			System.out.println("Couldn't find Gum");
		}
		System.out.println("After Class.forName(\"Gum\")");
		new Cookie();
		System.out.println("After creating Cookie");
	}
}

    这里的每个类Candy、Gum和Cookie,都有一个static子句,该子句在类第一次被加载时执行。这时都有相应的信息打印出来,告诉我们这个类什么时候被加载了。在main()中,创建对象的代码被置于打印语句之间,以帮助我们判断加载的时间点。

    从输出中可以看到,Class对象仅在需要的时候才被加载,static初始化是在类加载时进行的。特别有趣的一行是:

Class.forName("Gum");

    这个方法是Class类(所有Class对象都属于这个类)的一个static成员。Class对象就和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象的引用的一种方法。它是用一个包含目标类的文件名(注意拼写和大小写)的String作输入参数,返回的是一个Class对象的引用,上面的代码忽略了返回值。对forName()的调用是为了它产生的“副作用”:如果Gum还没有被加载就加载它。在加载过程中,Gum的static子句被执行。

    在前面的例子里,如果Class.forName()找不到你要加载的类,它会抛出异常ClassNotFoundException。这里我们只需要简单报告问题,但在更严密的程序里,可能要在异常处理程序中解决这个问题。

    无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。Class.forName()就是实现此功能的便捷途径,因为你不需要为了获得Class引用而持有该类型的对象。但是,如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它将返回表示该对象的实际类型的Class引用。Class包含很多有用的方法,下面是其中的一部分:

interface HasBatteries {
}

interface Waterproof {
}

interface Shoots {
}

class Toy {
	Toy() {
	}

	Toy(int i) {
	}
}

class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
	FancyToy() {
		super(1);
	}
}

public class ToyTest {
	static void printInfo(Class cc) {
		System.out.println("Class name: " + cc.getName() + "is interface?: [" + cc.isInterface() + "]");
		System.out.println("Simple name: " + cc.getSimpleName());
		System.out.println("Canonical name: " + cc.getCanonicalName());
	}

	public static void main(String[] args) {
		Class c = null;
		try {
			c = Class.forName("com.test12.FancyToy");
		} catch (ClassNotFoundException e) {
			System.out.println("Can't find FancyToy");
			System.exit(1);
		}
		printInfo(c);
		for (Class face : c.getInterfaces())
			printInfo(face);
		Class up = c.getSuperclass();
		Object obj = null;
		try {
			obj = up.newInstance();
		} catch (InstantiationException e) {
			System.out.println("Cannot instantiate");
			System.exit(1);
		} catch (IllegalAccessException e) {
			System.out.println("Cannot access");
			System.exit(1);
		}
		printInfo(obj.getClass());
	}
}

    FancyToy继承自Toy并实现了HasBatteries、Waterproof和Shoots接口。在main()中,用forName()方法在适当的try语句块中,创建了一个Class引用,并将其初始化为指向FancyToyClass。注意,在传递给forName()的字符串中,你必须使用全限定名(包含包名)。

    printInfo()使用getName()来产生全限定的类名,并分别使用getSimpleName()和getCanonicalName()来产生不含包名的类名和全限定的类名。isInterface()方法如同其名,可以告诉你这个Class对象是否表示某个接口。因此,通过Class对象,你可以发现你想要了解的类型的所有信息。

    在main()中调用的Class.getInterfaces()方法返回的是Class对象,它们表示在感兴趣的Class对象中所包含的接口。

    如果你有一个Class对象,还可以使用getSuperclass()方法查询其直接基类,这将返回你可以用来进一步查询的Class对象。因此,你可以在运行时发现一个对象完整的类继承结构。

    Class的newInstance()方法是实现“虚拟构造器”的一种途径,虚拟构造器允许你声明:“我不知道你的确切类型,但是无论如何要正确的创建你自己。”在前面的示例中,up仅仅只是一个Class引用,在编译期不具备任何更进一步的类型信息。当你创建新实例时,会得到Object引用,但是这个引用指向的是Toy对象。当然,在你可以发送Object能够接受的消息之外的任何消息之前,你必须更多地了解它,并执行某种转型。另外,使用newInstance()来创建的类,必须带有默认的构造器。在稍后部分,你将会看到如何通过使用java的反射API,用任意的构造器来动态地创建类的对象。

如果本文对您有很大的帮助,还请点赞关注一下。

发布了112 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104648705