编程知识 cdmana.com

《JAVA编程思想》学习笔记:第14章(类型信息)

目录
Java编程思想(一)第1~4章:概述
Java编程思想(二)第5章:初始化和清理
Java编程思想(三)第6章:访问权限
Java编程思想(四)第7章:复用类
Java编程思想(五)第8章:多态
Java编程思想(六)第9章:接口
Java编程思想(七)第10章:内部类
Java编程思想(八)第11章:持有对象
Java编程思想(九)第12章:异常
Java编程思想(十)第13章:字符串
Java编程思想(十一)第14章:类型信息
Java编程思想(十二)第15章:泛型
Java编程思想(十三)第16章:数组
Java编程思想(十四)第17章:深入研究容器
Java编程思想(十五)第18章:Java I/O系统
Java编程思想(十六)第19章:枚举
Java编程思想(十七)第20章:注解
Java编程思想(十八)第21章:并发

第十四章、类型信息

目录

14.1 RTTI

14.2 Class对象

14.3 注册工厂

14.4 instanceof 与 Class 的等价性

14.5 反射

14.6 动态代理

14.7. 空对象

14.8. 接口与类型信息

14.9 总结


14.1 RTTI

RTTI:(Runtime Type Identification)运行阶段类型识别

在Java中,所有的类型转换都是在运行时进行正确性检查的。这也是RTTI的含义:在运行时,识别一个对象的类型。

 

14.1.1 丢失具体类型信息的问题

多态中表现的类型转换:是RTTI最基本的使用形式,但这种转换并不彻底(多态≠RTTI)。

举例:如数组容器实际上将所有元素当作Object持有,取用时再自动将结果转型回声明类型。而数组在填充(持有)对象时,具体类型可能是声明类型的子类,这样放到数组里就会向上转型为声明类型,持有的对象就丢失了具体类型。而取用时将由Object只转型回声明类型,并不是具体的子类类型,所以这种转型并不彻底。

多态中表现了具体类型的行为,但那只是“多态机制”的事情,是由引用所指向的具体对象而决定的,并不等价于在运行时识别具体类型。

 

14.2 Class对象

14.2.1 RTTI在Java中的工作原理

要能够在运行时识别具体类型,说明必然有东西在运行时保存了具体类型信息,这个东西就是Class对象。

 

Class对象:一种特殊对象。即Class对象表示了运行时的类型信息,它包含了与类有关的信息。

a. 事实上Class对象就是用来创建类的所有的“常规”对象的。

b. 每个类都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。

c. Class对象在**.java文件编译成**.class文件时就生成了,且就保存在这个.class文件中。

 

14.2.2 Class对象用来生成对象(常规对象,非Class对象)

运行程序的JVM使用所谓的“类加载器”的子系统(class loader subsystem)通过加载Class对象(或者说.class文件)来生成一个类的对象。

所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序第一次使用类的静态成员时,就会加载这个类,这说明构造器也是静态方法,即使构造器前面没加static关键字。

因此,Java程序在它开始运行之前并非被完全加载,其各个部分是在必须时才被加载的。(C++这种静态加载语言是很难做到的。)

 

14.2.3 类加载器的工作(过程)

step1: 首先检查一个类的Class对象(或理解.class文件)是否已被加载;

step3: 一旦Class对象(.class文件)被加载了(载入内存),它就被用来创建这个类的所有对象。

其中:

a. static子句在类第一次被加载时执行。

b. Class对象仅在需要时才被加载,

c. static初始化是在类加载时进行的。

 

14.2.4 获得Class对象引用的方法(两种)

方法1:Class.forName()

Class.forName(net.mrliuli.rtti.Gum)是Class类的一个静态成员,用来返回一个Class对象的引用(Class对象和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器的工作))。使用这个方法时,如果net.mrliuli.rtti.Gum还没有被加载就加载它。在加载过程中,Gum的static子句被执行。

总之,无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。

