Java编程思想 类型信息总结

运行时类型信息使得你可以在程序运行时发现和使用类型信息

为什么需要RTTI
在这里插入图片描述
在这里插入图片描述
在这个例子中 当把Shape对象放入List的数组时会向上转型 但在向上转型为Shape的时候也丢失了Shape对象的具体类型 对于数组而言 它们只是Shape类的对象
当从数组中取出元素时 这种容器——实际上它将所有的事物都当作Object持有——会自动将结果转型回Shape 这是RTTI最基本的使用形式 因为在Java中 所有的类型转换都是在运行时进行正确性检查的 这也是RTTI最基本的使用形式 因为在Java中 所有的类型转换都是在运行时进行正确性检查的 这也是RTTI名字的含义 在运行时 识别一个对象的类型
在这个例子中 RTTI类型转换并不彻底 Object被转型为Shape 而不是转型为Circle Square或者Triangle 这是因为目前我们只知道这个List保存的都是Shape 在编译时 将由容器和Java的泛型系统来强制确保这一点 而在运行时 由类型转换操作来确保这一点
接下来就是多态机制的事情了 Shape对象实际执行什么样的代码 是由引用所指向的具体对象Circle Square或者Triangle而决定的 通常 也正是这样要求的 你希望大部分代码尽可能少地了解对象的具体类型 而是只与对象家族中的一个通用表示打交道(在这个例子中是Shape) 这样代码会更容易写 更容易读 且更便于维护 设计也更容易实现 理解和改变 所以 多态 是面向对象编程的基本目标

Class对象
要理解RTTI在Java中的工作原理 首先必须知道类型信息在运行时是如何表示的 这项工作是由称为Class对象的特殊对象完成的 它包含了与类有关的信息 事实上 Class对象就是用来创建类的所有的 常规 对象的 Java使用Class对象来执行其RTTI 即使你正在执行的是类似转型这样的操作 Class类还拥有大量的使用RTTI的其他方式
类是程序的一部分 每个类都有一个Class对象 换言之 每当编写并且编译了一个新类 就会产生一个Class对象(更恰当地说 是被保存在一个同名的.class文件中) 为了生成这个类的对象 运行这个程序的Java虚拟机(JVM)将使用被称为 类加载器 的子系统

一旦某个类的Class对象被载入内存 它就被用来创建这个类的所有对象 下面的示范程序可以证明这一点
在这里插入图片描述
在这里插入图片描述

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

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

类字面常量
Java还提供了另一种方法来生成对Class对象的引用 即使用类字面常量 对上述程序来说 就像下面这样
FancyToy.class;
这样做不仅更简单 而且更安全 因为它在编译时就会受到检查(因此不需要置于try语句块中) 并且它根除了对forName()方法的调用 所以也更高效
类字面常量不仅可以应用于普通的类 也可以应用于接口 数组以及基本数据类型 另外 对于基本数据类型的包装器类 还有一个标准字段TYPE TYPE字段是一个引用 指向对应的基本数据类型的Class对象 如下所示
在这里插入图片描述
建议使用 .class 的形式 以保持与普通类的一致性
注意 有一点很有趣 当使用 .class 来创建对Class对象的引用时 不会自动地初始化该Class对象 为了使用类而做的准备工作实际包含三个步骤

  1. 加载 这是由类记载器执行的 该步骤将查找字节码(通常在classpath所指定的路径中查找 但这并非是必需的) 并从这些字节码中创建一个Class对象
  2. 链接 在链接阶段将验证类中的字节码 为静态域分配存储空间 并且如果必需的话 将解析这个类创建的对其他类的所有引用
  3. 初始化 如果该类具有超类 则对其初始化 执行静态初始化器和静态初始化块
    初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

泛化的Class引用
Class引用总是指向某个Class对象 它可以制造类的实例 幷包含可作用于这些实例的所有方法代码 它还包含该类的静态成员 因此 Class引用表示的就是它所指向的对象的确切类型 而该对象便是Class类的一个对象
但是 Java SE5的设计者们看准机会 将它的类型变得更具体了一些 而这是通过允许你对Class引用所指向的Class对象的类型进行限定而实现的 这里用到了泛型语法 在下面的实例中 两种语法都是正确的
在这里插入图片描述

