Java编程基础之运行期类型鉴定

2024-08-31 13:18

本文主要是介绍Java编程基础之运行期类型鉴定,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、Class对象
为理解RTTI在Java里如何工作,首先必须了解类型信息在运行期是如何表示的。这时要用到一个名为“Class对象”的特殊形式的对象,其中包含了与类有关的信息(有时也把它叫作“元类”)。事实上,我们要用Class对象创建属于某个类的全部“常规”或“普通”对象。
对于作为程序一部分的每个类,它们都有一个Class对象。换言之,每次写一个新类时,同时也会创建一个Class对象(更恰当地说,是保存在一个完全同名的.class文件中)。在运行期,一旦我们想生成那个类的一个对象,用于执行程序的Java虚拟机(JVM)首先就会检查那个类型的Class对象是否已经载入。若尚未载入,JVM就会查找同名的.class文件,并将其载入。所以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) {e.printStackTrace();}System.out.println("After Class.forName(\"Gum\")");new Cookie();System.out.println("After creating Cookie");}
} ///:~

对每个类来说(Candy,Gum和Cookie),它们都有一个static从句,用于在类首次载入时执行。相应的信息会打印出来,告诉我们载入是什么时候进行的。在main()中,对象的创建代码位于打印语句之间,以便侦测载入时间。
特别有趣的一行是:
Class.forName("Gum");
该方法是Class(即全部Class所从属的)的一个static成员。而Class对象和其他任何对象都是类似的,所以能够获取和控制它的一个句柄(装载模块就是干这件事的)。为获得Class的一个句柄,一个办法是使用forName()。它的作用是取得包含了目标类文本名字的一个String(注意拼写和大小写)。最后返回的是一个Class句柄。
该程序在某个JVM中的输出如下:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie

可以看到,每个Class只有在它需要的时候才会载入,而static初始化工作是在类载入时执行的。
非常有趣的是,另一个JVM的输出变成了另一个样子:
Loading Candy
Loading Cookie
inside main
After creating Candy
Loading Gum
After Class.forName("Gum")
After creating Cookie

看来JVM通过检查main()中的代码,已经预测到了对Candy和Cookie的需要,但却看不到Gum,因为它是通过对forName()的一个调用创建的,而不是通过更典型的new调用。尽管这个JVM也达到了我们希望的效果,因为确实会在我们需要之前载入那些类,但却不能肯定这儿展示的行为百分之百正确。

 1.1类标记
在Java 1.1中,可以采用第二种方式来产生Class对象的句柄:使用“类标记”。对上述程序来说,看起来就象下面这样:
Gum.class;
这样做不仅更加简单,而且更安全,因为它会在编译期间得到检查。由于它取消了对方法调用的需要,所以执行的效率也会更高。
类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据类型的封装器类,它还存在一个名为TYPE的标准字段。TYPE字段的作用是为相关的基本数据类型产生Class对象的一个句柄,如下所示:

……等价于……

... is equivalent to ...

boolean.class

Boolean.TYPE

char.class

Character.TYPE

byte.class

Byte.TYPE

short.class

Short.TYPE

int.class

Integer.TYPE

long.class

Long.TYPE

float.class

Float.TYPE

double.class

Double.TYPE

void.class

Void.TYPE


2、类型转换检查

    对instanceof有一个比较小的限制:只可将其与一个已命名的类型比较,不能同Class对象作对比。

   动态的instanceof:Java 1.1为Class类添加了isInstance方法。利用它可以动态调用instanceof运算符。而在Java 1.0中,只能静态地调用它:

   

