Java反射机制详解

CONTENTS

  • 1. 为什么需要反射
  • 2. Class对象
    • 2.1 类字面量
    • 2.2 泛型类的引用
    • 2.3 cast()方法

反射使我们摆脱了只能在编译时执行面向类型操作的限制,并且让我们能够编写一些非常强大的程序。本文将讨论 Java 是如何在运行时发现对象和类的信息的,这通常有两种形式:简单反射,它假定你在编译时就已经知道了所有可用的类型;以及更复杂的反射,它允许我们在运行时发现和使用类的信息。

1. 为什么需要反射

面向对象编程的一个基本目标就是,让编写的代码只操纵基类的引用。我们来看下面这个例子:

package reflection;import java.util.stream.Stream;abstract class Shape {void draw() {System.out.println(this + ".draw()");}@Overridepublic abstract String toString();
}class Circle extends Shape {@Overridepublic String toString() { return "Circle"; }
}class Square extends Shape {@Overridepublic String toString() { return "Square"; }
}class Triangle extends Shape {@Overridepublic String toString() { return "Triangle"; }
}public class Shapes {public static void main(String[] args) {Stream.of(new Circle(), new Square(), new Triangle()).forEach(Shape::draw);/** Circle.draw()* Square.draw()* Triangle.draw()*/}
}

Shape 接口中的方法 draw() 是可以动态绑定的,因此客户程序员可以通过泛化的 Shape 引用来调用具体的 draw() 方法。在所有子类中,draw() 都被重写,并且因为它是一个动态绑定的方法,即使通过泛化的 Shape 引用来调用它,也会产生正确的行为,这就是多态。

基类里包含一个 draw() 方法,它通过将 this 传递给 System.out.println(),间接地使用了 toString() 方法来显示类的标识符(toString() 方法被声明为 abstract 的,这样就可以强制子类重写该方法,并防止没什么内容的 Shape 类被实例化)。

在此示例中,将一个 Shape 的子类对象放入 Stream<Shape> 时,会发生隐式的向上转型,在向上转型为 Shape 时,这个对象的确切类型信息就丢失了,对于流来说,它们只是 Shape 类的对象。

从技术上讲,Stream<Shape> 实际上将所有内容都当作 Object 保存。当一个元素被取出时,它会自动转回 Shape,这是反射最基本的形式,在运行时检查了所有的类型转换是否正确,这就是反射的意思:在运行时,确定对象的类型。

在这里,反射类型转换并不彻底:Object 只是被转换成了 Shape,而没有转换为最终的 CircleSquareTriangle。这是因为我们所能得到的信息就是,Stream<Shape> 里保存的都是 Shape,在编译时,这是由 Stream 和 Java 泛型系统强制保证的,而在运行时,类型转换操作会确保这一点。

接下来就该多态上场了,Shape 对象实际上执行的代码,取决于引用是属于CircleSquare 还是 Triangle。一般来说,这是合理的:你希望自己的代码尽可能少地知道对象的确切类型信息,而只和这类对象的通用表示(在本例中为Shape)打交道。这样的话我们的代码就更易于编写、阅读和维护,并且设计也更易于实现、理解和更改。所以多态是面向对象编程的一个基本目标。

2. Class对象

要想了解 Java 中的反射是如何工作的,就必须先了解类型信息在运行时是如何表示的。这项工作是通过叫作 Class 对象的特殊对象来完成的,它包含了与类相关的信息。事实上,Class 对象被用来创建类的所有“常规”对象,Java 使用 Class 对象执行反射,即使是类型转换这样的操作也一样。Class 类还有许多其他使用反射的方式。

程序中的每个类都有一个 Class 对象,也就是说,每次编写并编译一个新类时,都会生成一个 Class 对象(并被相应地存储在同名的 .class 文件中)。为了生成这个对象,Java 虚拟机(JVM)使用被称为类加载器(class loader)的子系统。

类加载器子系统实际上可以包含一条类加载器链,但里面只会有一个原始类加载器,它是 JVM 实现的一部分。原始类加载器通常从本地磁盘加载所谓的可信类,包括 Java API 类。

