# 第 5 章 继承
概要:
1)类、超类和子类
2)参数数量可变的方法
3)Object: 所有类的超类
4)枚举类
5)泛型数组列表
6)反射(反射是指在程序运行期间发现更多的类及其属性的能力。)
7)对象包装器与自动装箱
8)继承的设计技巧
利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。
# 5.1 类、超类和子类
# 5.1.1 定义子类
1.Manager 与 Employee 之间存在着明显的 “is-a”(是)关系,每个经理都是一名雇员:“is-a” 关系是继承的一个明显特征。
2. 关键字:extends
3.Java 中所有继承都是公有继承(c++ 中有私有继承和保护继承)
- 公有继承 (public):公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
- 私有继承 (private):私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
- 保护继承 (protected):保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
4. 子类比超类拥有的功能更加丰富。
# 5.1.2 覆盖方法
1. 子类有时需要重写超类方法并添加自己的特性。
2. 子类可以使用 super 调用超类的方法。
3. 子类可以增加域、方法,但绝对不能删除继承的任何域和方法。
# 5.1.3 子类构造器
1. 子类构造器可以使用 super 调用超类构造器,注意 super 语句必须是子类构造器第一条语句(写在最上面)。
2. 若子类构造器没有显式地调用超类构造器,则自动调用超类默认的构造器(即无参构造器),若超类没有无参构造器而子类构造器又没有显式调用超类的其他构造器,则编译错误。
3.this 和 super
- this 的两种用途:(1)引用隐式参数; (2)调用当前类的其他构造器
- super 的两种用途:(1)调用超类的方法; (2)调用超类的构造器(作为第一行)
4. 什么是多态?一个对象变量可以表示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
如定义一个超类 Employee 和一个子类 Manager 并且都有 getSalary () 方法,初始化一个 Employee 数组 new Employee [3], 数组第一个元素可以实例化为 Manager,第二第三个元素可以实例化为 Employee,这就是一种多态,遍历这个数组并实际调用 getSalary () 方法的过程存在动态绑定现象(员工调用员工的 getSalary () 经理调用经理的 getSalary ())。
# 5.1.4 继承层次
1. 由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。
2. 在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。一个祖先类可以有多个子孙继承链。
3.Java 不支持多继承
# 5.1.5 多态
1. 在 Java 程序设计语言中,对象变量是多态的。一个 Employee 变量既可以引用一个 Employee 类对象,也可以引用一个 Employee 类的任何一个子类的对象(例如,Manager、Executive、Secretary 等)。 - is-a 与置换法则
2. 不能将一个超类的引用赋给子类变量(因为不是所有 Employee 都是 Manager)。反之可以将一个子类数组的引用转换为超类数组的引用而不需要强制类型转换(如将 Manager [] 转为 Employee [] 是合法的)。
public static strictfp void main(String[] args) { | |
Manager[] managers = new Manager[10]; | |
Employee[] employees = managers; | |
// 上述实际上 managers 和 employees 引用同一个数组,而下面赋值竟然是 Employee 信息 | |
// 这里我们似乎把一个普通员工擅自归入了经理行列中,这搅乱了相邻存储空间 | |
// 下面这行编译正常,但运行时报错:java.lang.ArrayStoreException | |
employees[0] = new Employee("Jalen", 12.0, 1992, 12, 22); | |
managers[0].setBonus(1000); // 只有经理拥有奖金方法 | |
// 为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。 | |
// 例如,使用 new managers [10] 创建的数组是一个经理数组。 | |
} |
# 5.1.6 理解方法调用
1. 编译器查看对象的声明类型和方法名。假设调用 x.f(param),且隐式参数 x 声明为 C 类的对象。需要注意的是:有可能存在多个名字为 f,但参数类型不一样的方法。例如,可能存在方法 f(int)和方法 f(String)。编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。 至此,编译器已获得所有可能被调用的候选方法。
2. 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,对于调用 x.f(“Hello”)来说,编译器将会挑选 f(String),而不是 f(int)。由于允许类型转换(int 可以转换成 double,Manager 可以转换成 Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。 至此,编译器已获得需要调用的方法名字和参数类型。
3. 如果是 private 方法、static 方法、final 方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为 静态绑定
(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现 动态绑定
。
4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String),就直接调用它;否则,将在 D 类的超类中寻找 f(String),以此类推。
5. 每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。
6. 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是 public,子类方法一定要声明为 public。
# 5.1.7 阻止继承:final 类和方法
1. 不允许扩展的类被称为 final 类。即定义类时使用了 final 修饰符。
2. 类中的方法如果被声明为 final,则子类不能覆盖这个方法。(注:final 类中所有方法自动地称为 final 方法)
3. 域也可以被声明为 final。对于 final 域来说,构造对象之后就不允许改变它们的值了。不过,如果将一个类声明为 final,只有其中的方法自动地成为 final,而不包括域。
4. 内联(inlining)优化与弊端。
# 5.1.8 强制类型转换
1. 将某个类的对象引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。(注意类型检查)
- 只能在继承层次内进行类型转换
- 在将超类转换成子类之前,应该使用 instanceof 进行检查
2. 通过类型转换调整对象的类型不是一种好的做法,通常我们可以检查一下超类的设计是否合理并重新晚上一下超类,应尽量少用类型转换和 instanceof 运算符。
3. 类型转换失败会抛出 ClassCastException。
# 5.1.9 抽象类
1. 祖先类更加通用,人们只将他作为派生其他类的基类,而不作为想使用的特定的实例类。
2. 包含一个或多个抽象方法的类 本身必须被声明为抽象的,带有 abstract 修饰符。
3. 抽象类还可以包含具体数据和具体方法。
4. 扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
5. 类即使不含抽象方法,也可以将类声明为抽象类。
6. 抽象类不能被实例化。
7. 可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。
8. 在接口 interface 中将会看到更多的抽象方法。
# 5.1.10 受保护访问
1. 有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected。
2. 谨慎使用 protected 属性。
3.Object 类的 clone 方法。
4.Java 中的受保护部分对所有子类及同一个包中的所有其他类都可见。
# 5.2 Object:所有类的超类
1. 在 Java 中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。
2. 所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类。
# 5.2.1 equals 方法
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。从这点上看,将其作为默认操作也是合乎情理的。然而,对于多数类来说,这种判断并没有什么意义。例如,采用这种方式比较两个 PrintStream 对象是否相等就完全没有意义。然而,经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。
# 5.2.2 相等测试与继承
1.Java 语言规范要求 equals 方法具有下面的特性:
- 1)自反性:对于任何非空引用 x,x.equals(x)应该返回 true。
- 2)对称性:对于任何引用 x 和 y,当且仅当 y.equals(x)返回 true,x.equals(y)也应该返回 true。
- 3)传递性:对于任何引用 x、y 和 z,如果 x.equals(y)返回 true,y.equals(z)返回 true,x.equals(z)也应该返回 true。
- 4)一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y)应该返回同样的结果。
- 5)对于任意非空引用 x,x.equals(null)应该返回 false。
2. 无论集合采用何种方式实现,都需要拥有对任意两个集合进行比较的功能。
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用 getClass 进行检测。
- 如果由超类决定相等的概念,那么就可以使用 instanceof 进行检测,这样可以在不同子类的对象之间进行相等的比较。
3. 在标准 Java 库中包含 150 多个 equals 方法的实现,包括使用 instanceof 检测、调用 getClass 检测、捕获 ClassCastException 或者什么也不做。可以查看 java.sql.Timestamp 类的 API 文档,在这里实现人员不无尴尬地指出,他们使自己陷入了困境。Timestamp 类继承自 java.util.Date,而后者的 equals 方法使用了一个 instanceof 测试,这样一来就无法覆盖实现 equals 使之同时做到对称且正确。
4. 如果在子类中重新定义 equals,就要在其中包含调用 super.equals(other)。
5. 对于数组类型的域,可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。
# 5.2.3 hashCode 方法
1. 散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode()与 y.hashCode()基本上不会相同。
2. 由于 hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
3. 如果重新定义 equals 方法,就必须重新定义 hashCode 方法,以便用户可以将对象插入到散列表中。
4.hashCode 方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。
5.Equals 与 hashCode 的定义必须一致:如果 x.equals(y)返回 true,那么 x.hashCode()就必须与 y.hashCode()具有相同的值。
# 5.2.4 toString 方法
1. 在 Object 中还有一个重要的方法,就是 toString 方法,它用于返回表示对象值的字符串。
2. 实际上,最好通过调用 getClass().getName()获得类名的字符串,而不要将类名硬加到 toString 方法中。
3.toString 方法也可以供子类调用。 当然,设计子类的程序员也应该定义自己的 toString 方法,并将子类域的描述添加进去。如果超类使用了 getClass().getName(),那么子类只要调用 super.toString()就可以了。
4. 令人烦恼的是,数组继承了 object 类的 toString 方法,数组类型将按照旧的格式打印。例如生成字符串 “[I@1a46e30”(前缀 [I 表明是一个整型数组)。修正的方式是调用静态方法 Arrays.toString。
5.toString 方法是一种非常有用的调试工具。在标准类库中,许多类都定义了 toString 方法,以便用户能够获得一些有关对象状态的必要信息。
6. 强烈建议为自定义的每一个类增加 toString 方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。
# 泛型数组列表
1.Java 中允许在运行时确定数组的大小。即 ArrayList 类。它在添加和删除元素时,具有自动调节数组容量的功能。ArrayList 是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如, ArrayList<Employee>
。
ArrayList<Employee> staff = new ArrayList<>;
// Java7 开始可以省去右边的类型参数,被称为菱形语法。
2. 如果赋值给一个变量,或传递到某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在 <> 中。
3. 在 Java 的老版本中,程序员使用 Vector 类实现动态数组。不过,ArrayList 类更加有效,没有任何理由一定要使用 Vector 类。
4. 数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力:如果调用 add 且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
5. 数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配 100 个元素的存储空间,数组就有 100 个空位置可以使用。而容量为 100 个元素的数组列表只是拥有保存 100 个元素的潜力(实际上,重新分配空间的话,将会超过 100),但是在最初,甚至完成初始化构造之后,数组列表根本就不含有任何元素。
6. 一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize 方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用 trimToSize。
7.Java 没有运算符重载(运算符重载(英語:operator overloading)是多态的一种。这里,运算符(比如 +,= 或 ==)被当作多态函数,它们的行为随着其参数类型的不同而不同。运算符并不一定总是符号。)
# 5.3.1 访问数组列表的元素
1. 很遗憾,天下没有免费的午餐。数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。其原因是 ArrayList 类并不是 Java 程序设计语言的一部分;它只是一个由某些人编写且被放在标准库中的一个实用类。 使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [] 语法格式。
2. 使用 add 方法为数组添加新元素,而不要使用 set 方法,它只能替换数组中已经存在的元素内容。
3. 对数组实施插入和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担心。但如果数组存储的元素数比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表了。有关链表操作的实现方式将在第 9 章中讲述。
# 5.3.2 类型化与原始数组列表的兼容性
与遗留代码交互
# 5.4 对象包装器与自动装箱
1. 所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int。通常,这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void 和 Boolean(前 6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是 final,因此不能定义它们的子类。
2. 由于每个值分别包装在对象中,所以 ArrayList<Integer>
的效率远远低于 int [] 数组。因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。
3. 自动装箱(大家可能认为自动打包(autowrapping)更加合适,而 “装箱(boxing)” 这个词源自于 C#。):
ArrayList<Integer> list = new ArrayList<>(); | |
list.add(3); // 这里自动转为 list.add (Integer.valueOf (3)); |
4. 自动拆箱
int n = list.get(i); // 翻译成 int n = list.get (i).intValue (); |
5. 自动装箱规范要求 boolean、byte、char≤127,介于 - 128~127 之间的 short 和 int 被包装到固定的对象中。(cache)
6. 如果在一个条件表达式中混合使用 Integer 和 Double 类型,Integer 值就会拆箱,提升为 double,再装箱为 Double。
7. 最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。 使用数值对象包装器还有另外一个好处。Java 设计者发现,可以将某些基本方法放置在包装器中,例如,将一个数字字符串转换成数值。
# 5.5 参数数量可变的方法
1. 允许将一个数组传递给可变参数方法的最后一个参数。例如:
2.printf 的参数就是可变的。
3. 可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。例如,将 main 函数的参数 Stringp [] args 改为 String... args 完全没有问题。
# 5.6 枚举类
如同 Class 类一样,鉴于简化的考虑,Enum 类省略了一个类型参数。例如,实际上,应该将枚举类型 Size 扩展为 Enum<Size>
。类型参数在 compareTo 方法中使用。
# 5.7 反射
1. 反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中,它是 Java 组件的体系结构。使用反射,Java 可以支持 Visual Basic 用户习惯使用的工具。特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。
2. 能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:
- 在运行时分析类的能力。
- 在运行时查看对象,例如,编写一个 toString 方法供所有类使用。
- 实现通用的数组操作代码。
- 利用 Method 对象,这个对象很像 C++ 中的函数指针。
3. 反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。如果仅对设计应用程序感兴趣,而对构造工具不感兴趣,可以跳过该方面知识。
# 5.7.1 Class 类
1. 在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。 可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class。
- e.getClass().getName() // get class name
- Class.forName ("java.util.Random"); // 获取类名对应的 class 对象
- e.getClass ().newInstance (); // 动态地创建一个类的实例
- Class.forName ("java.util.Random").newInstance (); // 同上
newInstance 方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。
2. 在启动时,包含 main 方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类,以此类推。
3.Class 类实际上是一个泛型类。
4. 鉴于历史原因,getName 方法在应用于数组类型的时候会返回一个很奇怪的名字:
- Double [].class.getName()返回 “[Ljava.lang.Double;”。
- int [].class.getName()返回 “[I”。
# 5.7.2 捕获异常
1. 当程序运行过程中发生错误时,就会 “抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个 “捕获” 异常的处理器(handler)对异常情况进行处理。
2. 异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问 null 引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。
3.try {...} catch (Exception e){ e.printStackTrace ();} 捕获异常以及打印栈的轨迹。
# 5.7.3 利用反射分析类的能力
1. 反射机制最重要的内容 —— 检查类的结构。
2. 在 java.lang.reflect 包中有三个类 Field、Method 和 Constructor 分别用于描述类的域、方法和构造器。这三个类都有一个叫做 getName 的方法,用来返回项目的名称。Field 类有一个 getType 方法,用来返回描述域所属类型的 Class 对象。Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。这三个类还有一个叫做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外,还可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析 getModifiers 返回的整型数值。例如,可以使用 Modifier 类中的 isPublic、isPrivate 或 isFinal 判断方法或构造器是否是 public、private 或 final。我们需要做的全部工作就是调用 Modifier 类的相应方法,并对返回的整型数值进行分析,另外,还可以利用 Modifier.toString 方法将修饰符打印出来。
3.Class 类中的 getFields、getMethods 和 getConstructors 方法将分别返回类提供的 public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
# 5.7.4 在运行时使用反射分析对象
1. 反射机制的默认行为受限于 Java 的访问控制。然而,如果一个 Java 程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用 Field、Method 或 Constructor 对象的 setAccessible 方法。setAccessible 方法是 AccessibleObject 类中的一个方法,它是 Field、Method 和 Constructor 类的公共超类。这个特性是为调试、持久存储和相似机制提供的。
# 5.7.5 使用反射编写泛型数组代码
1.java.lang.reflect 包中的 Array 类允许动态地创建数组。例如,将这个特性应用到 Array 类中的 copyOf 方法实现中,这个方法可以用于扩展已经填满的数组。
2. 一 Java 数组会记住每个元素的类型,即创建数组时 new 表达式中使用的元素类型。
# 5.7.6 调用任意方法
1. 反射机制允许你调用任意方法。
2. 建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8 中的 lambda 表达式。特别要重申:建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快,更易于维护。
# 5.8 继承的设计技巧
- 1. 将公共操作和域放在超类
- 2. 不要使用受保护的域
- 3. 使用继承实现 “is-a” 关系
- 4. 除非所有继承的方法都有意义,否则不要使用继承
- 5. 在覆盖方法时,不要改变预期的行为
- 6. 使用多态,而非类型信息
- 7. 不要过多地使用反射
1. 有些程序员认为,将大多数的实例域定义为 protected 是一个不错的主意,只有这样,子类才能够在需要的时候直接访问它们。然而,protected 机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域,从而破坏了封装性。第二,在 Java 程序设计语言中,在同一个包中的所有类都可以访问 proteced 域,而不管它是否为这个类的子类。 不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
2. 反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。