运行时类型信息使得你可以在程序运行时发现和使用类型信息。Java是如何让我们在运行时识别对象和类的信息得呢?

主要有两种方式:1.传统RTTI,他假定我们在编译期间已经知道了所有类型;2.反射,它允许我们在运行时发现和使用类的信息。

一、为什么需要RTTI

我们来看一个例子:

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

abstract class Shape {
void draw() {
System.out.println(this + ".draw()");
} @Override
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> shapes = Arrays.asList(new Circle(), new Triangle(), new Square());
for (Shape shape : shapes) {
shape.draw();
}
}
}

结果:

Circle.draw()
Triangle.draw()
Square.draw()

分析:1.toString()被声明为abstract,以此强制继承者覆写该方法,并且防止对无格式的Shape的实例化。

      2.如果某个对象出现在字符串表达式中(比如“+”,字符串对象的表达式),toString()会自动被调用,以生成该对象的String。

      3.当Shape派生类的对象放入List<Shape>的数组时会向上转型,但是当向上转型为Shape时也丢失了Shape对象的具体类型。对数组而言,他们都只是Shape类的对象。

      4.当从数组中取出元素时(这种容器把所有的事物都当作Object持有),将结果转型会Shape。这是RTTI的最基本使用形式,因为在Java中所有的类型转换都是在运行时进行正确性检查的。这也是RTTI名字的含义:在运行时,识别一个对象的类型。

      5.但是例子中RTTI转型并不彻底:Object被转型为Shape而不是具体的派生类型,这是因为List<Shape>提供的信息是保存的都是Shape类型。在编译时,由容器和泛型保证这点,在运行时,由类型转换操作来确保这点。

      6.Shape对象具体执行什么,是由引用所指向的具体对象(派生类对象)决定的,而现实中大部分人正是希望尽可能少的了解对象的具体类型,而只是和家族中一个通用的类打交道,如:Shape,使得代码更易读写,设计更好实现,理解和改变,所以“多态”是面向对象编程的基本目标。

二、Class对象

  使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除某些特性。要理解RTTI在Java中的工作原理,首先应该知道类型信息在运行时是如何表示的。Class对象承担了这项工作,Class对象包含了与类有关的信息。Class对象就是用来创建所有对象的,Java使用Class对象来执行其RTTI,Class类除了执行转型操作外,还有拥有大量的使用RTTI的其他方式。

  每当编写并编译一个新类,就会生成一个Class对象,被存在同名的.class文件中。运行这个程序的Java 虚拟机(JVM)使用被称为“类加载器”的子系统,生成这个类的对象。类加载器子系统实际上可以包含以挑类加载链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的通常是加载本地盘的内容,这条链通常不需要添加额外的类加载器,但是如果有特殊需求比如:支持Web服务器应用,那么就要挂接额外的类加载器。

  所有的类在被第一次使用时,动态加载到JVM中。当第一个对类的静态成员的引用被创建时,就会加载这个类。这也证明构造器是静态方法,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。因此,Java程序在开始运行之前并未完全加载,各个部分在必须时才会加载。类加载器首先检查这个类的Class对象是否已加载,如果尚未加载,默认的类加载器就会根据类名查找.class文件。

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

class Candy {
static {
System.out.println("Loading Candy");
}
} class Gum {
Gum() {
System.out.println("Constructed Gum");
} static {
System.out.println("Loading Gum");
}
} public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main");
new Candy();
try {
Class.forName("Gum");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
new Gum();
new Candy();
}
}
结果:
Inside main
Loading Candy
Loading Gum
Constructed Gum

  Class.forName()会让类初始化:每个类都有一个static子句,该子句在类被第一次加载时执行,注意第二次加载时候就不走了。

  Class.forName("");这个方法是Class类的一个static成员。Class对象就和其他对象一样 我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象引用的一种方法,Class clazz = Class.forName("xxx"); xxx必须是带包名的全名!!! 如果你已经得到了一个类的引用xxx,还可以通过:Class clazz = xxx.getClass();方式获取class对象。Class包含很多方法 其中比较常用的有:

public T newInstance() 得到一个实例 相当于new 
public native boolean isInstance(Object obj) 是否是参数类的一个实例
public native boolean isAssignableFrom(Class<?> cls) 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口
public native boolean isInterface();判断是否是接口
public native boolean isArray();判断是否是数组
public native boolean isPrimitive();判断是否是基本类型
public boolean isAnnotation() 判断是否是注解
public boolean isSynthetic() 判断是否是同步代码块
public String getName() 返回带包的全名,包含类的类型信息(是引用类型 还是各种基本类型)

public ClassLoader getClassLoader() 获取该类的类加载器。
public TypeVariable<Class<T>>[] getTypeParameters() 获取泛型
public native Class<? super T> getSuperclass(); 获取父类和父类泛型
public Package getPackage() 获取包名
public Class<?>[] getInterfaces() 获取该类实现的接口列表
public Type[] getGenericInterfaces() 获取实现的接口列表及泛型
public native Class<?> getComponentType() 返回表示数组组件类型的 Class。如果此类不表示数组类,则此方法返回 null。如果此类是数组,则返回表示此类组件类型的 Class
public native int getModifiers() 返回类的修饰符
public native Object[] getSigners();// 我目前还不知道是干什么的如果有明确用处请评论告诉我 万分感谢
native void setSigners(Object[] signers)
public Method getEnclosingMethod() 获取局部或匿名内部类在定义时所在的方法
public String getSimpleName() 获取最简单的那个类名
public String getCanonicalName() 带有包名的类名
public String getTypeName() 如果是数组的话[类名] 否则和getSimpleName一样
public Field[] getFields() 获取类中public的字段
public Field[] getDeclaredFields() 获取类中所有字段
public Class<?>[] getClasses() 获取该类以及父类所有的public的内部类
public Class<?>[] getDeclaredClasses() 获取该类所有内部类,不包含父类的
public Method[] getMethods() 获取该类及父类所有的public的方法
public Method[] getDeclaredMethods() 获取该类中所有方法 ,不包含父类
public Constructor<?>[] getConstructors() 获取所有public的构造方法
public Constructor<?>[] getDeclaredConstructors() 获取该类所有构造方法
public Field getField(String name) 根据字段名public获取字段
public Field getDeclaredField(String name) 根据字段名所有获取字段
public Method getMethod(String name, Class<?>... parameterTypes) 根据方法名和参数获取public方法
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) 根据方法名和参数获取所有方法
public Constructor<T> getConstructor(Class<?>... parameterTypes) 根据参数获取public构造方法
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 根据参数获取所有构造反方法、
...  如遗漏重要方法 请提醒 谢谢~

类的字面常量

  Java还有另一种方式生成对Class的引用,即使用字面常量:ClassDemo.class;  这种方式 简单 高效(因为不用初始化类),且这种方式同样适用于接口,数组,基本类型。对于基本类型的包装类,还有一个标准字段TYPE:

        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

  使用这种方式创建对象的引用时,不会自动初始化该Class对象,为了使用类而做的准备工作实际包含三个步骤:

1.加载,由类加载器进行。该步骤将查找字节码,并从这些字节码中创建一个Class对象。

2.连接,在连接阶段将验证类中的字节码,为静态域分配存空间,如果有需要,将解析这个类创建的对其他类的所有引用。

3.初始化,如果该类具有父类,则对其初始化,执行静态初始化器和静态初始化块。

  初始化在对静态方法(构造器是隐式地静态)或非常数静态域进行首次引用时才会执行:

class InitableA {
static final int staticFinalA = 47;
static final int staticFinalB = ClassInitialization.random.nextInt(1000);
static {
System.out.println("Initializing InitableA");
}
} class InitableB {
static int staticNoFinal = 147;
static {
System.out.println("Initializing InitableB");
}
} public class ClassInitialization {
public static Random random = new Random(47);
public static void main(String[] args) {
System.out.println(InitableA.staticFinalA);
System.out.println(InitableA.staticFinalB);
System.out.println(InitableB.staticNoFinal);
}
}
结果:
1
Initializing InitableA
258
Initializing InitableB
147

  如果一个static final 是"编译期常量" ,就像 staticFinalA 那么需要对它的类进行初始化就可以读取,但是 staticFinalB 却会使类初始化,因为它不是编译期常量;只有static修饰符的  staticNoFinal 在对它读取之前,要先进行链接,为这个域分配存储空间并且初始化该存储空间。

泛型化的Class引用

  通过使用泛型语法,可以让编译器强制执行额外的类型检查:Class<Integer> intClass= int.class; 如果想放松限制,可以使用通配符"?":Class<?> anyClass = AnyClass.class;如果想限定某类型的子类可以使用extends 关键字:Class<? extends Number> numCLass = int.class; numClass = double.class;

  当使用泛型语法Class<Toy>用于创建Class对象时,newInstance()将返回该对象得确切信息而不只是Object。

  如果你手头是Toy的父类ToyFather,toyClass.getSuperclass()方法却只能返回 Class<? super Toy> 而不是确切的ToyFather ,并且在newInstance()时返回Object。

三、类型转换前先做检查

  迄今为止我们已知RTTI形式包含:

1)传统的类型转换,“(Shape)” ,由RTTI确保类型转换的正确性,检查未通过会报ClassCastException.

2)代表对象的类型的Class对象,通过查询Class对象可以获得运行时所需的信息。

  向上转型或向下转型是根据类层次结构图排列顺序定义的(父类在上子类在下);由于Circle一定是一个Shape所以向上转型不用有任何转型操作,而向下转型编译器则无法知道Shape以外的任何具体信息,只能使用显示类型转换,以告知编译器额外的信息。

3)instanceof  它可以判断对象是不是某个特定类型的实例。

四、注册工厂

  工厂方法设计模式,将对象的创建工作交给类自己完成。工厂方法可以被多态地调用,从而为你创建恰当类型的对象。简易版本:

public interface FactoryDemo<T> {
T create();
}

create返回的就是对应的对象.

五、instanceof 与 Class的等价性

  在查询类型信息时,以instanceof的形式(即使用instanceof的形式或isInstance()的形式)与直接比较Class对象有一个比较重要的差别:

instanceof:保持了类型的概念,指的是:“你是这个类或这个类的派生类么”。

而 == 或 equals() 比较的只是Class对象,没有考虑继承。