通过Class.forName(),就是一个便捷途径,这种方式不需要为了获得Class对象引用而持有该类型的对象。(即没有创建过或没有这个类型的对象的时候就可以获得Class对象引用。)

方法2:已创建对象的Object.getClass()

如果已经有一个类型的对象,那就可以通过调用这个对象的getClass()方法来获取它的Class对象引用了。这个方法属于Object,返回表示该对象的实际类型的Class对象引用。

方法3:类字面常量.class,如 FancyToy.class。建议使用这种方法。

 

14.2.5 Class包含的有用的方法

getName() 获取类的全限定名称

getSimpleName() 获取不含包名的类名

getCanonicalName() 获取全限定的类名

isInterface() 判断某个Class对象是否是接口

getInterfaces() 返回Class对象实现的接口数组

getSuperClass() 返回Class对象的直接基类

其中:newInstance()这个方法依赖于Class对象所代表的类必须具有可访问的默认的构造函数(Nullary constructor,即无参的构造器),否则会抛出InstantiationException 或 IllegalAccessException 异常。

备注:Class引用在编译期不具备任何更进一步的类型信息,所以它返回的只是一个Object引用,但是这个Object引用指向的是这个Class引用所代表的具体类型。即需要转型到具体类型才能给它发送Object以外的消息。

 

14.2.6 类字面常量

14.2.6.1 使用类字面常量.class是获取Class对象引用的另一种方法。

如 FancyToy.class。建议使用这种方法。

编译时就会受到检查(因此不需要放到try语句块中),所以既简单又安全。根除了对forName()的调用,所以也更高效。

类字面常量.class不仅适用于普通的类,也适用于接口、数组和基本类型。

注意,使用.class来创建Class对象的引用时,不会自动地初始化该Class对象。

 

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

step1: 加载。这是由类加载器执行的。该步骤将查找字节码(通常在CLASSPATH所指定的路径中查找),并从这些字节码中创建一个Class对象。

step2: 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。

step3: 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始块。

14.2.6.3 初始化惰性

初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行,即初始化有效地实现了尽可能 的“惰性”。

备注:

非常数静态域:static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);就不是编译期常量,对它的引用将强制进行类的初始化。

常数静态域: static final int staticFinal = 47;

class Initable{
    static final int staticFinal = 47;      // 常数静态域
    static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);     // 非常数静态域(不是编译期常量)
}
class Initable2{
    static int staticNonFinal = 147;    // 非常数静态域
}

public class ClassInitialization {
    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception {
        Class initalbe = Initable.class;      // 使用类字面常量.class获取Class对象引用,不会初始化
        System.out.println(Initable.staticFinal);       // 常数静态域首次引用,不会初始化
        System.out.println(Initable.staticFinal2);      // 非常数静态域首次引用,会初始化
        System.out.println(Initable2.staticNonFinal);   // 非常数静态域首次引用,会初始化
        Class initable3 = Class.forName("net.mrliuli.rtti.Initable3");      // 使用Class.forName()获取Class对象引用,会初始化
    }
}

14.2.7 泛化的Class引用

14.2.7.1 Class对象类型限制<T>

Class引用总是指向某个Class对象,此时,这个Class对象可以是各种类型的,当使用泛型语法对Class引用所指向的Class对象的类型进行限定时,这就使得Class对象的类型变得具体,这样编译器编译时也会做一些额外的类型检查工作。如

public class GenericClassReferences {
    public static void main(String[] args){
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class;    // Same thing
    }
}

14.2.7.2 使用通配符?放松对Class对象类型的限制

通配符<?>:是Java泛型的一部分,?表示“任何事物”。

Class<?> intClass = int.class; 与 Class intClass = int.class; 是等价的,但使用Class<?>优于使用Class,因为它说明了你是明确要使用一个非具体的类引用,才选择了一个非具体的版本,而不是由于你的疏忽。

有限通配符:<? extends **> 或者 <? super **>

Class<? extends Number>

Class<? super Number>