为了在使用泛化的Class引用时放松限制 我使用了通配符 它是Java泛型的一部分 通配符就是 ? 表示 任何事物 因此 我们可以在上例的普通Class引用中添加通配符 并产生相同的结果
在这里插入图片描述
在这里插入图片描述
为了创建一个Class引用 它被限定为某种类型 或该类型的任何子类型 你需要将通配符与extends关键字相结合 创建一个范围 因此 与仅仅声明Class不同 现在做如下声明
在这里插入图片描述

下面的示例使用了泛型类语法 它存储了一个类引用 稍后又产生了一个List 填充这个List的对象是使用newInstance()方法 通过该引用生成的
在这里插入图片描述

当你将泛型语法用于Class对象时 会发生一件很有趣的事情 newInstance()将返回该对象的确切类型 而不仅仅只是在ToyTest.java中看到的基本的Object 这在某种程度上有些受限
在这里插入图片描述

新的转型语法
Java SE5还添加了用于Class引用的转型语法 即cast()方法
在这里插入图片描述

类型转换前先做检查
迄今为止 我们已知的RTTI形式包括

  1. 传统的类型转换 如 (Shape) 由RTTI确保类型转换的正确性 如果执行了一个错误的类型转换 就会抛出一个ClassCastException异常
  2. 代表对象的类型的Class对象 通过查询Class对象可以获取运行时所需的信息
    RTTI在Java中还有第三种形式 就是关键字instanceof 它返回一个布尔值 告诉我们对象是不是某个特定类型的实例 可以用提问的方式使用它 就像这样
    if(x instanceof Dog)
    ((Dog)x).bark();

一般 可能想要查找某种类型(比如要找三角形 并填充成紫色) 这时可以轻松地使用instanceof来计数所有对象 例如 假设你有一个类的继承体系 描述了Pet(以及它们的主人 这是在后面的示例中出现的一个非常方便的特性) 在这个继承体系中的每个Individual都有一个id和一个可选的名字 正如你可以看到的 此处并不需要去了解Individual的代码——你只需了解可以创建其具名或不具名的对象 并且每个Individual都有一个id()方法 可以返回其唯一的标识符(通过对每个对象计数而创建的) 还有一个toString()方法 如果你没有为Individual提供名字 toString()方法只产生类型名
下面是继承自Individual的类继承体系
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来 我们需要一种方法 通过它可以随机地创建不同类型的宠物 并且为方便起见 还可以创建宠物数组和List 为了使该工具能够适应多种不同的实现 我们将其定义为抽象类
在这里插入图片描述

当你导出PetCreator的子类时 唯一所需提供的就是你希望使用randomPet()和其他方法来创建的宠物类型的List getTypes()方法通常只返回对一个静态List的引用 下面是使用forName()的一个具体实现
在这里插入图片描述

为了对Pet进行计数 我们需要一个能够跟踪各种不同类型的Pet的数量的工具 Map是此需求的首选 其中键是Pet类型名 而值是保存Pet数量的Integer 通过这种方式 你可以询问 有多少个Hamster对象 我们可以使用instanceof来对Pet进行计数
在这里插入图片描述
在这里插入图片描述

对instanceof有比较严格的限制:只可将其与命名类型进行比较 而不能与Class对象作比较 在前面的例子中 可能觉得写出那么一大堆instanceof表达式是很乏味的 的确如此 但是也没有办法让instanceof聪明起来 让它能够自动地创建一个Class对象的数组 然后将目标对象与这个数组中的对象进行逐一的比较 其实这并非是一种如你想象中那般好的限制 如果程序中编写了许多的instanceof表达式 就说明你的设计可能存在瑕疵

使用类字面常量
如果我们用类字面常量重新实现PetCount 那么改写后的结果在许多方面都会显得更加清晰
在这里插入图片描述
在这里插入图片描述

我们现在在typeinfo.pets类库中有了两种PetCreator的实现 为了将第二种实现作为默认实现 我们可以创建一个使用了LiteralPetCreator的外观
在这里插入图片描述

因为PetCount.countPets()接受的是一个PetCreator参数 我们可以很容易地测试LiteralPetCreator(通过上面的外观)
在这里插入图片描述

动态的instanceof
Class.isInstance方法提供了一种动态地测试对象的途径 于是所有那些单调的instanceof语句都可以从PetCount.java的例子中移除了 如下所示
在这里插入图片描述