package c11.petcount3;
import java.util.*;class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}class Counter { int i; }public class PetCount3 {public static void main(String[] args) {Vector pets = new Vector();Class[] petTypes = {Pet.class,Dog.class,Pug.class,Cat.class,Rodent.class,Gerbil.class,Hamster.class,};try {for(int i = 0; i < 15; i++) {// Offset by one to eliminate Pet.class:int rnd = 1 + (int)(Math.random() * (petTypes.length - 1));pets.addElement(petTypes[rnd].newInstance());}} catch(InstantiationException e) {}catch(IllegalAccessException e) {}Hashtable h = new Hashtable();for(int i = 0; i < petTypes.length; i++)h.put(petTypes[i].toString(),new Counter());for(int i = 0; i < pets.size(); i++) {Object o = pets.elementAt(i);// Using isInstance to eliminate individual// instanceof expressions:for (int j = 0; j < petTypes.length; ++j)if (petTypes[j].isInstance(o)) {String key = petTypes[j].toString();((Counter)h.get(key)).i++;}}for(int i = 0; i < pets.size(); i++)System.out.println(pets.elementAt(i).getClass().toString());Enumeration keys = h.keys();while(keys.hasMoreElements()) {String nm = (String)keys.nextElement();Counter cnt = (Counter)h.get(nm);System.out.println(nm.substring(nm.lastIndexOf('.') + 1) + " quantity: " + cnt.i);}}
} ///:~

可以看到,Java 1.1的isInstance()方法已取消了对instanceof表达式的需要。此外,这也意味着一旦要求添加新类型宠物,只需简单地改变petTypes数组即可;毋需改动程序剩余的部分(但在使用instanceof时却是必需的)。

3、  反射:运行期类信息
如果不知道一个对象的准确类型,RTTI会帮助我们调查。但却有一个限制:类型必须是在编译期间已知的,否则就不能用RTTI调查它,进而无法展开下一步的工作。换言之,编译器必须明确知道RTTI要处理的所有类
从表面看,这似乎并不是一个很大的限制,但假若得到的是一个不在自己程序空间内的对象的句柄,这时又会怎样呢?事实上,对象的类即使在编译期间也不可由我们的程序使用。例如,假设我们从磁盘或者网络获得一系列字节,而且被告知那些字节代表一个类。由于编译器在编译代码时并不知道那个类的情况,所以怎样才能顺利地使用这个类呢?
在传统的程序设计环境中,出现这种情况的概率或许很小。但当我们转移到一个规模更大的编程世界中,却必须对这个问题加以高度重视。第一个要注意的是基于组件的程序设计。在这种环境下,我们用“快速应用开发”(RAD)模型来构建程序项目。RAD一般是在应用程序构建工具中内建的。这是编制程序的一种可视途径(在屏幕上以窗体的形式出现)。可将代表不同组件的图标拖曳到窗体中。随后,通过设定这些组件的属性或者值,进行正确的配置。设计期间的配置要求任何组件都是可以“例示”的(即可以自由获得它们的实例)。这些组件也要揭示出自己的一部分内容,允许程序员读取和设置各种值。此外,用于控制GUI事件的组件必须揭示出与相应的方法有关的信息,以便RAD环境帮助程序员用自己的代码覆盖这些由事件驱动的方法。“反射”提供了一种特殊的机制,可以侦测可用的方法,并产生方法名。通过Java Beans,Java 1.1为这种基于组件的程序设计提供了一个基础结构。
在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调用”(RMI),它允许Java程序(版本1.1以上)使用由多台机器发布或分布的对象。这种对象的分布可能是由多方面的原因引起的:可能要做一件计算密集型的工作,想对它进行分割,让处于空闲状态的其他机器分担部分工作,从而加快处理进度。某些情况下,可能需要将用于控制特定类型任务(比如多层客户/服务器架构中的“运作规则”)的代码放置在一台特殊的机器上,使这台机器成为对那些行动进行描述的一个通用储藏所。而且可以方便地修改这个场所,使其对系统内的所有方面产生影响(这是一种特别有用的设计思路,因为机器是独立存在的,所以能轻易修改软件!)。分布式计算也能更充分地发挥某些专用硬件的作用,它们特别擅长执行一些特定的任务——例如矩阵逆转——但对常规编程来说却显得太夸张或者太昂贵了。
在Java 1.1中,Class类得到了扩展,可以支持“反射”的概念。针对Field,Method以及Constructor类(每个都实现了Memberinterface——成员接口),它们都新增了一个库:java.lang.reflect。这些类型的对象都是JVM在运行期创建的,用于代表未知类里对应的成员。这样便可用构建器创建新对象,用get()和set()方法读取和修改与Field对象关联的字段,以及用invoke()方法调用与Method对象关联的方法。此外,我们可调用方法getFields(),getMethods(),getConstructors(),分别返回用于表示字段、方法以及构建器的对象数组(在联机文档中,还可找到与Class类有关的更多的资料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。
大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道时,JVM只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的RTTI那样)。但在这之后,在我们做其他任何事情之前,Class对象必须载入。因此,用于那种特定类型的.class文件必须能由JVM调用(要么在本地机器内,要么可以通过网络取得)。所以RTTI和“反射”之间唯一的区别就是对RTTI来说,编译器会在编译期打开和检查.class文件。换句话说,我们可以用“普通”方式调用一个对象的所有方法;但对“反射”来说,.class文件在编译期间是不可使用的,而是由运行期环境打开和检查。

  3.1 一个类方法提取器
很少需要直接使用反射工具;之所以在语言中提供它们,仅仅是为了支持其他Java特性,比如对象序列化、Java Beans以及RMI。但是,我们许多时候仍然需要动态提取与一个类有关的资料。其中特别有用的工具便是一个类方法提取器。正如前面指出的那样,若检视类定义源码或者联机文档,只能看到在那个类定义中被定义或覆盖的方法,基础类那里还有大量资料拿不到。幸运的是,“反射”做到了这一点,可用它写一个简单的工具,令其自动展示整个接口。下面便是具体的程序:

//: ShowMethods.java
// Using Java 1.1 reflection to show all the 
// methods of a class, even if the methods are 
// defined in the base class.
import java.lang.reflect.*;public class ShowMethods {static final 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'";public static void main(String[] args) {if(args.length < 1) {System.out.println(usage);System.exit(0);}try {Class c = Class.forName(args[0]);Method[] m = c.getMethods();Constructor[] ctor = c.getConstructors();if(args.length == 1) {for (int i = 0; i < m.length; i++)System.out.println(m[i].toString());for (int i = 0; i < ctor.length; i++)System.out.println(ctor[i].toString());} else {for (int i = 0; i < m.length; i++)if(m[i].toString().indexOf(args[1])!= -1)System.out.println(m[i].toString());for (int i = 0; i < ctor.length; i++)if(ctor[i].toString().indexOf(args[1])!= -1)System.out.println(ctor[i].toString());}} catch (ClassNotFoundException e) {System.out.println("No such class: " + e);}}
} ///:~

Class方法getMethods()和getConstructors()可以分别返回Method和Constructor的一个数组。每个类都提供了进一步的方法,可解析出它们所代表的方法的名字、参数以及返回值。但也可以象这样一样只使用toString(),生成一个含有完整方法签名的字串。代码剩余的部分只是用于提取命令行信息,判断特定的签名是否与我们的目标字串相符(使用indexOf()),并打印出结果。
这里便用到了“反射”技术,因为由Class.forName()产生的结果不能在编译期间获知,所以所有方法签名信息都会在运行期间提取。若研究一下联机文档中关于“反射”(Reflection)的那部分文字,就会发现它已提供了足够多的支持,可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地,这也属于几乎完全不用我们操心的一个步骤——Java自己会利用这种支持,所以程序设计环境能够控制Java Beans——但它无论如何都是非常有趣的。
一个有趣的试验是运行java ShowMehods ShowMethods。这样做可得到一个列表,其中包括一个public默认构建器,尽管我们在代码中看见并没有定义一个构建器。我们看到的是由编译器自动合成的那一个构建器。如果随之将ShowMethods设为一个非public类(即换成“友好”类),合成的默认构建器便不会在输出结果中出现。合成的默认构建器会自动获得与类一样的访问权限。
ShowMethods的输出仍然有些“不爽”。例如,下面是通过调用java ShowMethods java.lang.String得到的输出结果的一部分:
public boolean java.lang.String.startsWith(java.lang.String,int)
public boolean java.lang.String.startsWith(java.lang.String)
public booleanjava.lang.String.endsWith(java.lang.String)

若能去掉象java.lang这样的限定词,结果显然会更令人满意。有鉴于此,可引入上一章介绍的StreamTokenizer类,解决这个问题:
//: ShowMethodsClean.java
// ShowMethods with the qualifiers stripped
// to make the results easier to read
import java.lang.reflect.*;
import java.io.*;public class ShowMethodsClean {static final String usage ="usage: \n" +"ShowMethodsClean qualified.class.name\n" +"To show all methods in class or: \n" +"ShowMethodsClean qualif.class.name word\n" +"To search for methods involving 'word'";public static void main(String[] args) {if(args.length < 1) {System.out.println(usage);System.exit(0);}try {Class c = Class.forName(args[0]);Method[] m = c.getMethods();Constructor[] ctor = c.getConstructors();// Convert to an array of cleaned Strings:String[] n = new String[m.length + ctor.length];for(int i = 0; i < m.length; i++) {String s = m[i].toString();n[i] = StripQualifiers.strip(s);}for(int i = 0; i < ctor.length; i++) {String s = ctor[i].toString();n[i + m.length] = StripQualifiers.strip(s);}if(args.length == 1)for (int i = 0; i < n.length; i++)System.out.println(n[i]);elsefor (int i = 0; i < n.length; i++)if(n[i].indexOf(args[1])!= -1)System.out.println(n[i]);} catch (ClassNotFoundException e) {System.out.println("No such class: " + e);}}
}class StripQualifiers {private StreamTokenizer st;public StripQualifiers(String qualified) {st = new StreamTokenizer(new StringReader(qualified));st.ordinaryChar(' '); // Keep the spaces}public String getNext() {String s = null;try {if(st.nextToken() !=StreamTokenizer.TT_EOF) {switch(st.ttype) {case StreamTokenizer.TT_EOL:s = null;break;case StreamTokenizer.TT_NUMBER:s = Double.toString(st.nval);break;case StreamTokenizer.TT_WORD:s = new String(st.sval);break;default: // single character in ttypes = String.valueOf((char)st.ttype);}}} catch(IOException e) {System.out.println(e);}return s;}public static String strip(String qualified) {StripQualifiers sq = new StripQualifiers(qualified);String s = "", si;while((si = sq.getNext()) != null) {int lastDot = si.lastIndexOf('.');if(lastDot != -1)si = si.substring(lastDot + 1);s += si;}return s;}
} ///:~

ShowMethodsClean方法非常接近前一个ShowMethods,只是它取得了Method和Constructor数组,并将它们转换成单个String数组。随后,每个这样的String对象都在StripQualifiers.Strip()里“过”一遍,删除所有方法限定词。正如大家看到的那样,此时用到了StreamTokenizer和String来完成这个工作。
假如记不得一个类是否有一个特定的方法,而且不想在联机文档里逐步检查类结构,或者不知道那个类是否能对某个对象(如Color对象)做某件事情,该工具便可节省大量编程时间。
















这篇关于Java编程基础之运行期类型鉴定的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1124006

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听