Featured image of post Java编程思想_14_ClassInfo

Java编程思想_14_ClassInfo

🌏Java编程思想 第14章 类型信息 🎯主要呈现的内容是对原书代码的理解、实践和反思 本章主要讨论的是Java如何让我们在运行时识别对象和类型信息:一种是传统的RTTI(Run-Time Type Information) 它假定我们在编译时已经知道了所有的类型; 另一种是反射机制 它允许我们在运行时发现和使用类的信息

🎄概述

运行时Class信息使得我们可以在运行的时候发现和使用Class信息 它使得我们只能在编译期执行面向对象的操作的禁锢中解脱出来

🎄为什么需要RTTI

RTTI (Run-Time Type Information) 是指运行时类型信息,在运行过程中,能够检测出Java对象的实际Class类型,从而执行相应的操作 这个地方为什么说是实际Class类型呢?因为在编译的时候称Class类型为编译类型 看下面的一个例子:

class Parent {
    private String name;
    public Parent(String name){
        this.name = name;
    }
    public void getInfo(){
        System.out.println("ParentName = " + this.name);
    }
}
class Son extends Parent {
    public Son(String name) {
        super(name);
    }
}
class Test{
    public static void main(String[] args){
        // 编译的时候new Parent("Parent") 和 new Son("Son")的编译类型都是Parent
        Parent[] a = new Parent[]{new Parent("Parent"), new Son("Son")};
        a[0].getInfo(); // a[0]的运行类型是Parent
        a[1].getInfo(); // a[1]的运行类型是Son
    }
    /**
     * ParentName = Parent
     * ParentName = Son
     */
}

通过RTTI,我们可以在运行时获取对象的实际类型信息,并根据需要进行类型转换和动态绑定。这对于实现多态性非常重要,因为在编译时无法确定对象的具体类型,只有在运行时才能确定。RTTI使得我们可以使用基类的引用来操作其派生类的对象,并根据实际类型来调用相应的方法

🎄Class对象

每个类都有一个Class对象 Class对象存在于方法区(编译之后),用来创建类的所有“常规”对象 Java使用Class对象来执行其RTTI

每当编写并编译了一个新的类 就会产生一个Class对象 更恰当地说 是被保存在一个同名的.class文件中

为了生成类的实例对象 JVM将会使用一个称为“类加载器”的子系统 所有的类都是在第一次使用的时候 动态地加载到 JVM中的,也就是说Java程序在运行之前并非完全加载,而是等到各个部分需要使用的时候按需加载

在需要使用Class对象的时候,比如某个时候需要new一个新的java对象 类加载器会检查这个类的Class对象是否已经加载到JVM内存 如果已经加载 则直接使用; 如果没有加载 则默认的类加载器会根据类名查找对应的.class文件并进行加载

🎯一旦某个类的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"); }
}

然后执行测试 由运行的结果我们可以看到 Class对象在没有被加载前是获取不到的

@SuppressWarnings({"all"})
public class SweetShop {
    public static void main(String[] args) {
        Class candyClass = Candy.class; // 当使用“.class”来创建堆Class对象的引用时 不会自动地初始化该Class对象
        System.out.println("C SweetShop M main() -> 这个时候加载了Candy.class 但是静态代码块没有执行");
        new Candy(); // 两次 new Candy() 其Class对象只会被加载1次
        new Candy();
        System.out.println("C SweetShop M main() -> After creating Candy");
        try {
            // 获取Candy类的Class对象
            System.out.println("CandyClass = " + Class.forName("bigbigmeng.online.SE_14_typeinfo.SE_14_2.Candy"));
            // 获取Gum类的Class对象
            Class.forName("bigbigmeng.online.SE_14_typeinfo.SE_14_2.Gum");
        } catch(ClassNotFoundException e) {
            // 获取不到 捕获异常
            System.out.println("C SweetShop M main() -> Couldn't find bigbigmeng.online.SE_14_typeinfo.SE_14_2.Gum");
        }
        System.out.println("C SweetShop M main() -> After Class.forName(\"Gum\")");
        new Cookie();
        System.out.println("C SweetShop M main() -> After creating Cookie");
    }
    /*
        C SweetShop M main() -> 这个时候加载了Candy.class 但是静态代码块没有执行
        ✅Loading Candy ⚡虽然创建了两个Candy对象 但是静态代码块只执行一次
        C SweetShop M main() -> After creating Candy
        ✅CandyClass = class bigbigmeng.online.SE_14_typeinfo.SE_14_2.Candy
        C SweetShop M main() -> After Class.forName("Gum")
        ✅Loading Cookie
        C SweetShop M main() -> After creating Cookie
    */
}

需要注意的是,我在使用Idea执行Class candyClass = Candy.class;的时候,Candy的静态代码块并没有执行,这是因为对于IDE来说,它只会加载和初始化实际使用到的类,以提高编译和运行效率。如果在执行Class candyClass = Candy.class;之后没有使用candyClass,那么Candy类的静态代码块就不会被执行。

🍭Class对象会在哪些情况下被加载?

  1. 创建对象:当通过new关键字创建一个类的实例时,该类的Class对象会被加载。
  2. 访问静态成员:当访问一个类的静态变量或调用类的静态方法时,该类的Class对象会被加载。
  3. 反射机制:通过Java反射机制获取类的Class对象时,该类的Class对象会被加载。

🍭Class对象包含哪些方法?

下面的实例中可以首先探索Class对象提供的5个方法

  • getName() 获取全类名
  • getSimpleName() 获取简单类名
  • getCanonicalName() 获取全类名
  • getInterfaces() 获取当前Class对象实现的所有接口的Class对象
  • getSuperClass() 获取上一级父类的Class对象