递归计数
在PetCount3.PetCount中的Map预加载了所有不同的Pet类 与预加载映射表不同的是 我们可以使用Class.isAssignableFrom() 并创建一个不局限于对Pet计数的通用工具
在这里插入图片描述
count()方法获取其参数的Class 然后使用isAssignableFrom()来执行运行时的检查 以效验你传递的对象确实属于我们感兴趣的继承结构 countClass()首先对该类的确切类型计数 然后 如果其超类可以赋值给baseType countClass()将其超类上递归计数
在这里插入图片描述
在这里插入图片描述

注册工厂
生成Pet继承结构中的对象存在着一个问题 即每次向该继承结构添加新的Pet类型时 必须将其添加为LiteralPetCreator.java中的项 如果在系统中已经存在了继承结构的常规的基础 然后在其上要添加更多的类 那么就有可能会出现问题

这里我们需要做的其他修改就是使用工厂方法设计模式 将对象的创建工作交给类自己去完成 工厂方法可以被多态地调用 从而为你创建恰当类型的对象 在下面这个非常简单的版本中 工厂方法就是Factory接口中的create()方法
在这里插入图片描述
泛型参数T使得create()可以在每种Factory实现中返回不同的类型 这也充分利用了协变返回类型
在下面的示例中 基类Part包含一个工厂对象的列表 对应于这个由createRandom()方法产生的类型 它们的工厂都被添加到了partFactories List中 从而被注册到了基类中
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

instanceof与Class的等价性
在查询类型信息时 以instanceof的形式(即以instanceof的形式或isInstance()的形式 它们产生相同的结果) 与直接比较Class对象有一个很重要的差别 下面的例子展示了这种差别
在这里插入图片描述
在这里插入图片描述

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

Class类与java.lang.reflect类库一起对反射的概念进行了支持 该类库包含了Field Method以及Constructor类(每个类都实现了Member接口) 这些类型的对象是由JVM在运行时创建的 用以表示未知类里对应的成员 这样你就可以使用Constructor创建新的对象 用get()和set()方法读取和修改与Field对象关联的字段 用invoke()方法调用与Method对象关联的方法 另外 还可以调用getFields() getMethods()和getConstructor()等很便利的方法 以返回表示字段 方法以及构造器的对象的数组(在JDK文档中 通过查找Class类可了解更多相关资料) 这样 匿名对象的类信息就能在运行时被完全确定下来 而在编译时不需要知道任何事情
重要的是 要认识到反射机制并没有什么神奇之处 当通过反射与一个未知类型的对象打交道时 JVM只是简单地检查这个对象 看它属于哪个特定的类(就像RTTI那样) 在用它做其他事情之前必须先加载那个类的Class对象 因此 那个类的.class文件对于JVM来说必须是可获取的 要么在本地机器上 要么可以通过网络取得 所以RTTI和反射之间真正的区别只在于 对RTTI来说 编译器在编译时打开和检查.class文件 (换句话说 我们可以用 普通 方式调用对象的所有方法) 而对于反射机制来说 .class文件在编译时是不可获取的 所以是在运行时打开和检查.class文件

类方法提取器
通常你不需要直接使用反射工具 但是它们在你需要创建更加动态的代码时会很有用 反射在Java中是用来支持其他特性的 例如对象序列化和JavaBean 但是 如果能动态地提取某个类的信息有的时候还是很有用的 请考虑类方法提取器 浏览实现了类定义的源代码或是其JDK文档 只能找到在这个类定义中被定义或被覆盖的方法 但对你来说 可能有数十个更有用的方法都是继承自基类的 要找出这些方法可能会很乏味且费时 幸运的是 反射机制提供了一种方法 使我们能够编写可以自动展示完整接口的简单工具 下面就是其工作方式
在这里插入图片描述
在这里插入图片描述

上面的输出是从下面的命令行产生的
java ShowMethods ShowMethods
你可以看到 输出中包含一个public的默认构造器 即便能在代码中看到根本没有定义任何构造器 所看到的这个包含在列表中的构造器是编译器自动合成的 如果将ShowMethods作为一个非public的类(也就是拥有包访问权限) 输出中就不会再显示出这个自动合成的默认构造器了 该自动合成的默认构造器会自动被赋予与类一样的访问权限

动态代理
代理是基本的设计模式之一 它是你为了提供额外的或不同的操作 而插入的用来代替 实际 对象的对象 这些操作通常涉及与 实际 对象的通信 因此代理通常充当着中间人的角色 下面是一个用来展示代理结构的简单示例
在这里插入图片描述
在这里插入图片描述