类在首次使用时才会被动态加载到 JVM 中。当程序第一次引用该类的静态成员时,就会触发这个类的加载(构造器是类的一个静态方法,尽管没有明确使用 static 关键字)。因此,使用 new 操作符创建类的新对象也算作对该类静态成员的引用,构造器的初次使用会导致该类的加载。

所以,Java 程序在运行前并不会被完全加载,而是在必要时加载对应的部分,这与许多传统语言不同,这种动态加载能力使得 Java 可以支持很多行为。

类加载器首先检查是否加载了该类型的 Class 对象,如果没有,默认的类加载器会定位到具有该名称的 .class 文件(例如,某个附加类加载器可能会在数据库中查找对应的字节码)。当该类的字节数据被加载时,它们会被验证,以确保没有被损坏,并且不包含恶意的 Java 代码(这是 Java 的众多安全防线里的一条)。

一旦该类型的 Class 对象加载到内存中,它就会用于创建该类型的所有对象:

package reflection;class Cookie {static {System.out.println("Loading Cookie");}
}class Gum {static {System.out.println("Loading Gum");}
}class Candy {static {System.out.println("Loading Candy");}
}public class SweetShop {public static void main(String[] args) {System.out.println("Inside main");new Cookie();System.out.println("After creating Cookie");try {Class<?> c = Class.forName("reflection.Gum");  // 一定要完整类名,即包名+类名System.out.println("c.getName(): " + c.getName());System.out.println("After Class.forName(reflection.Gum)");} catch (ClassNotFoundException e) {System.out.println(e);}new Candy();System.out.println("After creating Candy");/** Inside main* Loading Cookie* After creating Cookie* Loading Gum* c.getName(): reflection.Gum* After Class.forName(reflection.Gum)* Loading Candy* After creating Candy*/}
}

我们创建了三个具有静态代码块的类,该静态代码块会在第一次加载类时执行,输出的信息会告诉我们这个类是什么时候加载的。输出结果显示了 Class 对象仅在需要时才加载,并且静态代码块的初始化是在类加载时执行的。

所有的 Class 对象都属于 Class 类,Class 对象和其他对象一样,因此你可以获取并操作它的引用(这也是加载器所做的)。静态的 forName() 方法可以获得 Class 对象的引用,该方法接收了一个包含所需类的文本名称(注意拼写和大小写,且需要是类的完全限定名称,即包括包名称)的字符串,并返回了一个 Class 引用。

不管什么时候,只要在运行时用到类型信息,就必须首先获得相应的 Class 对象的引用,这时 Class.forName() 方法用起来就很方便了,因为不需要对应类型的对象就能获取 Class 引用。但是,如果已经有了一个你想要的类型的对象,就可以通过 getClass() 方法来获取 Class 引用,这个方法属于 Object 根类,它返回的 Class 引用表示了这个对象的实际类型。

Class 类有很多方法,下面是其中的一部分:

package reflection;interface Waterproof {}
interface Shoots {}class Toy {public Toy() {System.out.println("Creating Toy");}public Toy(int i) {}
}class FancyToy extends Toy implements Waterproof, Shoots {public FancyToy() { super(1); }
}public class ClassMethods {static void printInfo(Class c) {System.out.println("getName(): " + c.getName());System.out.println("isInterface(): " + c.isInterface());System.out.println("getSimpleName(): " + c.getSimpleName());System.out.println("getCanonicalName(): " + c.getCanonicalName());}public static void main(String[] args) {Class<?> c = null;try {c = Class.forName("reflection.FancyToy");} catch (ClassNotFoundException e) {System.out.println(e);System.exit(1);}printInfo(c);for (Class iface: c.getInterfaces()) {System.out.println("--------------------");printInfo(iface);}Class sc = c.getSuperclass();Object obj = null;System.out.println("--------------------");try {obj = sc.newInstance();  // 对应类要有public的无参构造器} catch (Exception e) {throw new RuntimeException("Can't instantiate");}printInfo(obj.getClass());  // obj.getClass()即为sc/** getName(): reflection.FancyToy* isInterface(): false* getSimpleName(): FancyToy* getCanonicalName(): reflection.FancyToy* --------------------* getName(): reflection.Waterproof* isInterface(): true* getSimpleName(): Waterproof* getCanonicalName(): reflection.Waterproof* --------------------* getName(): reflection.Shoots* isInterface(): true* getSimpleName(): Shoots* getCanonicalName(): reflection.Shoots* --------------------* Creating Toy* getName(): reflection.Toy* isInterface(): false* getSimpleName(): Toy* getCanonicalName(): reflection.Toy*/}
}

printInfo() 方法使用 getName() 来生成完全限定的类名,使用 getSimpleName()getCanonicalName() 分别生成不带包的名称和完全限定的名称,isInterface() 可以告诉你这个 Class 对象是否表示一个接口,getInterfaces() 方法返回了一个 Class 对象数组,它们表示所调用的 Class 对象的所有接口。还可以使用 getSuperclass() 来查询 Class 对象的直接基类,它将返回一个 Class 引用,而你可以对它做进一步查询。

ClassnewInstance() 方法是实现虚拟构造器的一种途径,这相当于声明:我不知道你的确切类型,但无论如何你都要正确地创建自己。sc 只是一个 Class 引用,它在编译时没有更多的类型信息,当创建一个新实例时,你会得到一个 Object 引用,但该引用指向了一个 Toy 对象,你可以给它发送 Object 能接收的消息,但如果想要发送除此之外的其他消息,就必须进一步了解它,并进行某种类型转换。此外,使用 Class.newInstance() 创建的类必须有一个无参构造器。

注意,此示例中的 newInstance() 在 Java 8 中还是正常的,但在更高版本中已被弃用,Java 推荐使用 Constructor.newInstance() 来代替。

2.1 类字面量

Java 还提供了另一种方式来生成 Class 对象的引用:类字面量。它看起来像这样:

FancyToy.class

这更简单也更安全,因为它会进行编译时检查(因此不必放在 try 块中),另外它还消除了对 forName() 方法的调用,所以效率也更高。

注意,使用 .class 的形式创建 Class 对象的引用时,该 Class 对象不会自动初始化。实际上,在使用一个类之前,需要先执行以下三个步骤:

  • 加载:这是由类加载器执行的,该步骤会先找到字节码(通常在类路径中的磁盘上,但也不一定),然后从这些字节码中创建一个 Class 对象。
  • 链接:链接阶段会验证类中的字节码,为静态字段分配存储空间,并在必要时解析该类对其他类的所有引用。
  • 初始化:如果有基类的话,会先初始化基类,执行静态初始化器和静态初始化块。

其中,初始化会被延迟到首次引用静态方法(构造器是隐式静态的)或非常量静态字段时:

package reflection;class A {static final int STATIC_FINAL = 1;static int x = 2;static {System.out.println("Initializing A");}
}class B {static {System.out.println("Initializing B");}
}public class ClassInitialization {public static void main(String[] args) throws ClassNotFoundException {System.out.println("Inside main");Class a = A.class;  // 不会初始化System.out.println("After creating A ref");System.out.println("A.STATIC_FINAL: " + A.STATIC_FINAL);  // 还是不会初始化System.out.println("A.x: " + A.x);  // 初始化Class b = Class.forName("reflection.B");  // 初始化System.out.println("After creating B ref");/** Inside main* After creating A ref* A.STATIC_FINAL: 1* Initializing A* A.x: 2* Initializing B* After creating B ref*/}
}

仅使用 .class 语法来获取对类的引用不会导致初始化,而 Class.forName() 会立即初始化类以产生 Class 引用。如果一个 static final 字段的值是编译时常量,比如 A.STATIC_FINAL,那么这个值不需要初始化 A 类就能读取。

2.2 泛型类的引用

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

我们可以使用泛型语法来限制 Class 引用的类型:

package reflection;public class GenericClassReferences {public static void main(String[] args) {Class c1 = int.class;c1 = double.class;  // 合法Class<Integer> c2 = int.class;c2 = Integer.class;  // 合法c2 = Double.class;  // 不合法}
}

泛化的类引用 c2 只能分配给其声明的类型,通过使用泛型语法,可以让编译器强制执行额外的类型检查。

如果想放松使用泛化的 Class 引用时的限制,需要使用通配符 ?,它是 Java 泛型的一部分,表示任何事物:

package reflection;public class GenericClassReferences {public static void main(String[] args) {Class<?> c = Integer.class;c = Double.class;  // 合法}
}

我们不能这么写:

Class<Number> c = Integer.class;

即使 Integer 继承自 Number,但是 IntegerClass 对象不是 NumberClass 对象的子类。

如果想创建一个 Class引用,并将其限制为某个类型或任意子类型,可以将通配符与 extends 关键字组合来创建一个界限

package reflection;public class GenericClassReferences {public static void main(String[] args) {Class<? extends Number> c = Integer.class;c = Double.class;  // 合法}
}

将泛型语法添加到 Class 引用的一个原因是提供编译时的类型检查,这样的话,如果你做错了什么,那么很快就能发现。

下面是一个使用了泛型类语法的示例,它存储了一个类引用,然后使用 newInstance() 来生成对象:

package reflection;import java.util.function.Supplier;
import java.util.stream.Stream;class People {private static long counter;private final long id = counter++;public People() {}  // 需要有public的无参构造器才能调用newInstance()@Overridepublic String toString() {return "People " + id;}
}public class DynamicSupplier<T> implements Supplier<T> {private Class<T> c;public DynamicSupplier(Class<T> c) { this.c = c; }@Overridepublic T get() {try {return c.getConstructor().newInstance();} catch (Exception e) {throw new RuntimeException(e);}}public static void main(String[] args) {Stream.generate(new DynamicSupplier<>(People.class)).skip(5).limit(5).forEach(System.out::println);/** People 5* People 6* People 7* People 8* People 9*/}
}

DynamicSupplier 会强制要求它使用的任何类型都有一个 public 的无参构造器,如果不符合条件,就会抛出一个异常。在上面的例子中,People 类自动生成的无参构造器不是 public 的,因为 People 类不是 public 的,所以我们必须显式定义它。

Class 对象使用泛型语法时,newInstance() 会返回对象的确切类型,而不仅仅是简单的 Object,但它也会受到一些限制:

package reflection;class Cat {public Cat() {}
}class Kitty extends Cat {public Kitty() {}
}public class GenericCat {public static void main(String[] args) throws Exception {Class<Kitty> kittyClass = Kitty.class;Kitty kitty = kittyClass.getConstructor().newInstance();System.out.println(kitty.getClass().getName());  // reflection.Kitty//        Class<Cat> kittySuper = kittyClass.getSuperclass();  // 不合法Class<? super Kitty> kittySuper = kittyClass.getSuperclass();Object obj = kittySuper.getConstructor().newInstance();System.out.println(obj.getClass().getName());  // reflection.Cat}
}

如果你得到了 Kitty 的基类,那么编译器只允许你声明这个基类引用是 Kitty某个基类,即 Class<? super Kitty>,而不能被声明成 Class<Cat>,因为 getSuperclass() 返回了基类(不是接口),而编译器在编译时就知道这个基类是什么,在这里就是 Cat.class,而不仅仅是 Kitty 的某个基类。因为存在这种模糊性,所以 kittySuper.getConstructor().newInstance() 的返回值不是一个确切的类型,而只是一个 Object

2.3 cast()方法

cast() 方法是用于 Class 引用的类型转换:

package reflection;class House {}class BigHouse extends House {}public class ClassCast {public static void main(String[] args) {House h = new BigHouse();Class<BigHouse> bigHouseClass = BigHouse.class;BigHouse bh = bigHouseClass.cast(h);
//        BigHouse bh = (BigHouse)h;  // 这种方式更简洁}
}

cast() 方法接收参数对象并将其转换为 Class 引用的类型,在你不能使用普通类型转换(最后一行)的情况下很有用,如果你正在编写泛型代码并且存储了一个用于转型的 Class 引用,就可能会遇到这种情况,不过这很罕见。

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

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

相关文章

基于Qt命令行处理XML文件读写

Qt源码在后面,文本介绍Qt国际化语言和XML # XML基础(一) ## 1、概述 ### 1.1 定义(xml是个啥玩意儿?) XML(extensible Markup Language)俗称差妹儿,专业称之为:可拓展标记语言。 (1)何为标记,指的是一种标记语言,或者标签语言,即用一系列的标签来对数据进行…

C++中声明和实现析构函数

C中声明和实现析构函数 与构造函数一样&#xff0c;析构函数也是一种特殊的函数。构造函数在实例化对象时被调用&#xff0c;而析构函数在对象销毁时自动被调用。 析构函数看起来像一个与类同名的函数&#xff0c;但前面有一个腭化符号&#xff08; &#xff5e;&#xff09;…

Linux常用命令——chage命令

在线Linux命令查询工具 chage 修改帐号和密码的有效期限 补充说明 chage命令是用来修改帐号和密码的有效期限。 语法 chage [选项] 用户名选项 -m&#xff1a;密码可更改的最小天数。为零时代表任何时候都可以更改密码。 -M&#xff1a;密码保持有效的最大天数。 -w&…

数字媒体技术基础之:ICC 配置文件

ICC 配置文件&#xff08;也称为 ICC 色彩配置文件或 ICC 色彩描述文件&#xff09;是由国际色彩联盟&#xff08;International Color Consortium, ICC&#xff09;制定的一种标准文件格式&#xff0c;用于在不同的设备和软件之间保持颜色的一致性。 ICC 配置文件包含有关设备…

Ubuntu中nano使用

1.nano配置文件 sudo nano /etc/nanorc2.显示每行得数字 //etc/nanorc配置文件中设置 set linenumbers //取消注释使用Nano编辑器入门指南 引言 1.1 关于Nano编辑器 Nano是一个简单易用的文本编辑器&#xff0c;适用于终端环境。它具有轻量级、快速启动和基本功能的特点&…

项目实战:通过axios加载水果库存系统的首页数据

1、创建静态页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><link rel"stylesheet" href"style/index.css"><script src"script/axios.mi…

2023-11-04:用go语言,如果n = 1,打印 1*** 如果n = 2,打印 1*** 3*** 2*** 如果n = 3,打印

2023-11-04&#xff1a;用go语言&#xff0c;如果n 1&#xff0c;打印 1*** 如果n 2&#xff0c;打印 1***3*** 2*** 如果n 3&#xff0c;打印 1***3*** 2***4*** 5*** 6*** 如果n 4&#xff0c;打印 1***3*** 2***4*** 5*** 6***10** 9*** 8*** 7*** 输入…

EASYX图片操作

easyx学习网址 建议使用谷歌搜索引擎搜索相关的资料 eg1:图片显示到桌面 #include <stdio.h> #include <easyx.h> #include <iostream> #include <math.h> #include <stdlib.h> #include <conio.h> #include <time.h> #define PI 3…

2.8 CSS 伸缩盒模型

1.模型简介 传统布局是指:基于传统盒状模型&#xff0c;主要靠: display 属性 position 属性float 属性。2009年&#xff0c;w3C提出了一种新的盒子模型——Flexible Box(伸缩盒模型&#xff0c;又称:弹性盒子)。它可以轻松的控制:元素分布方式、元素对齐方式、元素视觉顺序..…

DBeaver关闭代码的提示

在DBeaver中会遇到如下现象&#xff0c;很烦&#xff0c;怎么取消这个提示框呢&#xff1f; 解决方案&#xff1a;

根据一个类型 获取该类型的 特殊判断 优雅写法

需求&#xff1a;一个统计接口&#xff0c;时间类型参数有以下&#xff1a;今日、近七天、近三十日等 如果我要查询的话&#xff0c;SQL 里的条件必定是一个时间范围&#xff0c;所以就需要根据类型来算好这个时间范围&#xff0c;所以可以写成下面这样。 到时候直接就是 获取…

Android 如何在Android studio中快速创建raw和assets文件夹

一 方案 1. 创建raw文件夹 切成project浏览模式——>找到res文件粘贴要放入raw文件夹下的文件。 当然此时raw文件还没有&#xff0c;直接在右侧输入框中出现的路径~\res后面加上\raw即可。 2. 创建assets文件夹 同理在main文件夹下粘贴要放入assets文件夹的文件&#xff0…

免费外文文献检索网站,你一定要知道

01. Sci-Hub 网址链接&#xff1a;https://tool.yovisun.com/scihub/ Sci-hub是一个可以无限搜索、查阅和下载大量优质论文的数据库。其优点在于可以免费下载论文文献。 使用方法&#xff1a; 在Sci—hub搜索栏中粘贴所需文献的网址或者DOI&#xff0c;然后点击右侧的open即可…

强大日志查看器,助力数据联动分析

前言 我们曾讨论过观测云查看器强大的查询筛选和搜索功能&#xff0c;能够帮助用户快速、精准地检索数据&#xff0c;定位故障问题&#xff08;参见《如何使用查看器筛选、搜索功能进行数据定位&#xff1f;》&#xff09;。除此之外&#xff0c;日志查看器不仅可以帮助我们收…

VSCode中的任务什么情况下需要配置多个问题匹配器problemMatcher?多个问题匹配器之间的关系是什么?

☞ ░ 前往老猿Python博客 ░ https://blog.csdn.net/LaoYuanPython 一、简介 在 VS Code 中&#xff0c;tasks.json 文件中的 problemMatcher 字段用于定义如何解析任务输出中的问题&#xff08;错误、警告等&#xff09;。 problemMatcher是一个描述问题匹配器的接口&…

UOS安装Jenkins

一&#xff0c;环境准备 1.安装jdk 直接使用命令行&#xff08;sudo apt install -y openjdk-11-jdk&#xff09;安装jdk11 2.安装maven 参考此篇文章即可 UOS安装并配置Maven工具_uos 安装maven_蓝天下的一员的博客-CSDN博客 不过要注意这篇文章有个小错误&#xff0c;我…

vim三种模式,文本操作(操作字符/光标,列出行号可视化块模式/多文件查看)

目录 vim--文本编辑器 功能 基本概念 命令/默认模式 插入模式 底行模式 文本操作 引入 移动光标位置 删除字符 -- x/dd 复制/粘贴字符 -- yw/yyp 替换文本 -- r / %s 底行模式 全局替换 -- /g 撤销操作 -- u / ctrlr 修改字符 -- cw 示例 跳行 -- ctrlg 底行…

13 Linux 蜂鸣器

一、蜂鸣器驱动原理 常用蜂鸣器分两种&#xff0c;有源蜂鸣器和无源蜂鸣器。 它们俩的区别&#xff1a;有源蜂鸣器具有内置的振荡器和驱动电路&#xff0c;无源蜂鸣器没有&#xff1b;源蜂鸣器只需简单的数字信号来控制&#xff0c;无源蜂鸣器需要外部电路或微控制器来提供特定…

【已解决】设置SSH主机:VS Code-正在本地下载 VS Code 服务器

问题描述 很简单&#xff0c;就是我电脑强制重启之后用vscode再去连服务器&#xff0c;发现连不上了 解决办法 如上图&#xff0c;点击重试按钮&#xff0c;下面的这些东西就可以复制粘贴了 ctrf查找commit&#xff0c;这个时候就能找到一串d037ac076cee195194f93ce6fe2bdfe296…

AtCoder Beginner Contest 327 题解 A-D

目录 A - abB - A^AC - Number PlaceD - Good Tuple Problem A - ab 原题链接 题目描述 判断一个给定的字符串是否存在字符a和字符b相邻。 public static void solve() throws IOException{int a readInt();String s readString();boolean f s.contains("ab") ||…