Java编程思想——第14章 类型信息(一)的更多相关文章

  1. Java编程思想——第14章 类型信息(二)反射

    六.反射:运行时的类信息 我们已经知道了,在编译时,编译器必须知道所有要通过RTTI来处理的类.而反射提供了一种机制——用来检查可用的方法,并返回方法名.区别就在于RTTI是处理已知类的,而反射用于处 ...

  2. Java编程思想之十四 类型信息

    第十四章 类型信息 运行时类型信息使得你可以在程序运行时发现和使用类型信息 14.1 为什么需要RTTI 面向对象编程中基本的目的是:让代码只操作对基类的引用. 多态: import java.uti ...

  3. thinking in java学习笔记:14章 类型信息

    14.2 Class 对象 https://github.com/zhaojiatao/javase 1.什么是Class对象,Class对象是用来做什么的? Class对象是java程序用来创建类的 ...

  4. Java编程思想——第17章 容器深入研究(two)

    六.队列 排队,先进先出.除并发应用外Queue只有两个实现:LinkedList,PriorityQueue.他们的差异在于排序而非性能. 一些常用方法: 继承自Collection的方法: add ...

  5. Java编程思想 第21章 并发

    这是在2013年的笔记整理.现在重新拿出来,放在网上,重新总结下. 两种基本的线程实现方式 以及中断 package thread; /** * * @author zjf * @create_tim ...

  6. Java编程思想——第17章 容器深入研究 读书笔记(三)

    七.队列 排队,先进先出. 除并发应用外Queue只有两个实现:LinkedList,PriorityQueue.他们的差异在于排序而非性能. 一些常用方法: 继承自Collection的方法: ad ...

  7. Java编程思想读书笔记--第14章类型信息

    7.动态代理 代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替“实际”对象的对象.这些操作通常涉及与“实际”对象的通信,因此代理通常充当着中间人的角色. 什么是代理模式? ...

  8. Java编程思想(第一章 对象入门)总结

    面向对象编程(oop) 1.1抽象的进步 所有编程语言的最终目的都是提供一种“抽象”方法.   难点是 在机器模型(位于“方案空间”)和实际解决问题模型(位于“问题空间”)之间,程序员必须建立起一种联 ...

  9. Java编程思想-第四章练习题

    练习1:写一个程序,打印从1到100的值 public class Print1To100{ public static void main(String args[]){ for(int i = 1 ...

随机推荐

  1. 04-numpy读取本地数据和索引

    1.numpy读取数据 CSV:Comma-Separated Value,逗号分隔值文件 显示:表格状态 源文件:换行和逗号分隔行列的格式化文本,每一行的数据表示一条记录 由于csv便于展示,读取和 ...

  2. YiShaAdmin,基于.NET Core Web开源的后台快速开发框架

    YiShaAdmin YiShaAdmin 基于.NET Core Web开发,借鉴了很多开源项目的优点,让你开发Web管理系统和移动端Api更简单,所以我也把她开源了. 她可以用于所有的Web应用程 ...

  3. phaser学习总结之Tween详解

    前言 在上一章phaser学习总结之phaser入门教程中,我们已经初步入门了phaser,并通过一个案例了解了phaser,现在我们需要对phaser中的对象进行讲解,本章需要讲解的是tween,即 ...

  4. 面试官:你有m个鸡蛋,如何用最少的次数测出鸡蛋会在哪一层碎?

    假设你面前有一栋n层的大楼和m个鸡蛋,假设将鸡蛋从f层或更高的地方放扔下去,鸡蛋才会碎,否则就不会.你需要设计一种策略来确定f的值,求最坏情况下扔鸡蛋次数的最小值. leetcode原题链接 乍一看这 ...

  5. Linux Shell 基础知识(二)

    1.本文知识结构 2.文件的查询与检索 2.1. cd 目录切换 找到文件/目录位置:cd 切换到上一个工作目录: cd - 切换到home目录: cd or cd ~ 显示当前路径: pwd 更改当 ...

  6. jmeter从获取token开始设计接口

    用自己实习时候的一个项目来实现一下获取token的接口测试 以登录dmp的学科列表为例子: 从登录开始,打开开发者选项 点击登录 在开发者窗口中network xhr Fildder中,看登录时的请求 ...

  7. C# 8 的模式匹配

    C# 7 里面的Pattern Mathing is 模式 switch 和 when C# 8 里面的Pattern Matching 使用Deconstructor 和 位置匹配模式 下面两个类T ...

  8. javascript DOM节点

    获得子节点方式: 1.将文本内容也当成节点 childNodes firstChild lastChild 2.获得标签为内容的节点 children firstElementChild lastEl ...

  9. Java 爬虫服务器被屏蔽,不要慌,咱们换一台服务器

    这是 Java 爬虫系列博文的第四篇,在上一篇 Java 爬虫遇上数据异步加载,试试这两种办法! 中,我们从内置浏览器内核和反向解析法两个角度简单的聊了聊关于处理数据异步加载问题.在这篇文章中,我们简 ...

  10. 04-03 scikit-learn库之AdaBoost算法

    目录 scikit-learn库之AdaBoost算法 一.AdaBoostClassifier 1.1 使用场景 1.2 参数 1.3 属性 1.4 方法 二.AdaBoostRegressor 更 ...