要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 Class对象 的特殊对象完成的,它包含了与类有关的信息。实际上,Class 对象就是用来创建该类所有"常规"对象的。Java 使用 Class 对象来实现 RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。不仅如此,Class 类还提供了很多使用 RTTI 的其它方式。
类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。
类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是 JVM 实现的一部分。原生类加载器加载的是”可信类”(包括 Java API 类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。
所有 Class 对象都属于 Class 类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。forName() 是 Class 类的一个静态方法,我们可以使用 forName() 根据目标类的类名(String)得到该类的 Class 对象。上面的代码忽略了 forName() 的返回值,因为那个调用是为了得到它产生的“副作用”。从结果可以看出,forName() 执行的副作用是如果 Gum 类没有被加载就加载它,而在加载的过程中,Gum 的 static 初始化块被执行了。
无论何时,只要你想在运行时使用类型信息,就必须先得到那个 Class 对象的引用。Class.forName() 就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用 getClass() 方法来获取 Class 引用了,这个方法来自根类 Object,它将返回表示该对象实际类型的 Class 对象的引用。Class 包含很多有用的方法,下面代码展示了其中的一部分:
// typeinfo/toys/ToyTest.java
// 测试 Class 类
// {java typeinfo.toys.ToyTest}
package typeinfo.toys;
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
// 注释下面的无参数构造器会引起 NoSuchMethodError 错误
Toy() {}
Toy(int i) {}
}
class FancyToy extends Toy
implements HasBatteries, Waterproof, Shoots {
FancyToy() { super(1); }
}
public class ToyTest {
static void printInfo(Class cc) {
System.out.println("Class name: " + cc.getName() +
" is interface? [" + cc.isInterface() + "]");
System.out.println(
"Simple name: " + cc.getSimpleName());
System.out.println(
"Canonical name : " + cc.getCanonicalName());
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("typeinfo.toys.FancyToy");
} catch(ClassNotFoundException e) {
System.out.println("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
for(Class face : c.getInterfaces())
printInfo(face);
Class up = c.getSuperclass();
Object obj = null;
try {
// Requires no-arg constructor:
obj = up.newInstance();
} catch(InstantiationException e) {
System.out.println("Cannot instantiate");
System.exit(1);
} catch(IllegalAccessException e) {
System.out.println("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}
输出结果:
Class name: typeinfo.toys.FancyToy is interface?
[false]
Simple name: FancyToy
Canonical name : typeinfo.toys.FancyToy
Class name: typeinfo.toys.HasBatteries is interface?
[true]
Simple name: HasBatteries
Canonical name : typeinfo.toys.HasBatteries
Class name: typeinfo.toys.Waterproof is interface?
[true]
Simple name: Waterproof
Canonical name : typeinfo.toys.Waterproof
Class name: typeinfo.toys.Shoots is interface? [true]
Simple name: Shoots
Canonical name : typeinfo.toys.Shoots
Class name: typeinfo.toys.Toy is interface? [false]
Simple name: Toy
Canonical name : typeinfo.toys.Toy
FancyToy 继承自 Toy 并实现了 HasBatteries、Waterproof 和 Shoots 接口。在 main 方法中,我们创建了一个 Class 引用,然后在 try 语句里边用 forName() 方法创建了一个 FancyToy 的类对象并赋值给该引用。需要注意的是,传递给 forName() 的字符串必须使用类的全限定名(包含包名)。
printInfo() 函数使用 getName() 来产生完整类名,使用 getSimpleName() 产生不带包名的类名,getCanonicalName() 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 getName() 相同)。isInterface() 用于判断某个 Class 对象代表的是否为一个接口。因此,通过 Class 对象,你可以得到关于该类型的所有信息。
在主方法中调用的 Class.getInterfaces() 方法返回的是存放 Class 对象的数组,里面的 Class 对象表示的是那个类实现的接口。
另外,你还可以调用 getSuperclass() 方法来得到父类的 Class 对象,再用父类的 Class 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。
Class 对象的 newInstance() 方法是实现“虚拟构造器”的一种途径,虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象。在前面的例子中,up 只是一个 Class 对象的引用,在编译期并不知道这个引用会指向哪个类的 Class 对象。当你创建新实例时,会得到一个 Object 引用,但是这个引用指向的是 Toy 对象。当然,由于得到的是 Object 引用,目前你只能给它发送 Object 对象能够接受的调用。而如果你想请求具体对象才有的调用,你就得先获取该对象更多的类型信息,并执行某种转型。另外,使用 newInstance() 来创建的类,必须带有无参数的构造器。在本章稍后部分,你将会看到如何通过 Java 的反射 API,用任意的构造器来动态地创建类的对象。
// typeinfo/ClassCasts.java
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; // ... 或者这样做.
}
}
cast() 方法接受参数对象,并将其类型转换为 Class 引用的类型。但是,如果观察上面的代码,你就会发现,与实现了相同功能的 main 方法中最后一行相比,这种转型好像做了很多额外的工作。
cast() 在无法使用普通类型转换的情况下会显得非常有用,在你编写泛型代码(你将在泛型这一章学习到)时,如果你保存了 Class 引用,并希望以后通过这个引用来执行转型,你就需要用到 cast()。但事实却是这种情况非常少见,我发现整个 Java 类库中,只有一处使用了 cast()(在 com.sun.mirror.util.DeclarationFilter 中)。
Java 类库中另一个没有任何用处的特性就是 Class.asSubclass(),该方法允许你将一个 Class 对象转型为更加具体的类型。
// typeinfo/pets/Person.java
package typeinfo.pets;
public class Person extends Individual {
public Person(String name) { super(name); }
}
// typeinfo/pets/Pet.java
package typeinfo.pets;
public class Pet extends Individual {
public Pet(String name) { super(name); }
public Pet() { super(); }
}
// typeinfo/pets/Dog.java
package typeinfo.pets;
public class Dog extends Pet {
public Dog(String name) { super(name); }
public Dog() { super(); }
}
// typeinfo/pets/Mutt.java
package typeinfo.pets;
public class Mutt extends Dog {
public Mutt(String name) { super(name); }
public Mutt() { super(); }
}
// typeinfo/pets/Pug.java
package typeinfo.pets;
public class Pug extends Dog {
public Pug(String name) { super(name); }
public Pug() { super(); }
}
// typeinfo/pets/Cat.java
package typeinfo.pets;
public class Cat extends Pet {
public Cat(String name) { super(name); }
public Cat() { super(); }
}
// typeinfo/pets/EgyptianMau.java
package typeinfo.pets;
public class EgyptianMau extends Cat {
public EgyptianMau(String name) { super(name); }
public EgyptianMau() { super(); }
}
// typeinfo/pets/Manx.java
package typeinfo.pets;
public class Manx extends Cat {
public Manx(String name) { super(name); }
public Manx() { super(); }
}
// typeinfo/pets/Cymric.java
package typeinfo.pets;
public class Cymric extends Manx {
public Cymric(String name) { super(name); }
public Cymric() { super(); }
}
// typeinfo/pets/Rodent.java
package typeinfo.pets;
public class Rodent extends Pet {
public Rodent(String name) { super(name); }
public Rodent() { super(); }
}
// typeinfo/pets/Rat.java
package typeinfo.pets;
public class Rat extends Rodent {
public Rat(String name) { super(name); }
public Rat() { super(); }
}
// typeinfo/pets/Mouse.java
package typeinfo.pets;
public class Mouse extends Rodent {
public Mouse(String name) { super(name); }
public Mouse() { super(); }
}
// typeinfo/pets/Hamster.java
package typeinfo.pets;
public class Hamster extends Rodent {
public Hamster(String name) { super(name); }
public Hamster() { super(); }
}
// typeinfo/pets/PetCreator.java
// Creates random sequences of Pets
package typeinfo.pets;
import java.util.*;
import java.util.function.*;
public abstract class PetCreator implements Supplier<Pet> {
private Random rand = new Random(47);
// The List of the different types of Pet to create:
public abstract List<Class<? extends Pet>> types();
public Pet get() { // Create one random Pet
int n = rand.nextInt(types().size());
try {
return types().get(n).newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
抽象的 types() 方法需要子类来实现,以此来获取 Class 对象构成的 List(这是模板方法设计模式的一种变体)。注意,其中类的类型被定义为“任何从 Pet 导出的类型”,因此 newInstance() 不需要转型就可以产生 Pet。get() 随机的选取出一个 Class 对象,然后可以通过 Class.newInstance() 来生成该类的新实例。
[class typeinfo.pets.Mutt, class typeinfo.pets.Pug,
class typeinfo.pets.EgyptianMau, class
typeinfo.pets.Manx, class typeinfo.pets.Cymric, class
typeinfo.pets.Rat, class typeinfo.pets.Mouse, class
typeinfo.pets.Hamster]
// typeinfo/pets/Pets.java
// Facade to produce a default PetCreator
package typeinfo.pets;
import java.util.*;
import java.util.stream.*;
public class Pets {
public static final PetCreator CREATOR = new LiteralPetCreator();
public static Pet get() {
return CREATOR.get();
}
public static Pet[] array(int size) {
Pet[] result = new Pet[size];
for (int i = 0; i < size; i++)
result[i] = CREATOR.get();
return result;
}
public static List<Pet> list(int size) {
List<Pet> result = new ArrayList<>();
Collections.addAll(result, array(size));
return result;
}
public static Stream<Pet> stream() {
return Stream.generate(CREATOR);
}
}
我们在这里所做的另一个更改是使用工厂方法设计模式将对象的创建推迟到类本身。工厂方法可以以多态方式调用,并为你创建适当类型的对象。事实证明,java.util.function.Supplier 用 T get() 描述了原型工厂方法。协变返回类型允许 get() 为 Supplier 的每个子类实现返回不同的类型。
在本例中,基类 Part 包含一个工厂对象的静态列表,列表成员类型为 Supplier<Part>。对于应该由 get() 方法生成的类型的工厂,通过将它们添加到 prototypes 列表向基类“注册”。奇怪的是,这些工厂本身就是对象的实例。此列表中的每个对象都是用于创建其他对象的原型:
// typeinfo/RegisteredFactories.java
// 注册工厂到基础类
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
class Part implements Supplier<Part> {
@Override
public String toString() {
return getClass().getSimpleName();
}
static List<Supplier<? extends Part>> prototypes =
Arrays.asList(
new FuelFilter(),
new AirFilter(),
new CabinAirFilter(),
new OilFilter(),
new FanBelt(),
new PowerSteeringBelt(),
new GeneratorBelt()
);
private static Random rand = new Random(47);
public Part get() {
int n = rand.nextInt(prototypes.size());
return prototypes.get(n).get();
}
}
class Filter extends Part {}
class FuelFilter extends Filter {
@Override
public FuelFilter get() {
return new FuelFilter();
}
}
class AirFilter extends Filter {
@Override
public AirFilter get() {
return new AirFilter();
}
}
class CabinAirFilter extends Filter {
@Override
public CabinAirFilter get() {
return new CabinAirFilter();
}
}
class OilFilter extends Filter {
@Override
public OilFilter get() {
return new OilFilter();
}
}
class Belt extends Part {}
class FanBelt extends Belt {
@Override
public FanBelt get() {
return new FanBelt();
}
}
class GeneratorBelt extends Belt {
@Override
public GeneratorBelt get() {
return new GeneratorBelt();
}
}
class PowerSteeringBelt extends Belt {
@Override
public PowerSteeringBelt get() {
return new PowerSteeringBelt();
}
}
public class RegisteredFactories {
public static void main(String[] args) {
Stream.generate(new Part())
.limit(10)
.forEach(System.out::println);
}
}
并非层次结构中的所有类都应实例化;这里的 Filter 和 Belt 只是分类器,这样你就不会创建任何一个类的实例,而是只创建它们的子类(请注意,如果尝试这样做,你将获得 Part 基类的行为)。
因为 Part implements Supplier<Part>,Part 通过其 get() 方法供应其他 Part。如果为基类 Part 调用 get()(或者如果 generate() 调用 get()),它将创建随机特定的 Part 子类型,每个子类型最终都从 Part 继承,并重写相应的 get() 以生成它们中的一个。
// typeinfo/FamilyVsExactType.java
// instanceof 与 class 的差别
// {java typeinfo.FamilyVsExactType}
package typeinfo;
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
System.out.println(
"Testing x of type " + x.getClass());
System.out.println(
"x instanceof Base " + (x instanceof Base));
System.out.println(
"x instanceof Derived " + (x instanceof Derived));
System.out.println(
"Base.isInstance(x) " + Base.class.isInstance(x));
System.out.println(
"Derived.isInstance(x) " +
Derived.class.isInstance(x));
System.out.println(
"x.getClass() == Base.class " +
(x.getClass() == Base.class));
System.out.println(
"x.getClass() == Derived.class " +
(x.getClass() == Derived.class));
System.out.println(
"x.getClass().equals(Base.class)) "+
(x.getClass().equals(Base.class)));
System.out.println(
"x.getClass().equals(Derived.class)) " +
(x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
输出结果:
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
在传统编程环境中,这是一个牵强的场景。但是,当我们进入一个更大的编程世界时,会有一些重要的情况发生。第一个是基于组件的编程,你可以在应用程序构建器集成开发环境中使用快速应用程序开发(RAD)构建项目。这是一种通过将表示组件的图标移动到窗体上来创建程序的可视化方法。然后,通过在编程时设置这些组件的一些值来配置这些组件。这种设计时配置要求任何组件都是可实例化的,它公开自己的部分,并且允许读取和修改其属性。此外,处理图形用户界面(GUI)事件的组件必须公开有关适当方法的信息,以便 IDE 可以帮助程序员覆写这些事件处理方法。反射提供了检测可用方法并生成方法名称的机制。
// typeinfo/ShowMethods.java
// 使用反射展示一个类的所有方法,甚至包括定义在基类中方法
// {java ShowMethods ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
public class ShowMethods {
private static String usage =
"usage:\n" +
"ShowMethods qualified.class.name\n" +
"To show all methods in class or:\n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(usage);
System.exit(0);
}
int lines = 0;
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if (args.length == 1) {
for (Method method : methods)
System.out.println(
p.matcher(
method.toString()).replaceAll(""));
for (Constructor ctor : ctors)
System.out.println(
p.matcher(ctor.toString()).replaceAll(""));
lines = methods.length + ctors.length;
} else {
for (Method method : methods)
if (method.toString().contains(args[1])) {
System.out.println(p.matcher(
method.toString()).replaceAll(""));
lines++;
}
for (Constructor ctor : ctors)
if (ctor.toString().contains(args[1])) {
System.out.println(p.matcher(
ctor.toString()).replaceAll(""));
lines++;
}
}
} catch (ClassNotFoundException e) {
System.out.println("No such class: " + e);
}
}
}
输出结果:
public static void main(String[])
public final void wait() throws InterruptedException
public final void wait(long,int) throws
InterruptedException
public final native void wait(long) throws
InterruptedException
public boolean equals(Object)
public String toString()
public native int hashCode()
public final native Class getClass()
public final native void notify()
public final native void notifyAll()
public ShowMethods()
实际上,在所有地方都使用 Optional 是没有意义的,有时候检查一下是不是 null 也挺好的,或者有时我们可以合理地假设不会出现 null,甚至有时候检查 NullPointException 异常也是可以接受的。Optional 最有用武之地的是在那些“更接近数据”的地方,在问题空间中代表实体的对象上。举个简单的例子,很多系统中都有 Person 类型,代码中有些情况下你可能没有一个实际的 Person 对象(或者可能有,但是你还没用关于那个人的所有信息)。这时,在传统方法下,你会用到一个 null 引用,并且在使用的时候测试它是不是 null。而现在,我们可以使用 Optional:
// typeinfo/Person.java
// Using Optional with regular classes
import onjava.*;
import java.util.*;
class Person {
public final Optional<String> first;
public final Optional<String> last;
public final Optional<String> address;
// etc.
public final Boolean empty;
Person(String first, String last, String address) {
this.first = Optional.ofNullable(first);
this.last = Optional.ofNullable(last);
this.address = Optional.ofNullable(address);
empty = !this.first.isPresent()
&& !this.last.isPresent()
&& !this.address.isPresent();
}
Person(String first, String last) {
this(first, last, null);
}
Person(String last) {
this(null, last, null);
}
Person() {
this(null, null, null);
}
@Override
public String toString() {
if (empty)
return "<Empty>";
return (first.orElse("") +
" " + last.orElse("") +
" " + address.orElse("")).trim();
}
public static void main(String[] args) {
System.out.println(new Person());
System.out.println(new Person("Smith"));
System.out.println(new Person("Bob", "Smith"));
System.out.println(new Person("Bob", "Smith",
"11 Degree Lane, Frostbite Falls, MN"));
}
}
输出结果:
<Empty>
Smith
Bob Smith
Bob Smith 11 Degree Lane, Frostbite Falls, MN
Person 的设计有时候又叫“数据传输对象(DTO,data-transfer object)”。注意,所有字段都是 public 和 final 的,所以没有 getter 和 setter 方法。也就是说,Person 是不可变的,你只能通过构造器给它赋值,之后就只能读而不能修改它的值(字符串本身就是不可变的,因此你无法修改字符串的内容,也无法给它的字段重新赋值)。如果你想修改一个 Person,你只能用一个新的 Person 对象来替换它。empty 字段在对象创建的时候被赋值,用于快速判断这个 Person 对象是不是空对象。
Person 字段的限制又不太一样:如果你把它的值设为 null,程序会自动把将它赋值成一个空的 Person 对象。先前我们也用过类似的方法把字段转换成 Optional,但这里我们是在返回结果的时候使用 orElse(new Person()) 插入一个空的 Person 对象替代了 null。
在 Position 里边,我们没有创建一个表示“空”的标志位或者方法,因为 person 字段的 Person 对象为空,就表示这个 Position 是个空缺位置。之后,你可能会发现你必须添加一个显式的表示“空位”的方法,但是正如 YAGNI (You Aren't Going to Need It,你永远不需要它)所言,在初稿时“实现尽最大可能的简单”,直到程序在某些方面要求你为其添加一些额外的特性,而不是假设这是必要的。
// typeinfo/Operation.java
import java.util.function.*;
public class Operation {
public final Supplier<String> description;
public final Runnable command;
public Operation(Supplier<String> descr, Runnable cmd) {
description = descr;
command = cmd;
}
}
现在我们可以创建一个扫雪 Robot:
// typeinfo/SnowRemovalRobot.java
import java.util.*;
public class SnowRemovalRobot implements Robot {
private String name;
public SnowRemovalRobot(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public String model() {
return "SnowBot Series 11";
}
private List<Operation> ops = Arrays.asList(
new Operation(
() -> name + " can shovel snow",
() -> System.out.println(
name + " shoveling snow")),
new Operation(
() -> name + " can chip ice",
() -> System.out.println(name + " chipping ice")),
new Operation(
() -> name + " can clear the roof",
() -> System.out.println(
name + " clearing roof")));
public List<Operation> operations() {
return ops;
}
public static void main(String[] args) {
Robot.test(new SnowRemovalRobot("Slusher"));
}
}
输出结果:
Robot name: Slusher
Robot model: SnowBot Series 11
Slusher can shovel snow
Slusher shoveling snow
Slusher can chip ice
Slusher chipping ice
Slusher can clear the roof
Slusher clearing roof
// typeinfo/interfacea/A.java
package typeinfo.interfacea;
public interface A {
void f();
}
然后实现这个接口,你可以看到其代码是怎么从实际类型开始顺藤摸瓜的:
// typeinfo/InterfaceViolation.java
// Sneaking around an interface
import typeinfo.interfacea.*;
class B implements A {
public void f() {
}
public void g() {
}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // Compile error
System.out.println(a.getClass().getName());
if (a instanceof B) {
B b = (B) a;
b.g();
}
}
}
输出结果:
B
通过使用 RTTI,我们发现 a 是用 B 实现的。通过将其转型为 B,我们可以调用不在 A 中的方法。
这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 interface 关键字正在保护你,但其实并没有。另外,在本例中使用 B 来实现 A 这种情况是有公开案例可查的。
// typeinfo/ModifyingPrivateFields.java
import java.lang.reflect.*;
class WithPrivateFinalField {
private int i = 1;
private final String s = "I'm totally safe";
private String s2 = "Am I safe?";
@Override
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
}
}
public class ModifyingPrivateFields {
public static void main(String[] args) throws Exception {
WithPrivateFinalField pf =
new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("i");
f.setAccessible(true);
System.out.println(
"f.getInt(pf): " + f.getInt(pf));
f.setInt(pf, 47);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
f = pf.getClass().getDeclaredField("s2");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
}
}
输出结果:
i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
但实际上 final 字段在被修改时是安全的。运行时系统会在不抛出异常的情况下接受任何修改的尝试,但是实际上不会发生任何修改。