初识Java 17-1 反射

news2024/11/27 12:35:19

目录

反射的基本作用

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库中也极少使用)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1169078.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【前端笔记】ant-design-vue 3.x使用modal.method()自定义content内容小记

在一次编写业务代码时&#xff0c;碰到了一种既想要Modal.success样式&#xff0c;有想要定制其content内容的情况。 大部分情况下&#xff0c;使用Modal.method()这种方式时&#xff0c;可能content内容固定都是字符串&#xff0c;那如果想要做更高级的交互怎么办&#xff1f…

文件管理技巧:根据大小智能分类并移动至目标文件夹

在文件管理过程中&#xff0c;我们经常需要整理大量的文件。根据文件的大小&#xff0c;将其智能分类并移动至目标文件夹&#xff0c;可以帮助我们更高效地管理文件&#xff0c;提高工作效率。通过使用云炫文件管理器可以根据文件大小进行智能分类和移动至目标文件夹&#xff0…

【LeetCode】117. 填充每个节点的下一个右侧节点指针 II

117. 填充每个节点的下一个右侧节点指针 II 难度&#xff1a;中等 题目 给定一个二叉树&#xff1a; struct Node {int val;Node *left;Node *right;Node *next; }填充它的每个 next 指针&#xff0c;让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点&#xff0c…

【Linux】centos7安装配置及Linux常用命令

目录 一.Centos安装与配置 1.1.创建 1.2.安装配置CentOS 7实操 二.Linux常用命令 2.1.常用命令 2.2.三种模式 三.换源处理(切换国内源) 3.1.拍照备份 好啦今天就到这里哦&#xff01;&#xff01;希望能帮到你哦&#xff01;&#xff01;&#xff01; 一.Centos安装与配…

Ubuntu重启后进入initramfs导致无法开机解决方案

今天&#xff0c;我的电脑意外关机&#xff0c;重新开机后打开了虚拟机。该虚拟机使用的是 Ubuntu 22.04 系统。但重启后&#xff0c;系统一直显示(initramfs):&#xff0c;导致无法正常启动。最后&#xff0c;在网上查找了一些解决方案&#xff0c;成功解决了这个开机问题。在…

高等数学教材重难点题型总结(九)多元函数微分法及其应用

第九章习题总结完毕&#xff0c;最难的应该就是方程组求解隐函数中的雅可比行列式了&#xff0c;其他方面无论是期末还是考研都不会出太多难题。对于多元极限和连续性质要理解得更深刻一些&#xff0c;而方向导数、梯度等公式&#xff0c;应该熟练掌握~ 1. 写出多元函数的定义域…

Linux 将Qt程序打包为AppImage包

前言 在 Linux 环境下&#xff0c;开发完 Qt 程序后&#xff0c;也需要制作为一个安装包或者可执行文件进行分发。这里介绍使用 linuxdeployqt 将 Qt 程序打包为 .AppImage 应用程序&#xff08;类似于 Windows 的绿色免安装软件&#xff09; 环境配置 配置 Qt 环境变量 这…

docker compose实现容器编排

Compose 使用的三个步骤&#xff1a; 使用 Dockerfile 定义应用程序的环境 使用 compose.yml 定义构成应用程序的服务&#xff0c;这样它们可以在隔离环境中一起运行 最后&#xff0c;执行 docker compose up 命令来启动并运行整个应用程序 为什么需要docker compose Dock…

VBA快速动态考勤统计

实例需求&#xff1a;某公司的上下班打卡记录如下所示&#xff0c;其中Table_In为上班打卡记录&#xff0c;Table_Out为下班打卡记录。 现在需要根据日期整理为如下格式的考勤表。需要注意如下几点&#xff1a; 每天的打卡次数不确定最后一列Total/Day统计该天的出勤总时长&a…

服务器数据恢复—Zfs文件系统下文件被误删除的如何恢复数据?

服务器故障&#xff1a; 一台zfs文件系统服务器&#xff0c;管理员误操作删除服务器上的数据。 服务器数据恢复过程&#xff1a; 1、将故障服务器所有磁盘编号后取出&#xff0c;硬件工程师检测所有硬盘后没有发现有磁盘存在硬件故障。以只读方式将全部磁盘做扇区级别的镜像备…

