目录
反射的基本作用
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保存。当取出一个元素时,再将它转换回Shape。Object和Shape之间的转换体现了最基本的反射,这种反射会检查所有的类型转换是否正确。
事实上这里的反射类型转换并不彻底,因为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对象不会自动初始化。一个类在被使用之前,会经历以下三个步骤:
- 加载:此时,类加载器根据字节码创建一个Class对象。
- 链接:该阶段会验证类中的字节码,并且为静态字段分配空间,并在必要时解析该类对其他类的所有引用。
- 初始化:若有基类,则先初始化基类,在执行静态初始化器和静态初始化块。
【例子:滞后的初始化】
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;
这是因为尽管Number是Integer的基类,但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库中也极少使用)。