Java的动态代理比代理的思想更向前迈进了一步 因为它可以动态地创建代理并动态地处理对所代理方法的调用 在动态代理上所做的所有调用都会被重定向到单一的调用处理器上 它的工作是揭示调用的类型并确定相应的对策 下面是用动态代理重写的SimpleProxyDemo.java
在这里插入图片描述
在这里插入图片描述

通常 你会执行被代理的操作 然后使用Method.invoke()将请求转发给被代理对象 并传入必须的参数 这初看起来可能有些受限 就像你只能执行泛化操作一样 但是 你可以通过传递其他的参数 来过滤某些方法调用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

空对象
当你使用内置的null表示缺少对象时 在每次使用引用时都必须测试其是否为null 这显得枯燥 而且势必产生相当乏味的代码 问题在于null除了在你试图用它执行任何操作来产生NullPointerException之外 它自己没有其他任何行为 有时引入空对象 的思想将会很有用 它可以接受传递给它的所代表的对象的消息 但是将返回表示为实际上并不存在任何 真实 对象的值 通过这种方式 你可以假设所有的对象都是有效的 而不必浪费编程精力去检查null(并阅读所产生的代码)
尽管想象一种可以自动为我们创建空对象的编程语言显得很有趣 但是实际上 到处使用空对象并没有任何意义——有时检查null就可以了 有时你可以合理地假设你根本不会遇到null 有时甚至通过NullPointerException来探测异常也可以接受的 空对象最有用之处在于它更靠近数据 因为对象表示的是问题空间内的实体 有一个简单的例子 许多系统都有一个Person类 而在代码中 有很多情况是你没有一个实际的人(或者你有 但是你还没有这个人的全部信息) 因此 通常你会使用一个null引用并测试它 与此不同的是 我们可以使用空对象 但是即使空对象可以相应 实际 对象可以响应的所有消息 你仍需要某种方式去测试其是否为空 要达到此目的 最简单的方式是创建一个标记接口
在这里插入图片描述
这使得instanceof可以探测空对象 更重要的是 这并不要求你在所有的类中都添加isNull()方法(毕竟 这只是执行RTTI的一种不同方式——为什么不使用内置的工具呢)
在这里插入图片描述
通常 空对象都是单例 因此这里将其作为静态final实例创建 这可以正常工作的 因为Person是不可变的——你只能在构造器中设置它的值 然后读取这些值 但是你不能修改它们(因为String自身具备内在的不可变性) 如果你想要修改一个NullPerson 那只能用一个新的Person对象来替换它 注意 你可以选择使用instanceof来探测泛化的Null还是更具体的NullPerson 但是由于使用了单例方式 所以你还可以只使用equals()甚至==来与Person.Null比较
现在假设你回到了互联网刚出现时的雄心万丈的年代 并且你已经因你惊人的理念而获得了一大笔的风险投资 你现在要招兵买马了 但是在虚位以待时 你可以将Person空对象放在每个Position上
在这里插入图片描述
有了Position 你就不需要创建空对象了 因为Person.Null的存在就表示这是一个空Position 稍后 你可能会发现需要增加一个显式的用于Position的空对象 但是YAGNI(You Aren’t Going to Need It 你永不需要它)声明 在你的设计草案的初稿中 应该努力使用最简单且可以工作的事物 直至程序的某个方面要求你添加额外的特性 而不是一开始就假设它是必须的
Staff类现在可以在你填充职位时查询空对象
在这里插入图片描述
在这里插入图片描述
注意 你在某些地方仍必须测试空对象 这与检查是否为null没有差异 但是在其他地方(例如本例中的toSting()转换) 你就不必执行额外的测试了 而可以直接假设所有对象都是有效的
如果你用接口取代具体类 那么就可以使用DynamicProxy来自动地创建空对象 假设我们有一个Robot接口 它定义了一个名字 一个模型和一个描述Robot行为能力的List Operation包含一个描述和一个命令(这是一种命令模式类型)
在这里插入图片描述
你可以通过调用operations()来访问Robot的服务
在这里插入图片描述
在这里插入图片描述
这里也使用了嵌套类来执行测试
我们现在可以创建一个扫雪Robot
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
假设存在许多不同类型的Robot 我们想对每一种Robot类型都创建一个空对象 去执行某些特殊操作——在本例中 即提供空对象所代表的Robot确切类型的信息 这些信息是通过动态代理捕获的
在这里插入图片描述
在这里插入图片描述
无论何时 如果你需要一个空Robot对象 只需调用newNullRobot() 并传递需要代理的Robot的类型 代理会满足Robot和Null接口的需求 并提供它所代理的类型的确切名字

