【JavaSE】运行时类型信息(RTTI、反射)

时间:2023-03-08 17:39:53

运行时类型信息使得你可以在程序运行时发现和使用类型信息。——《Think in java 4th》


通常我们在面向对象的程序设计中我们经常使用多态特性使得大部分代码尽可能地少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道,这样代码会更容易写,更容易读,且便于维护,设计也更容易实现、理解和改变。所以“多态”是面向对象编程的基本目标。但是,有些时候能够知道某个泛化引用对确切类型,就可以使用最简单的方式去解决它,或者我们必须去了解其确切功能和隐藏部分去完成某个功能时,使用RTTI和反射机制可以帮助我们去完成我们想要实现的功能。


1. Class对象

        类是程序的一部分,每个类都有一个Class对象。为了生成这个类的对象,jvm使用了被称为“类加载器”的子系统。类加载器子系统实际上包含一条类加载器链,但是只有一个原生类加载器,它是jvm实现的一部分(native)。如果你有特殊需求,也可以添加额外对自定义类加载器。

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

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

        一旦一个类的Class对象被载入内存,它就被用来创建这个类的所有对象。

1)获取Class类对象引用的方法

① Class.forName("全类名");

这个方法是Class类的一个static成员,Class对象就和其他对象一样,我们可以获取并操作它的引用(这就是类加载器的工作),forName()是取得引用的一种方法。它会使用调用类的类加载器加载指定的类。如果找不到目标类则会抛出ClassNotFoundException。使用此方法,你不需要为了获得Class引用而持有该类型的对象。

② 对象.getClass();

如果你已经有了一个想要的类型的对象,那就可以通过调用getClass()方法来获取Class引用了,这个方法属于Object类的成员方法,它将返回表示该对象实际类型的Class引用。如果你有一个Class对象,还可以使用getInterfaces()getSuperclass()等方法获取接口类Class对象和基类Class对象,isInterface()方法判断是否为接口。

③ 类字面常量 类.class

Java还提供了另一种方法来生成对Class对象的引用(例:Object.class),相比于Class.forName()不仅更简单、而且更安全,因为它在编译器就会受到检查(因此不需要置于try语句块中),根除了对forName()方法的调用,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象:

【JavaSE】运行时类型信息(RTTI、反射)

Java编程思想中建议使用.class的形式,以保持与普通类的一致性。

有趣的是,使用.class来创建对Class对象的引用时,不会自动初始化该Class对象。


为了使用类而做的准备工作实际包含三个步骤:

【JavaSE】运行时类型信息(RTTI、反射)

仅使用.class语法来获得对类的引用不会引发初始化,但是使用Class.forName()立即就进行了初始化。如果一个static final值是“编译期常量”,那么这个值不需要进行初始化就可以被读取。


2)泛化的Class引用

定义一个Class对象的引用可以任意地指向任何Class对象

Class intClass = int.class;
intClass = double.class;

然而如果你操作有误(将本应指向int.class的引用指向了double.class),只有在运行时才可能发现错误的赋值,因为编译器不知道那你的意图,不会报错。Java SE5提供了Class泛型,对Class引用指向的Class对象进行了限定。

Class<Integer> intClass = int.class;
//intClass = double.class;//编译期报错

使用Class泛型的目的是让编译期进行类型检查,误操作时编译期报错,及时发现错误。

通配符?表示任何类型

Class<?> intClass = int.class;
intClass = double.class;

Class与Class使用效果是等价的,但是Class有不同的表示意义,Class<?>表示我就是选择了不具体的类型。

Integer是Number子类,但Integer的Class对象 不是 Number的Class对象 的子类

//class<Number> numberClass = int.class;//不合法

使用Class<? extends Number>表示一个范围,限定Class为Number子类的Class对象

class<? extends Number> numberClass = int.class;//OK

使用泛型的Class引用.newInstance(),返回的是确切类型的实例对象,而不是Object。

除了类似Class<? super FancyToy>的引用调用newInstance(),返回Object类型的实例。

Class<? super FancyToy>表示“某个类,是FancyToy的超类”,是含糊的,所以返回Object类型的实例。

class Toy{}

class FancyToy extends Toy{}

public class GenericClass {

	public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Class<? super FancyToy> up = Toy.class;
Object obj = up.newInstance();//返回Object
Class<Toy> toyClass = Toy.class;
Toy toy = toyClass.newInstance();//返回Toy
} }

3)新的转型语法

Java SE5还添加了用于Class引用的转型语法,即cast()方法:

class Building {}
class House extends Building {} public class ClassCasts {
public static void main(String[] args){
Building b = new House();
Class<House> houseType = House.class;
House h = houseType.cast(b); // 等价操作
h = (House)b; // 等价操作
}
}

cast() 方法接受参数对象,并将其转型为Class引用的类型,新的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就可以解决。

在javaSE5中另一个新特性就是 Class.asSubclass() ,该方法允许你将一个类对象转型为更具体的类型,它将一个类转换成另外一个的实例,如果转换异常就会抛出ClassCastException异常,也就是这个类不是另外一个类的实例;所以我们可以通过它抛出异常的方式来判断一个类是否是另外一个类的实例。(java编程思想中说这是个没什么用的特性) Class.asSubclass浅谈

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; public class Teacher { public static void main(String[] args) { Class<?> clazz = B.class;
String name = clazz.getName();
try {
Class classA = clazz.asSubclass(A.class);
} catch (ClassCastException e) {
System.out.println(name+"不是类StopThread的子类");
}
}
}

4)类型转换检查

在对对象进行转型的时候需要先进行类型检查,否则可能会抛出ClassCastException

① instanceof 关键字

对 instanceof 有比较严格的限制:只可以将其与命名类型进行比较,而不能与Class对象作比较。

A a = new B();
if(a instanceof B) {
B b = (B)a;
}

需要说明的是 instanceof 关键字检查的是is-a关系,也就是可以判断继承关系,a instanceof Object总为true,而直接getClass()并进行相等(==、equals 结果一致)比较只能比较确切类型,并且不同类加载器加载的同一个类的Class是不同的。

② Class.isInstance() 方法

Class.isInstance() 方法提供了一种动态地测试对象的途径。

A a = new B();
if( B.class.isInstance(a) ) {
B b = (B)a;
} // 与 instanceof 的结果一致

能够完成一些使用 instanceof 关键字 无法完成的效果。


5)反射:运行时的类信息

        如果不知道某个对象的确切类型,RTTI可以告诉你,但有一个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它,并利用这些信息做一些有用的事。换句话说,在编译时,编译器必须知道所有要通过RTTI来处理的类。

        在传统的编程环境中不太可能出现这种情况。但当我们置身于更大规模的编程世界中,在许多重要情况下我们可能需要获取一个指向某个并不在你的程序空间中的对象的引用,事实上,在编译时你的程序根本没法获知这个对象所属的类,比如从磁盘的某个文件中,或者网络连接中获取了一串字节,并且被告知这些字节代表了一个类,那么怎样才能使用这样的类呢?

        Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了FieldMethod以及Construtor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get()和set()方法读取和修改与Field对象相关联的字段,用invoke()方法调用与Method对象相关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象数组(详情可查找Class类的JDK文档 )。

Java高级特性——反射(详细方法): https://www.jianshu.com/p/9be58ee20dee

// 使用反射展示类的所有方法, 即使方法是在基类中定义的
package typeinfo; // Print类的print方法等价于System.Out.Println,方便减少代码量
import static xyz.util.Print.*; import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.regex.Pattern; // {Args: typeinfo.ShowMethods}
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) {
print(usage);
System.exit(0);
}
int lines = 0;
try {
Class<?> c = Class.forName(args[0]);
// 反射获得对象c所属类的方法
Method[] methods = c.getMethods();
// 反射获得对象c所属类的构造
Constructor[] ctors = c.getConstructors();
if (args.length == 1) {
for (Method method : methods)
print(p.matcher(method.toString()).replaceAll(""));
for (Constructor ctor : ctors)
print(p.matcher(ctor.toString()).replaceAll(""));
}
} catch (ClassNotFoundException e) {
print("No such class: " + e);
}
} /*
public static void main(String[])
public final void wait() throws InterruptedException
public final void wait(long,int) throws InterruptedException
public final native void wait(long) throws InterruptedException
public boolean equals(Object)
public String toString()
public native int hashCode()
public final native Class getClass()
public final native void notify()
public final native void notifyAll()
public ShowMethods()
*/
}

Java 反射获取私有方法:https://www.cnblogs.com/raincute/p/9848483.html

通常我们创建一个类时,它的私有方法在类外是不可见的,但是可以通过反射机制来获取调用。

Method的setAccessible()方法可以绕过私有的安全检查,所以我们就可以调用类的私有方法。

method.setAccessible(true);//获取私有权限
field.setAccessible(true);

参考文章:https://blog.****.net/gd_hacker/article/details/80272159


总结: 引用 《 Think in java 4th》

参考文章:https://www.cnblogs.com/Harley-Quinn/p/5263802.html

【JavaSE】运行时类型信息(RTTI、反射)