类型(Class)信息
运行时类型信息使得你可以在程序运行时发现和使用类型信息。他使你从只能在编译期执行面向类型的操作的禁锢中解脱了出来,并且可以使用某些非常强大的程序。
Java是如何让我们在运行时识别对象和类的信息的
- 传统的RTTI,假定我们在编译时已经知道了所有的类型
- 反射机制,允许我们在运行时发现和使用类的信息。
RTTI的形式包括:
- 传统的类型转换,保证类型转换的正确性,如果执行了一个错误的转换,抛出
ClassCastException
- 代表对象的类型的
Class
对象,通过查询Class对象可以获取运行时所需的信息 instanceof
,返回一个布尔值,告诉我们对象是不是某个特定类型的实例。
为什么需要RTTI
RTTI:Run-Time Type Identification运行时类型信息,通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用对象的实际派生类型。
考虑一个常见的多态场景
1 | public abstract class Shapes { |
在这个例子中,当将Shape放入到List
当从数组中取出对象,List是将所有的事物都当作Object持有,会自动将结果转型回Shape,这是RTTI最基本的使用,即在运行时识别对象的类型。在Java中所有的类型转换都是在运行时进行正确性检查的。
应用
用于泛型,在编译期,我们可以知道的是List内包含的是Shape对象,因此RTTI可以完成转换,即符合假定我们在编译时已经知道了所有的类型。
用于类型指定,RTTI可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除特例
Class对象
Class对象表示着类型信息在运行时的表现,它包含了与类有关的信息,并且Class对象就是用来创建类的所有常规对象的。Java使用Class对象来执行其RTTI,
创建对象
类是程序的一部分,每个类有一个Class对象,每当编译一个新类,则会产生一个Class对象(保存在.class文件中),为了生成类的对象,JVM将使用类加载器进行加载,具体加载过程不展开描述了,可以去看JVM类加载器相关。
运用类型信息
无论何时想要在运行时使用类型信息,就必须先获得对恰当Class对象的引用,Class.forName是一个合适的方法,因为不必为了获得Class引用而持有该类型的对象。
层次结构
1 | public final class Class<T> implements |
类字面常量
例如:UserInfo.class。这样做不仅更简单,而且更安全,因为在编译时就能受到检查,因此不需要try catch,并且根除了对forName的调用,更高效。
可以应用与普通类、接口、数组、基本数据类型。对于基本数据类型的包装类型,有一个标准字段TYPE,是一个指向对应基本数据类型的Clas对象。
1 | boolean.class == Boolean.TYPE |
创建Class对象的引用
使用.class创建对class对象的引用时,不会自动初始化Class对象,初始化被延迟到了对静态方法或非常数静态域进行首次引用。即初始化有效地实现了尽可能惰性。
如果一个static&final值是编译器常量,即=47,那么这个值不需要对类进行初始化就可以立即读取,如果是static|final则需要初始化。
对比forName
forName在获得类的引用时,会立即进行初始化,而.class方法获得引用时,不会立即初始化。
泛化的Class引用
在Java SE5中将Class的类型变的更具体了一些,即允许对Class引用所指向的CLass对象的类型进行限定而实现,即使用泛型,
1 | Class<Integer> genericIntClass = Integer.class; |
普通的类引用可以被重新赋值指向任何其他的Class对象,通过使用泛型,可以让编译器强制执行额外的检测
为了在使用泛化的Class引用时放松限制,则需要使用通配符”?”,并且可以使用extends进行一定程度的限制
1 | Class<?> genericIntClass = Integer.class; |
获得Class对象
Class.forName(String name)
,一般使用类字面常量。
常见的应用场景为:在 JDBC 开发中常用此方法加载数据库驱动。
1 | package io.github.dunwu.javacore.reflect; |
Object.getClass()
Object 类中有 getClass 方法,因为所有类都继承 Object 类。从而调用 Object 类来获取
1 | package io.github.dunwu.javacore.reflect; |
- 类字面常量
1 | public class ReflectClassDemo02 { |
获得Class
Class.forName(String)
该方法是Class类的一个static成员,forName()是取得Class引用的一种方法,其参数为目标类的文本名称(需要包括包名称),返回一个Class对象的引用。
如果找不到需要加载的类,则会抛出异常ClassNotFoundException
ClassObj.getSuperClass()
获得Clss对象的直接基类
ClassObj.getInterfaces()
返回Class对象,表示在感兴趣的Class对象中包含的接口
类型转换
Class.cast(Object)
接受参数对象,并转型为Class引用的类型。对于无法使用普通转型的情况比较有用。例如有时存储了Class引用,并希望以后通过这个引用来执行转型
1 | Building b = new House(); |
Class.isInstance(Object)
接收一个对象参数,判断该对象是否属于该Class,返回boolean值
1 | public class InstanceofDemo { |
创建实例
Class对象创建实例对象主要有两种方式
- 用
Class
对象的newInstance
方法。 - 用
Constructor
对象的newInstance
方法。
1 | public class NewInstanceDemo { |
ClassObj.newInstance()
是实现虚拟构造器的一种途径。虚拟构造器允许你声明“我不知道你的确切类型,但是无论如何要正确地创建你自己”。
返回的类型为Class,当创建新的实例时,会得到Object的引用,但是这个引用指向的是具体类型的对象,因此需要转型。
使用newInstance()创建新类必须带有默认的构造器(无参)
Field
Class
对象提供以下方法获取对象的成员(Field
):
getFiled
- 根据名称获取公有的(public)类成员。getDeclaredField
- 根据名称获取已声明的类成员。但不能得到其父类的类成员。getFields
- 获取所有公有的(public)类成员。getDeclaredFields
- 获取所有已声明的类成员。
示例如下:
1 | public class ReflectFieldDemo { |
Method
Class
对象提供以下方法获取对象的方法(Method
):
getMethod
- 返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。getDeclaredMethod
- 返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。getMethods
- 返回类或接口的所有 public 方法,包括其父类的 public 方法。getDeclaredMethods
- 返回类或接口声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。
获取一个 Method
对象后,可以用 invoke
方法来调用这个方法。
invoke
方法的原型为:
1 | public Object invoke(Object obj, Object... args) |
示例:
1 | public class ReflectMethodDemo { |
Constructor
Class
对象提供以下方法获取对象的构造方法(Constructor
):
getConstructor
- 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。getDeclaredConstructor
- 返回类的特定构造方法。参数为方法参数对应 Class 的对象。getConstructors
- 返回类的所有 public 构造方法。getDeclaredConstructors
- 返回类的所有构造方法。
获取一个 Constructor
对象后,可以用 newInstance
方法来创建类实例。
示例:
1 | public class ReflectMethodConstructorDemo { |
Array
数组在 Java 里是比较特殊的一种类型,它可以赋值给一个对象引用。下面我们看一看利用反射创建数组的例子:
1 | public class ReflectArrayDemo { |
其中的 Array 类为 java.lang.reflect.Array
类。我们通过 Array.newInstance
创建数组对象,它的原型是:
1 | public static Object newInstance(Class<?> componentType, int length) |
Method对象
java.lang.reflect.Method
提供有关类或接口上单个方法的信息和访问权限。反映的方法可以是类方法或实例方法(包括抽象方法)
层次结构
public final class Method extends Executable
Annotation
T getAnnotation(Class annotationClass)
。如果存在这样的注解则返回该元素的指定类型的注解,否则返回NullAnnotation[] getAnnotation()
。返回此元素上直接存在的所有注释
Field对象
层次结构
public final class Field extends AccessibleObject implements Member
Parameter对象
层次结构
public final class Parameter implements AnnotatedElement
Constructor对象
层次结构
public final class Constructor<T> extends Executable
公共接口
abstract Class Executable
Interface Member
Interface GenericDeclaration
Class AccessibleObject
Interface AnnotatedElement
Interface Type
类型转换前先检查
- 关键字
instanceof
返回一个布尔值,告诉我们对象是不是某个特定类型的实例
1 | if(object instanceof Dog) |
若不使用instanceof
,并且object
并不是Dog
类型,则会得到ClassCastException
异常
Class.isInstance()
方法,(Native方法)
动态的instanceof
1 | public class PetCount3 { |
instanceof与Class的等价性
在查询类型信息时,instanceof与直接比较Class对象有一个重要差别
- instanceof保持了类型的概念,指的是该对象是这个类吗,或者是这个类的派生类吗。
- 比较Class对象,则没有考虑继承问题,即它是这个确切的类吗
接口与类型信息
interface的一种重要目标就是允许程序员隔离构件,进而降低耦合性,但是通过类型信息,这种耦合性还是会传播出去,即接口并非是对解耦的无懈可击的保障。
1 | public interface a{ |
通过RTTI,a是被当做B实现的,通过将a转型为B,可以调用不在A中的方法。但是这种方式给了一个机会即代码耦合度很高。
解决的办法即使用包访问权限,其中只有HiddenC是public的,在调用时产生一个A接口类型的对象,虽然返回的是C类型,但是在包外部不能使用A外的任何方法,因为无法在包外部命名C
1 | //包访问权限 |
但是即使RTTI无效,利用反射依然可以到达并调用所有方法,即使是private方法。
总结
- 不使用多态的时候,很容易出现一系列的switch语句,而使用RTTI可以做到简洁,但是会损失多态机制的重要价值
- 使用多态机制的方法调用,要求我们有基类定义的控制权,而当我们扩展的基类并没有包含我们想要的方法,这时RTTI是一种解决之道,你可以检查自己特定类型然后调用自己的方法。因此不需要switch语句。