将通配符与extends关键字相结合如Class<? extends Number>,就创建了一个范围,使得这个Class引用被限定为Number类型或其子类型。

 

14.2.7.5 类型转换前先做检查

RTTI形式包括:

方式1:传统类型转换,如(Shape),代表对象的类型的Class对象

方式2:关键字 instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型或其子类。如if(x instanceof Dog)语句会检查对象x是否从属于Dog类。

方式3:动态的instanceof:Class.isInstance()方法提供了一种动态地测试对象的途径。Class.isInstance()方法使我们不再需要instanceof表达式。

 

14.2.7.6 isAssignableFrom()

Class.isAssignableFrom() :调用类型可以被参数类型赋值,即判断传递进来的参数是否属于调用类型继承结构(是调用类型或调用类型的子类)。

 

14.3 注册工厂

工厂方法(设计模式):将对象的创建工作交给类自己完成。

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

14.4 instanceof 与 Class 的等价性

instanceof 和 isInstance() 保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”

== 和 equals() 没有考虑继承——它要么是这个确切的类型,要么不是。

 

14.5 反射

运行时的类信息(Reflection: runtime class information)

Class类与 java.lang.reflect类库一起对反射的概念进行了支持。

RMI: 远程方法调用(Remote Method Invocation):在跨网络的远程平台上创建和运行对象的能力。

 

RTTI与反射的真正区别在于:

对于RTTI来说,是编译时打开和检查.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法。)

对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

 

14.6 动态代理

Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。

在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。

通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,需要三个参数:

ClassLoader loader 一个类加载器,通常可以从已经被加载的对象中获取其类加载器

Class<?>[] interfaces 一个希望代理要实现的接口列表(不是类或抽象类)

InvocationHandler h 一个调用处理器接口的实现

动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器传递一个“实际”对象(即被代理的对象)的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发(即去调用实际对象)。

14.6.1 动态代理的优点及美中不足

优点:动态代理与静态代理相较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

美中不足:它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。

 

14.7. 空对象

eg:

 

14.7.2 模拟对象与桩(Mock Objects & Stubs)

空对象的逻辑变体是模拟对象和桩。

 

14.8. 接口与类型信息

通过使用反射,可以到达并调用一个类的所有方法,包括私有方法!如果知道方法名,就可以在其Method对象上调用setAccessible(true),然后访问私有方法。

class A {
    private void g(){
        System.out.println("B.g()");
    }
}

/**
 * 通过反射调用所有方法(包括私有的)
 */
public class HiddenImplementation {
    static void callHiddenMethod(Object obj, String methodName, Object[] args) throws Exception{
        Method method = obj.getClass().getDeclaredMethod(methodName);
        method.setAccessible(true);
        method.invoke(obj, args);
    }
    public static void main(String[] args) throws Exception{
        callHiddenMethod(new A(), "g", null);
    }
}

14.9 总结

  • 多态:通过运行时动态绑定, 动态判定当前对象引用的类型, 调用本类对应的函数;如果没有,则调用父类的函数。
  • RTTI:instanceOf/ isInstance() :JAVA RTTI的核心是.Class,Class对象存放着对应类所需要的所有的类型信息,包括类的变量,类的属性,类的超类,类实现的借口,类的修饰符,类的对应的类的加载器等等。
  • 反射:是在运行状态时,对于任意的一个类,都能够知道这个类的所有属性和方法;对任意一个对象都能够通过反射机制调用一个类的任意方法,这种动态获取类信息及动态调用类对象方法的功能称为java的反射机制。
  • 动态代理:Java的动态代理比代理的思想更前进了一步,它可以动态地创建并代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的策略。 
  • Spring主要有两大思想:一个是IoC,另一个就是AOP。对于IoC,它利用的是反射机制,依赖注入就不用多说了;而对于Spring的核心AOP来说,使用了动态代理,其实底层也是反射。

 

版权声明
本文为[架构师训练营]所创,转载请带上原文链接,感谢
https://cbk419323.blog.csdn.net/article/details/104096499

Scroll to Top