package bigbigmeng.online.SE_14_typeinfo.SE_14_2;

/**
@author Liu Xianmeng
@createTime 2023/9/23 12:55
@instruction
*/

@SuppressWarnings({"all"})
public class ToyTest {
    static void printInfo(Class cc) {
        System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
        System.out.println("Simple name: " + cc.getSimpleName());
        System.out.println("Canonical name : " + cc.getCanonicalName());
    }
    public static void main(String[] args) {
        Class c = null;
        try {
            c = Class.forName("bigbigmeng.online.SE_14_typeinfo.SE_14_2.FancyToy");
        } catch(ClassNotFoundException e) {
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        for(Class face : c.getInterfaces()) {
            printInfo(face);
        }
        Class up = c.getSuperclass();
        Object obj = null;
        try {
            // Requires default constructor:
            // 🎯newInstance()方法走的是默认构造器
            obj = up.newInstance();
        } catch(InstantiationException e) {
            System.out.println("Cannot instantiate");
            System.exit(1);
        } catch(IllegalAccessException e) {
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(obj.getClass());
    }
}

interface HasBatteries {}
interface Waterproof {}
interface Shoots {}

class Toy {
    // Comment out the following default constructor
    // to see NoSuchMethodError from (*1*)
    Toy() {} // ❌如果将这个无参的构造器注释掉 则无法使用newInstance()创建Toy()对象
    Toy(int i) {}
}

class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
    FancyToy() { super(1); }
}

⚡练习

⚡练习1 注释掉Toy类的Toy() {}

查看运行结果

class Toy {
    // Comment out the following default constructor
    // to see NoSuchMethodError from (*1*)
    Toy() {} // ❌如果将这个无参的构造器注释掉 则无法使用newInstance()创建Toy()对象
    Toy(int i) {}
}

⚡练习2 添加一个新的interface给FancyToy类

interface NewInterface {}

⚡练习3 添加一个新的子类Rhomboid(菱形)

将其强制类型转换为Square 看看会发生什么

public class Shapes {
    public static void main(String[] args) {
        List<Shape> shapeList = Arrays.asList(
            new Circle2(), new Square(), new Triangle()
        );
        for(Shape shape : shapeList) {
            shape.draw();
        }
        /*
            Circle2.draw()
            Square.draw()
            Triangle.draw()
         */
    }
}
abstract class Shape {
    void draw() { System.out.println(this + ".draw()"); }
    abstract public String toString();
}
class Circle2 extends Shape { public String toString() { return "Circle2"; }}
class Square extends Shape { public String toString() { return "Square"; }}
class Triangle extends Shape { public String toString() { return "Triangle"; }}

创建一个新的类

class Rhomboid extends Shape { public String toString() { return "Rhomboid"; }}

提示类型冲突 无法转换

⚡练习4 使用instanceOf检查类型

⚡练习5 实现Shapes中的rotate(Shape)方法 让它能判断自己旋转的是不是Circle

public void rotate(Shape shape) {
    if(shape instanceof Circle2) {
        System.out.println("可以进行循转");
    }
}

⚡练习6 写一个方法 接收一个对象 打印出其继承体系的所有父类对象全类名

public class Shapes {
    public static void main(String[] args) {
        Rhomboid rhomboid = new Rhomboid();
        printAllParentObjects(rhomboid, 1);
        /*
            第1层父类class = bigbigmeng.online.SE_14_typeinfo.SE_14_2.Shape
            第2层父类class = java.lang.Object
         */
    }
    public static void printAllParentObjects(Object obj, int count) {
        Class clazz = obj.getClass().getSuperclass();
        System.out.println("第" + count + "层父类class = " + clazz.getName());
        if(!clazz.getName().equals("java.lang.Object")) {
            printAllParentObjects(clazz, ++count);
        }
    }
}

🎄类字面常量

在Java中,类字面常量是指使用类的名称来引用该类的静态成员或获取对该类的Class对象的引用的方式。类字面常量使用类的名称加上.class后缀。

以下是一些使用类字面常量的示例:

引用静态成员:

int max = Integer.MAX_VALUE;

获取对类的Class对象的引用:

Class<?> clazz = String.class;

在这些示例中,Integer.MAX_VALUE引用了Integer类的静态成员MAX_VALUEString.class获取了String类的Class对象的引用。

类字面常量提供了一种简洁的方式来使用类的静态成员或获取对类的Class对象的引用,避免了通过实例化对象或使用反射来访问和操作类的成员。

需要注意的是,❗类字面常量只能用于编译时确定的类型。例如,不能将变量或方法的返回类型作为类字面常量的一部分使用。

有趣的一点是 当使用“.class”来创建堆Class对象的引用时 不会自动地初始化该Class对象 为了使用类而做的实际的准备工作包含下面的三个部分:

  1. 加载 这是由类加载器执行的 该步骤将查找字节码(通常在classpath所指定的路径中查找,但并不是必须的)并从这些字节码中创建一个Class对象
  2. 链接 在链接阶段将验证类中的字节码 为静态域分配存储空间 如果必需的话 还会解析这个类创建的对其他类的所有引用
  3. 初始化 如果该类具有超类 则需要先初始化超类 执行静态初始化器和静态初始化块

其中 初始化被延迟到对静态方法(构造器隐式是静态的)或者非常数静态域进行首次引用时才执行

看下面的代码示例:

🍭基本数据类型的类字面常量

类字面常量不仅可以用于普通方法 而且可以用于接口、数组以及基本数据类型

System.out.println(int.class);
System.out.println(Integer.TYPE);
System.out.println(int.class == Integer.TYPE);
/*
    int
    int
    true
 */
Licensed under CC BY-NC-SA 4.0