模拟对象与桩
空对象的逻辑变体是模拟对象和桩 与空对象一样 它们都表示在最终的程序中使用的 实际 对象 但是 模拟对象和桩都只是假扮可以传递实际信息的存货对象 而不是像空对象那样可以成为null的一种更加智能化的替代物
模拟对象和桩之间的差异在于程度不同 模拟对象往往是轻量级和自测试的 通常很多模拟对象被创建出来是为了处理各种不同的测试情况 桩只是返回桩数据 它通常是重量级的 并且经常在测试之间被复用 桩可以根据它们被调用的方式 通过配置进行修改 因此桩是一种复杂对象 它要做很多事 然而对于模拟对象 如果你需要做很多事情 通常会创建大量小而简单的模拟对象

接口与类型信息
interface关键字的一种重要目标就是允许程序员隔离构件 进而降低耦合性 如果你编写接口 那么就可以实现这一目标 但是通过类型信息 这种耦合性还是会传播出去——接口并非是对解耦的一种无懈可击的保障 下面有一个示例 先是一个接口
在这里插入图片描述
然后实现这个接口 你可以看到其代码是如何围绕着实际的实现类型潜行的
在这里插入图片描述
通过使用RTTI 我们发现a是被当作B实现的 通过将其转型为B 我们可以调用不在A中的方法
这是完全和法和可接受的 但是你也许并不想让客户端程序员这么做 因为这给了他们一个机会 使得他们的代码与你的代码的耦合程度超过你的期望 也就是说 你可能认为interface关键字正在保护着你 但是它并没有 在本例中使用B来实现A这一事实是公开有案可查的
一种解决方案是直接声明 如果程序员决定使用实际的类而不是接口 他们需要自己对自己负责 这在很多情况下可能都是合理的 但 可能 还不够 你也许希望应用一些更严苛的控制
最简单的方式是对实现使用包访问权限 这样在包外部的客户端就不能看到它了
在这里插入图片描述
在这个包中唯一public的部分 即HiddenC 在被调用时将产生A接口类型的对象 这里有趣之处在于 即使你从makeA()返回的是C类型 你在包的外部仍旧不能使用A之外的任何方法 因为你不能在包的外部命名C
现在如果你试图将其向下转型为C 则将被禁止 因为在包的外部没有任何C类型可用
在这里插入图片描述
在这里插入图片描述
正如你所看到的 通过使用反射 仍旧可以到达并调用所有方法 甚至是private方法 如果知道方法名 你就可以在其Method对象上调用setAccessible(true) 就像在callHiddenMethod()中看到的那样
你可能会认为 可以通过只发布编译后的代码来阻止这种情况 但是这并不解决问题 因为只需运行javap 一个随JDK发布的反编译器即可突破这一限制 下面是一个使用它的命令行
javap -private C
-private标志表示所有的成员都应该表示 甚至包括私有成员 下面是输出
在这里插入图片描述
因此任何人都可以获取你最私有的方法的名字和签名 然后调用它们
如果你将接口实现为一个私有内部类 又会怎样呢 下面展示了这种情况
在这里插入图片描述
在这里插入图片描述
这里对反射仍旧没有隐藏任何东西 那么如果是匿名类呢
在这里插入图片描述
看起来没有任何方式可以阻止反射到达并调用那些非公共访问权限的方法 对于域来说 的确如此 即便是private域
在这里插入图片描述
在这里插入图片描述
但是 final域实际上在遭遇修改时是安全的 运行时系统会在不抛异常的情况下接受任何修改尝试 但是实际上不会发生任何修改
通常 所有这些违反访问权限的操作并非世上最遭之事 如果有人使用这样的技术去调用标识为private或包访问权限的方法(很明显这些访问权限表示这些人不应该调用它们) 那么对他们来说 如果你修改了这些方法的某些方面 他们不应该抱怨 另一方面 总是在类中留下后门的这一事实 也许可以使得你能够解决某些特定类型的问题 但如果不这样做 这些问题将难以或者不可能解决 通常反射带来的好处是不可否认的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章