初识Java 17-1 反射

目录

反射的基本作用

Class对象

类字面量

泛型类的引用

cast()方法


本笔记参考自: 《On Java 中文版》


||| 反射可以在程序运行时发现并使用对象的类型信息。

        反射的存在使Java的编程不再局限于面向类型的操作。这一特性有利有弊,在深入Java之前,我们需要先了解它。

        Java中的反射一般可以分为两种:

  • 简单反射:这种反射假定程序员在编程时就已经知道所有可用的类型,并根据这一假设形成各种特性。
  • 复杂发射:这种反射允许我们在运行时发现和使用类。

反射的基本作用

        反射可以在运行时确定对象的类型。为了详细地说明这点,这里需要引入一个常见的案例 —— Shape

这是一个典型的类层次结构:顶部的基类,以及向下扩展的子类。在面向对象的编程中,我们会希望只通过基类的引用就能操作我们的代码。这么做的好处是,即使之后添加新的代码,原有的代码也不会受到太多影响。

        根据Shape的层次结构,我们可以编写这样的代码:

【例子:构建典型的层次结构】

import java.util.stream.Stream;

abstract class Shape {
    void draw() {
        System.out.println(this + ".draw()");
    }

    // 将toString()定义为抽象类,这样就可以强制子类重写该方法
    @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) {
        Stream.of(new Circle(), new Square(), new Triangle())
                .forEach(Shape::draw);
    }
}

        程序执行的结果是:

        基类的draw()会通过this间接调用toString()方法,这样就可以打印各个子类的标识符了。

        这里需要注意的是main()方法,Stram.of()方法存储了Shape的各个子类对象,这就相当于将子类对象放入了一个Stream<Shape>中。

在这一向上转型的过程中,对象的确切类型信息丢失了。对于流而言,这些都是Shape对象。

    从技术的角度出发,这一过程实际上是Stream<Shape>将所有内容都当作Object保存。当取出一个元素时,再将它转换回ShapeObjectShape之间的转换体现了最基本的反射,这种反射会检查所有的类型转换是否正确

    事实上这里的反射类型转换并不彻底,因为Object只是转换成了Shape,而不是任何更具体的子类(这是因为Stream<Shape>中存储的就是Shape)。

---

        一般而言,在构建了类的层次结构后,就是多态发挥作用了:通过与这类对象的通用表示(基类,在本例中是Shape)打交道,我们可以生产出更方便的代码。但是凡事总有例外,假设:

    我们需要知道一个被泛化的引用的具体类型,以此来解决某一编程问题。

例如:我需要一个“旋转”图形的方法,但旋转圆形并无意义,我想要跳过它。这里,反射就发挥了作用:通过反射,我们可以查询到某个Shape引用所指的具体类型,并对其进行特殊处理。

        接下来将介绍如何通过反射获得具体类型。

Class对象

        Java中的反射依赖于Class对象的这一特殊对象完成,这一对象会包含类相关的信息。

Class对象被用来创建类的所有“常规”对象。Java通过Class执行反射,这包括了类型转换等操作。除此之外,Class类中还有许多使用反射的方法。

    每编写并编译一个新类,JVM为了生成这个对象,都会使用其被称为类加载器的子系统,这时就会生成一个Class对象(并且将其存储在对应的.class文件中)。

    类会在首次被使用时动态地加载到JVM中(例如引用了一个该类的静态成员)。因为构造器是隐式的静态方法,因此构造器的初次使用也会引发对应类的加载。因此,Java程序是在必要时加载对应部分

        类加载器会首先检查是否加载了该类的Class对象,若不存在,默认的类加载器会去.class文件中寻找对应类的字节数据。一旦Class对象被加载,它就会被用于创建该类型的所有对象。

【例子:类加载器的工作顺序】

package reflection;

class Cookie {
    static {
        System.out.println("加载Cookie类");
    }
}

class Gum {
    static {
        System.out.println("加载Gum类");
    }
}

class Candy {
    static {
        System.out.println("加载Candy类");
    }
}

public class SweetShop {
    public static void main(String[] args) {
        System.out.println("在main()方法中");

        new Candy();
        System.out.println("创建完Candy对象后");

        try {
            // forName()寻找Gum类,引发该类的加载,并执行static块
            Class.forName("reflection.Gum");
        } catch (ClassNotFoundException e) {
            System.out.println("找不到Gum类");
        }
        System.out.println("在执行完 forName("Gum") 后");

        new Cookie();
        System.out.println("创建为Cookie对象后");
    }
}

        程序执行的结果是:

        因为Class对象总是在需要时被加载,因此当我们创建对象时,类加载器它们加载到了JVM中。另外,静态代码库的初始化是在类加载的时候执行的。

        上述例子中有一条语句不同于其他两个类的加载:

Class对象同样可以被创建,并通过引用使用它的方法。forName()方法可以通过获取一个文本名称,返回一个Class引用。除此之外,这个方法也会加载Class对象。

