读书笔记-《ON JAVA 中文版》-摘要20[第十九章 类型信息-1]

文章目录

  • 第十九章 类型信息
    • 1. 为什么需要 RTTI
    • 2. Class 对象
      • 2.1 Class 对象
      • 2.2 类字面常量
      • 2.3 泛化的 Class 引用
    • 3. 类型转换检测
    • 4. 注册工厂
    • 5. 类的等价比较
    • 6. 反射:运行时类信息
    • 7. 自我学习总结

第十九章 类型信息

RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息

RTTI 把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。

本章将讨论 Java 是如何在运行时识别对象和类信息的。主要有两种方式:

  1. “传统的” RTTI:假定我们在编译时已经知道了所有的类型;

  2. “反射”机制:允许我们在运行时发现和使用类的信息。

1. 为什么需要 RTTI

import java.util.stream.Stream;

abstract class Shape {
    void draw() {
        // PS:基类中包含 draw() 方法,它通过传递 this 参数传递给 System.out.println() ,
        // 间接地使用 toString() 打印类标识符
        System.out.println(this + ".draw()");
    }

    @Override
    public abstract 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) {
        // PS:在把 Shape 对象放入 Stream 中时就会进行向上转型(隐式)
        Stream.of(new Circle(), new Square(), new Triangle())
                .forEach(Shape::draw);
    }
}

输出:

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

严格来说, Stream 实际上是把放入其中的所有对象都当做 Object 对象来持有,只是取元素时会自动将其类型转为 Shape 。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型

—PS:多态的应用

2. Class 对象

2.1 Class 对象

Java 使用 Class 对象来实现 RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。

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

所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。

其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。

因此,Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。

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

2.2 类字面常量

Java 还提供了另一种方法来生成类对象的引用:类字面常量(

例如:FancyToy.class)。类字面常量不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。

当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:

  1. 加载,这是由类加载器执行的

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

  3. 初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。

直到第一次引用一个 static 方法(构造器隐式地是 static )或者非常量的 static 字段,才会进行类初始化。

package typeinfo;

import java.util.Random;

class Initable {
    static final int STATIC_FINAL = 47;
    static final int STATIC_FINAL2 =
            ClassInitialization.rand.nextInt(1000);

    static {
        System.out.println("Initializing Initable");
    }
}

class Initable2 {
    static int staticNonFinal = 147;

    static {
        System.out.println("Initializing Initable2");
    }
}

class Initable3 {
    static int staticNonFinal = 74;

    static {
        System.out.println("Initializing Initable3");
    }
}

public class ClassInitialization {
    public static Random rand = new Random(47);