如何有效搭建产品帮助中心?看这一篇文就够了!

在当今快节奏的数字化时代&#xff0c;产品帮助中心成为了企业提供优质客户支持和增强用户体验的重要组成部分。无论是软件、电子设备还是在线服务&#xff0c;用户都期望能够快速找到解决问题的方法和获得详细的产品指导。因此&#xff0c;搭建一个高效且易于使用的产品帮助中…

Canvas绘制简易雨滴碰撞效果

实现会动的图形&#xff0c;向下播放多张静态的图片。一秒内要大于屏幕刷新的帧数(60) 也就是每隔1/60s执行一次函数在每次绘制的正方形上添加一个背景色为白色蒙板。 效果图 源代码 <!DOCTYPE html> <html lang"en"><head><meta charset"…

数据库进阶教学——数据库故障恢复(日志文件)

目录 一、日志简介 二、日志文件操作 1、查看日志状态 2、开启日志功能 3、查看日志文件 4、查看当前日志 5、查看日志中的事件 6、删除日志文件 7、查看和修改日志文件有效期 8、查看日志文件详细信息 三、删除的数据库恢复 一、日志简介 日志是记录所有数据库表结…

全国首批!中国儿童青少年戏剧艺术普及推广中心——福建省艺术馆、福州市文化馆推广中心授牌仪式在福州举办

2023年11月1日&#xff0c;由中国儿童艺术剧院、文化和旅游部全国公共文化发展中心主办&#xff0c;福建省文化和旅游厅支持&#xff0c;福建省艺术馆、福州市文化和旅游局承办&#xff0c;福州市文化馆协办的“中国儿童青少年戏剧艺术普及推广中心——福建省艺术馆、福州市文化…

Centos 7.x上利用certbot申请Let‘s Encrypt的SSH证书(HTTPS证书)

目录 01-安装Certbot02-在网站的根目录依次新建文件夹.well-known和acme-challenge03-申请证书 要在CentOS 7.x上为域名申请Let’s Encrypt证书&#xff0c;你可以使用Certbot工具&#xff0c;它是一个自动化证书颁发工具&#xff0c;用于管理Let’s Encrypt证书。以下是在Cent…

天津优选Java培训机构 影响Java培训费用的因素

Java作为如今流行的计算机编程语言&#xff0c;其优势在于言语简略、面向对象&#xff0c;并且应用广泛。随着市场对于Java开发人员的需求越来越大&#xff0c;越来越多非本专业的人也通过培训转行进入IT行业。 Java的就业优势 市场需求大&#xff1a;Java人才的市场需求很大…

高匿IP有什么作用

在互联网的蓬勃发展中&#xff0c;IP地址作为网络通信的基础&#xff0c;一直扮演着举足轻重的角色。而在诸多IP地址中&#xff0c;高匿IP地址则是一种特殊类型&#xff0c;其作用和价值在某些特定场合下尤为突出。那么&#xff0c;高匿IP地址究竟有哪些用处呢&#xff1f; 首先…

无声的世界,精神科用药并结合临床的一些分析及笔记(十)

目录 回 “ 家 ” 克服恐惧 奥沙西泮 除夕 酒与药 警告 离别 回 “ 家 ” 她的锥切手术进行的很顺利&#xff0c;按计划继续返回安定医院调节心理状态&#xff0c;病友们都盼着我们回“家”。当我俩跨入病区&#xff0c;大家都涌过来帮我们大包小包的拎着行李&#xff0…

Airtest工具根据App页面文字信息提取坐标进行截图保存在自定义文件夹

Airtest工具根据App页面文字信息提取坐标进行截图保存在自定义文件夹 一、项目背景 在一个项目中&#xff0c;选项被选中和未选中的节点元素的属性值无变化&#xff0c;通过AI识别率达不到百分百&#xff0c;想着通过计算图片的HSV值来判断选择能否被选中。&#xff08;HSV比…