---

        这里简单介绍一些Class类的方法:

  • forName():根据字符串参数生成一个Class对象。其中,参数必须是类的完全限定名称(包括包名)。
  • getInterfaces():返回一个Class对象数组,表示引用该方法的Class对象的所有接口。

        尽管不属于Class类,但这里需要提一下Object.getClass()。这一方法可以返回一个Class引用,这一引用表示的就是这个对象的实际类型。

        还有一些专门获取名称的方法:

  • getName():生成完全限定的类名。
  • getSuperName():查询Class对象的直接基类。
  • getSimpleName():生成不带包的名称。
  • getCanonicalName():生成完全限定的名称。

除此之外,还有一些接下来的例子会用到的方法:

  • isInterface():顾名思义,用于判断这个Class对象是否是一个接口。
  • newInstance():该方法用于创建实例,返回的引用可用Object接收。(Java 8以上已弃用该方法,可使用Constructor.newInstance()替代。)

【例子:Class类的一些方法】

package reflection.toys;

interface HasBatteries {
}

interface Waterproof {
}

interface Shoots {
}

class Toy {
    // 之后出现的Class.newInstance()方法需要一个无参构造器
    // 因此这里需要定义一个:
    public Toy() {
    }

    public Toy(int i) {
    }
}

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

public class ToyTest {
    static void printInfo(Class cc) {
        System.out.println("类名:" + cc.getName() +
                ",是否是接口?[" + cc.isInterface() + "]");
        System.out.println("简易类名:" + cc.getSimpleName());
        System.out.println("完整类名:" + cc.getCanonicalName());
    }

    // 由于Class.newInstance()在更高版本中以被弃用
    // 因此需要通过@SuppressWarnings来抑制弃用警告
    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        Class c = null;
        try {
            c = Class.forName("reflection.toys.FancyToy");
        }catch (ClassNotFoundException e){
            System.out.println("无法找到FancyToy类");
            System.exit(1);
        }
        printInfo(c);

        System.out.println();
        for (Class face:c.getInterfaces())
            printInfo(face);

        System.out.println();
        Class up = c.getSuperclass();
        Object obj = null;
        try {
            obj = up.newInstance(); // 该方法需要public的无参构造器
        }catch (Exception e){
            throw new RuntimeException("无法实例化");
        }
        printInfo(obj.getClass());
    }
}

        程序执行的结果是:

        这里再提一下newInstance()方法。

这一方法可被用于“虚构构造器”,当我们不知道确切的类型时,可以尝试使用它进行对象创建。除此之外,若使用该方法创建实例,需要调用该方法的对象存在一个public的无参构造器。顺便一提,若未定义所需的构造器,则会触发以下的报错:

类字面量

        Java还提供了另一种方法来生成Class对象的引用:类字面量。引用上面的例子,可以创建一个FancyToy类的类字面量:

FancyToy.class;

因为这种方式接受编译时检查,因此它会更加安全。

        类字面量适用于常规类、接口、数组和基本类型。除此之外,每个基本包装类都有一个名为TYPE的标准字段,用于指向一个和基本类型对应的Class对象的引用。例如:

boolean.class // 基本的boolean类型
Boolean.TYPE // 包装类使用TYPE获取Class引用

    .class是一个更好的选择,应为它与常规类更一致。

        注意:使用“.class”创建的Class对象不会自动初始化。一个类在被使用之前,会经历以下三个步骤:

  1. 加载:此时,类加载器根据字节码创建一个Class对象。
  2. 链接:该阶段会验证类中的字节码,并且为静态字段分配空间,并在必要时解析该类对其他类的所有引用。
  3. 初始化:若有基类,则先初始化基类,在执行静态初始化器和静态初始化块。

【例子:滞后的初始化】

package reflection;

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("初始化Initable类");
    }
}

class Initable2 {
    static int staticNonFinal = 147;

    static {
        System.out.println("初始化Initable2类");
    }
}

class Initable3 {
    static int staticNonFinal = 74;

    static {
        System.out.println("初始化Initable3类");
    }
}

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

    public static void main(String[] args)
            throws Exception {
        Class initable = Initable.class;
        System.out.println("创建完Initable的反射后");
        // 不会触发初始化:编译时常量不会触发初始化
        System.out.println(Initable.STATIC_FINAL);
        // 会触发初始化:调用方法
        System.out.println(Initable.STATIC_FINAL2);
        System.out.println();

        // 会触发初始化:非final字段
        System.out.println(Initable2.staticNonFinal);
        System.out.println();

        Class initable3 = Class.forName("reflection.Initable3");
        System.out.println("创建完Initable3的反射后");
        System.out.println(Initable3.staticNonFinal);
    }
}

        程序执行的结果是:

        实际上,初始化是“尽可能懒惰的”

        在上述程序中,可以看见:对Initable.STATIC_FINAL的使用不会触发初始化,因为这是一个编译时常量。但Initable.STATIC_FINAL2不是,因此会触发强制的类的初始化(具体而言,是先链接,后初始化)


