Java字符串与输入输出详解

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

本文介绍了 Java 的反射机制,反射使我们摆脱了只能在编译时执行面向类型操作的限制,并且让我们能够编写一些非常强大的程序。

本文将讨论 Java 是如何在运行时发现对象和类的信息的,这通常有两种形式:简单反射,它假定你在编译时就已经知道了所有可用的类型;以及更复杂的反射,它允许我们在运行时发现和使用类的信息。

1. 为什么需要反射

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package reflection;

import java.util.stream.Stream;

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

@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);

/*
* 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 对象加载到内存中,它就会用于创建该类型的所有对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 类有很多方法,下面是其中的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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 对象的引用:类字面量。它看起来像这样:

1
FancyToy.class

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 引用的类型:

1
2
3
4
5
6
7
8
9
10
11
12
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 泛型的一部分,表示任何事物:

1
2
3
4
5
6
7
8
package reflection;

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

我们不能这么写:

1
Class<Number> c = Integer.class;

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

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

1
2
3
4
5
6
7
8
package reflection;

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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()

@Override
public String toString() {
return "People " + id;
}
}

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

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

@Override
public 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,但它也会受到一些限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 引用的类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 引用,就可能会遇到这种情况,不过这很罕见。