    public static void main(String[] args) throws ClassNotFoundException {
        // 仅使用 .class 语 法来获得对类对象的引用不会引发初始化
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");

        // 如果一个 static final 值是“编译期常量”(如 Initable.staticFinal ),
        // 那么这个值不需要对 Initable 类进行初始化就可以被读取
        System.out.println(Initable.STATIC_FINAL);

        // PS:对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量,
        // Initable.staticFinal2 虽然也是用 static final 修饰
        System.out.println(Initable.STATIC_FINAL2);

        // 如果一个 static 字段不是 final 的,那么在对它访问时,总是要求在它被读取之前,
        // 要先进行链 接(为这个字段分配存储空间)和初始化(初始化该存储空间)
        System.out.println(Initable2.staticNonFinal);

        //  Class.forName() 来产生 Class 引 用会立即就进行初始化
        Class initable3 = Class.forName("typeinfo.Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}

输出:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

2.3 泛化的 Class 引用

Java 引入泛型语法之后,我们可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定。在下面的实例中,两种语法都是正确的:

public class GenericClassReferences {
    public static void main(String[] args) {
        Class intClass = int.class;
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class; // 同一个东西

        // 普通的类引用可以重新赋值指向任何其他的 Class 对象
        intClass = double.class;

        // 使用泛型限定的类引用只能指向其声明的类型
        // genericIntClass = double.class;
    }
}

为了在使用 Class 引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ? ,表示“任何事物”。

public class WildcardClassReferences {
    public static void main(String[] args) {
        Class<?> intClass = int.class;
        intClass = double.class;

        // 为了创建一个限定指向某种类型或其子类的 Class 引用,
        // 我们需要将通配符与 extends 关键字配 合使用,创建一个范围限定。
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
    }
}

3. 类型转换检测

直到现在,我们已知的 RTTI 类型包括:

  1. 传统的类型转换,由 RTTI 确保转换的正确性,如果执行了一个错误的类型转换,就会抛出一个 ClassCastException 异常。

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

RTTI 在 Java 中还有第三种形式,那就是关键字 instanceof 。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子:

if(x instanceof Dog) 
    ((Dog)x).bark();

在将 x 的类型转换为 Dog 之前, if 语句会先检查 x 是否是 Dog 类型的对象。进行向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用 instanceof 是非常重要的,否则会得到一个 ClassCastException 异常。

instanceof 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较。

Class.isInstance() 方法提供了一种动态测试对象类型的方法。

Dog.isInstance(x)

4. 注册工厂

工厂方法可以以多态方式调用,并为你创建适当类型的对象。

5. 类的等价比较

当你查询类型信息时,需要注意:instanceof 的形式(即 instanceof 或 isInstance() ,这两者产生的结果相同) 和 与 Class 对象直接比较 这两者间存在重要区别。

class Base {
}

class Derived extends Base {
}

public class FamilyVsExactType {
    static void test(Object x) {
        System.out.println("Testing x of type " + x.getClass());
        System.out.println("x instanceof Base " + (x instanceof Base));
        System.out.println("x instanceof Derived " + (x instanceof Derived));
        System.out.println("Base.isInstance(x) " + Base.class.isInstance(x));
        System.out.println("Derived.isInstance(x) " + Derived.class.isInstance(x));
        System.out.println("x.getClass() == Base.class " + (x.getClass() == Base.class));
        System.out.println("x.getClass() == Derived.class " + (x.getClass() == Derived.class));
        System.out.println("x.getClass().equals(Base.class)) " + (x.getClass().equals(Base.class)));
        System.out.println("x.getClass().equals(Derived.class)) " + (x.getClass().equals(Derived.class)));
        System.out.println("------------");
    }

    public static void main(String[] args) {
        test(new Base());
        test(new Derived());
    }
}

输出:

Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
------------
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
------------

instanceof 和 isInstance() 产生的结果相同, equals() 和 == 产生的结果也相同。但测试本身得出了不同的

结论。与类型的概念一致, instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的 Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。

6. 反射:运行时类信息

类 Class 支持反射的概念, java.lang.reflect 库中包含类 Field 、 Method 和 Constructor (每一个都实现了 Member 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 Constructor 创建新对象, get() 和 set() 方法读取和修改与 Field 对象关联的字段, invoke() 方法调用与 Method 对象关联的方法。此外,还可以调

用便利方法 getFields() 、 getMethods() 、 getConstructors() 等,以返回表示字段、方法和构造函数的对象数组。

RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 .class 文件。换句话说,你可以 用“正常”的方式调用一个对象的所有方法。通过反射, .class 文件在编译时不可用;它由运行时环境打开并检查。

7. 自我学习总结

  1. RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息

  2. Java 使用 Class 对象来实现 RTTI,保存在 java 文件编译后 .class 文件中

  3. Java 还提供了另一种方法来生成类对象的引用:类字面常量(

    例如:FancyToy.class),此时不会自动初始化该 Class 对象

  4. 使用类前要做到3个步骤:加载(JVM加载类)、链接(为 static 字段分配存储空间)、初始化(初始化超类及 static 修饰的方法和块)

  5. 可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定:

Class<Integer> genericIntClass = int.class;

​ 通配符与 extends 关键字配合使用,创建一个范围限定:

Class<? extends Number> bounded = int.class;
  1. 类型转换检测:

​ 1)instanceof

if(x instanceof Dog) 
    ((Dog)x).bark();

​ 2)isInstance

Dog.isInstance(x)
  1. instanceof 说的是“你是这个类,还是从这个类派生的类?”(父类或者本类);而如果使用 == 比较实际的 Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是(确切的对象)

你可能感兴趣的:(读书笔记,java,开发语言)