泛型类的引用

        Class对象可用于生成类的实例,这些实例会包含了该类的代码、静态字段和静态方法。一个Class引用表示的就是其指向的确切类型:Class类的一个对象。

【例子:Class引用指向的实例】

public class ClassInstance {
    public static void main(String[] args) {
        Class<?> cl = String.class;
        System.out.println(cl);
    }
}

        程序执行的结果是:

---

        通过泛型语法可以限制Class引用的类型。

【例子:通过泛型语法限制Class

public class GenericClassReferences {
    public static void main(String[] args) {
        Class intClass = int.class;
        intClass = double.class;

        // 两种语法是一致的
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class;
        // 但这样不行
        // genericIntClass = double.class;
    }
}

        通过泛型语法,可以让编译器强制执行额外的类型检查(实际上这也是将泛型语法加入到Class引用中的原因之一)。下面的是就是IDEA的检查警告:

        另外,我们也可以使用通配符?(这一通配符表示“任何事物”)来放松泛型带来的限制,因此我们可以这样编写代码:

【例子:使用通配符?放宽限制】

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

        虽然也可以使用普通的Class,但这么做能够更好地表达我们的代码意图,告诉读者我们不是故意放宽限制的

    想要通过这种代码放宽限制是行不通的:

Class<Number> genericIntClass = int.class;

这是因为尽管NumberInteger的基类,但Class<Number>Class<Integer>却是毫无关系到。

        通配符?也可以和其他关键字组合使用,这将划定一个界限。这里先以Shape层次结构为例:

  • ? extends Shape:将泛型的类型限制为Shape类或其的任意子类型
  • ? super Circle:将泛型的类型限制为Circle类或其的任何父类

接下来通过这种方式再对之前的例子进行修改。

【例子:组合通配符?和关键字】

public class BoundedClassReferences {
    public static void main(String[] args) {
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
        // 可以是任何继承了Number的类
    }
}

        接下来再看看这种泛型语法的实际运用,这里还运用了newInstance()来生成对象:

【例子:Class的泛型语法运用】

package reflection;

import java.util.function.Supplier;
import java.util.stream.Stream;

class ID {
    private static long counter;
    private final long id = counter++;

    @Override
    public String toString() {
        return Long.toString(id);
    }

    // getConstructor().newInstance()需要的public的无参构造器
    public ID() {
    }
}

public class DynamicSupplier<T> implements Supplier<T> {
    private Class<T> type;

    public DynamicSupplier(Class<T> type) {
        this.type = type;
    }

    @Override
    public T get() {
        try {
            return type.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Stream.generate(
                        new DynamicSupplier<>(ID.class))
                .skip(10)
                .limit(5)
                .forEach(System.out::println);
    }
}

        程序执行的结果是:

        由于ID类并非public的,因此其默认的无参构造器也是非public的。为了让newInstance()方法能够正常执行,我们需要显式地定义一个无参构造器。

        从程序的输出结果可以看出,对于一个使用了泛型语法的Class对象,newInstance()生成了具体的类型,而不仅仅是Object。不过正如之前演示的Class.newInstance()一样,这一新的方法也会受到一定的限制。

【例子:受限制的newInstance()

        这里重复利用了之前ToyTest.java的类:

package reflection.toys;

public class GenericToyTest {
    public static void main(String[] args)
            throws Exception {
        Class<FancyToy> ftc = FancyToy.class;
        //会生成确切的类型:
        FancyToy fancyToy =
                ftc.getConstructor().newInstance();

        // 允许的声明:
        Class<? super FancyToy> up = ftc.getSuperclass();
        // 但这种做法无法通过:
        // Class<Toy> up2 = ftc.getSuperclass();

        // up生成的实例只能由Object承接:
        Object obj = up.getConstructor().newInstance();
    }
} 

        通过getSuperclass()方法可以获得基类,但编译器只允许我们将对应的基类引用声明为“FancyToy的某个基类”,而不能直接声明为“Toy”。这似乎表明编译器在揣着明白装糊涂。也因为这个原因,语句

up.getConstructor().newInstance()

返回的不是一个具体的引用,而是一个Object


cast()方法

        Class还有一个用于类型转换的方法:cast()

【例子:使用cast()进行类型转换】

class Building {}

class House extends Building {}

public class ClassCasts {
    public static void main(String[] args) {
        Building b = new House();
        Class<House> houseType = House.class;

        // 两种类型转换的方式
        House h = houseType.cast(b);
        h = (House) b;
    }
}

        相比于使用圆括号(House)进行的强制转换,cast()似乎更加麻烦。但当我们无法使用普通类型转换时,cast()就可以发挥它的作用了(话虽如此,其实这一方法在整个Java库中也极少使用)。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>