3.1 Java 中的面向对象编程功能
前两章说过,在Java中,除基本类型外的其他一切都是对象。Java支持基本类型,因此不是纯粹的OOP语言,但不失为一种严肃的OOP语言。
要有效地使用Java,你必须熟悉OOP。如果你将OOP忘得差不多了,也不用担心,因为本章将力图帮助你复习这方面的知识,虽然不会全面介绍OOP。本章重点介绍各种与OOP相关的主题:
- 定义类;
- 定义包;
- 添加类成员——变量和方法;
- 构造函数和析构函数;
- 继承;
- 接口;
- 抽象类;
- 向上转换和向下转换。
3.1.1 定义类
从前两章的示例可知,要定义类,只需使用Java关键字class,并在它后面加上类名和大括号({ })。其中大括号让程序员知道类包含哪些代码:
class ClassName {}
上述代码遵守了所有的Java语法规则,因此能够通过编译,但删除其中的任何一部分都将导致编译错误。
给JVM类命名时,应采用骆驼拼写法:类名的第一个字母大写;如果类名由多个单词组成,单词之间不用空格或下划线分隔,且每个单词的首字母都大写。给类命名时需遵守如下规则。
- 类名不能以数字打头。
- 类名不能包含连字符或空格;类名可包含下划线,但根据约定不使用它。
- 类名可包含数字,但不能以数字打头。
- 类名不能是Java保留的关键字。要将关键字用作类名,必须至少修改或添加一个字符,这样才能避免类名违反这种规则。
3.1.2 类访问限定符
类的可见性是可调整的。在没有明确指定的情况下(就像前面的示例那样),可见性是包私有的,即只能在所属的包中引用和实例化它。有关JVM中包的工作原理的详细说明,请参阅前一章。
在大多数情况下,你都希望创建可在任何地方引用和实例化的类。为此可在关键字class前面添加访问限定符public,这样在任何包中都能够看到并实例化当前类:
public class ClassName {}
即便是公有类,也可包含私有的构造函数。这样的类可在其他包中看到并引用,但不能访问其构造函数的代码无法实例化它。
Java编程语言有一个不同寻常的要求,那就是在一个源代码文件中,只能定义一个公有类,且文件名必须与类名完全相同。其他JVM语言通常没有这样的限制。
3.1.3 类限定符final——锁定类
可在类名前添加非访问限定符final,这将禁止其他类继承当前类:
public final class ThisClassCanNotBeOverriden {}
关键字final可放在访问限定符的前面,也可放在它后面,但根据约定,先指定访问限定符,再指定非访问限定符。如果你试图继承final类,编译器将拒绝对代码进行编译。
3.1.4 定义包
要将类放在包中,可使用关键字package。指定类所属包的代码必须是第一行非注释代码;与其他Java语句一样,这种语句也以分号(;)结尾,如下所示:
package com.example.package_name;
前一章详细讨论过包,包括命名约定和需求。未指定所属的包时,类将被加入到默认包中。
根据约定,存储Java源代码文件的目录的结构必须与包名匹配。所有流行的IDE都能看懂包结构,它们不显示各个子目录,而是显示完整的包名。例如,下面是Eclipse IDE的项目资源管理器(project explorer)的屏幕截图:

3.1.5 导入类
要在代码中引用类,可使用全限定类名。例如,要在方法中使用ArrayList,可像下面这样编写代码:
java.util.ArrayList list = new java.util.ArrayList();
这要求大量的键击,即便对以繁琐著称的语言来说亦如此。当然,Java提供了解决方案:通过使用关键字import,引用类时只需使用其名称。import语句的最基本形式类似于下面这样:
import java.util.ArrayList;class Demo {ArrayList list = new ArrayList();}
在这个方法的代码中,现在只需使用类名ArrayList,而无需再使用全限定类名。import语句必须位于指定类所属包的语句后面,同时位于第一行类定义代码前面。在一条import语句中,不能指定多个包。
类名发生冲突(即在多个包中使用相同的类名)时,可只导入其中的一个类。但在这种情况下,要在代码中引用其他的同名类,必须使用全限定类名。
还可在一条import语句中导入指定包中的所有类:
import java.util.*;
然而,为较大的系统编写代码时,不推荐使用这种形式的import语句,因为这会增加出现类名冲突的风险。请注意,这只会导入指定包中的所有类,而子包不受影响,即必须单独导入它们。例如,java.util.concurrent包包含用于并发编程的实用类。要导入java.util和java.util.concurrent包中的类,必须分别使用不同的import语句。
默认导入了
java.lang包中的所有类,你可直接使用它们。
3.1.6 添加类成员——变量和方法
如果没有变量和方法,类将既乏味又无用。变量用于存储通常由方法进行处理的数据。与类一样,变量和方法的可见性也是可以修改的,为此可在它们前面加上访问限定符,但它们还支持其他的限定符。我们将先讨论定义变量和方法的语法,再讨论限定符。因此本节包含如下主题:
- 实例变量;
- 实例方法;
- 访问限定符;
- 限定符
static; - 限定符
final; 方法重载。
实例变量
通常,每个对象实例都有自己的变量,这些变量被称为实例变量(instance variable)。在Java中,实例变量是像下面这样定义的:
TYPE variableName;
其中TYPE可以是任何基本类型(int、double等)和引用类型。如果你要使用的类已经使用import语句导入了,则只需指定类名,否则必须指定全限定类名。可在声明变量的同时对其进行初始化:
public class Test {int i = 25;Object o = new Object();}
对于未在类级显式地初始化的变量,其值将被隐式地初始化为0(如果其类型为int、long或short)、0.0(如果其类型为float或double)、false(如果其类型为boolean)或null(如果其类型为引用)。对变量名的要求与类名相同,但命名约定不同。根据约定,变量名以小写字母开头。与类名一样,变量名也可包含$和下划线等特殊字符,但不推荐这样做。
- 方法
在Java中,函数可返回对象或null。对于什么都不返回的方法,必须使用关键字void,这借鉴了C语言。常规类中的方法必须包含放在大括号({ })内的方法体:
public class ClassWithTwoMethods {boolean b;void methodReturnsNothingAndNoParameters() {}Object methodReturnsAnObject(boolean b, int i) {this.b = b;return null;}}
有返回类型的方法必须在方法体中返回相应的对象或null,否则将无法通过编译,但使用关键字void的方法什么都不返回。
在方法中声明的变量必须初始化后才能使用。不同于类变量和实例变量,对于在方法中声明的变量,不会自动对其进行初始化。
注意:从前面的示例可知,在方法中可使用关键字
this来访问类成员。
3.1.7 限定符
在变量和方法前面都可指定限定符。限定符分两类:
- 访问限定符;
- 非访问限定符。
很多访问限定符和非访问限定符都可结合起来使用。你将看到,指定多个限定符时,它们的顺序不重要。根据约定,应先指定访问限定符,再指定非访问限定符,但这并非是不可违背的规则。
- 使用访问限定符保护类成员
要指定类的哪些成员(变量和方法)可被其他类访问,可在它们前面加上访问限定符。相比于类本身,可用于类成员的访问限定符更多。下表列出了可用于类成员的所有访问访问限定符。
名称访问限定符描述 公有public公有成员可被能够访问其所属类的所有代码访问 受保护protected受保护的成员在当前类、当前包的其他类以及继承当前类的类中可见且可访问,但对其他所有的类来说都不可见 包私有没有指定访问限定符时,类成员仅在当前类和当前包的其他类中可见且可访问,但对其他包中的类来说是不可见的,对继承了当前类的类来说亦如此 私有private私有成员仅在其所属的类中可见且可访问,在其他类中都不可见也无法访问
继承其他类的类可重写被继承类中它能够访问的所有方法,但使用限定符final定义的方法除外(这一点你马上就将看到)。在任何情况下都不能重写私有方法,因为类无法重写它看不到或不能访问的方法。
这是Python程序员不熟悉的地方。在Python中,类中的所有变量和方法都是公有的,因此可在任何地方修改类变量以及调用类的方法,即便它们是仅供内部使用的。
- 访问限定符举例
下面来看一段包含两个类的Java源代码。
第一个类包含4个变量,而每个变量都使用了不同的访问限定符(或根本没有指定限定符):
package chapter02.access_modifiers.demonstration;public class DemoVariables {public String publicVariable = "This is a public variable";protected String protectedVariable = "This is a protected variable";String packagePrivateVariable = "This is a package-private variable";private String privateVariable = "This is a private variable";}
请注意,这个类放在chapter02.access_modifiers.demonstration包中。
第二个类位于chapter02.access_modifiers包中,它创建第一个类的实例:
package chapter02.access_modifiers;import chapter02.access_modifiers.demonstration.DemoVariables;public class AccessModifiersMain {public static void main(String[] args) {DemoVariables demo = new DemoVariables();System.out.println(demo.publicVariable);}}
虽然这两个包的名称都以chapter02.access_modifiers打头,但在JVM看来,它们之间毫无关系。
在JVM看来,只要包名不完全相同,它们就是不同的包。
另外,注意AccessModifiersMain类(以下称之为主类)创建了DemoVariables类(以下称之为演示类)的一个实例,但主类没有继承演示类。因此,我们可得出如下结论。
- 主类可随便访问演示类的变量<code>publicVariable</code>。事实上,这是主类能够看到并访问的唯一一个演示类成员。- 主类不能访问演示类的变量<code>protectedVariable</code>,因为这两个类位于不同的包中,且主类没有继承演示类。只要这两个条件有一个不同,主类就能访问这个变量。- 仅当主类和演示类位于同一个包中时,主类才能访问演示类的变量<code>packagePrivateVariable</code>。鉴于主类和演示类位于不同的包中,因此主类看不到也无法访问这个成员。- 只有演示类才能访问变量<code>privateVariable</code>,其他任何类都无法访问。
- 限定符
static——实例变量和类变量
通常,你将创建类的每个实例独有的变量,并添加使用这些数据的方法。在这种情况下,类的每个实例都有自己的变量值,因此修改变量只影响当前实例。要调用实例方法,必须通过类的实例,即经过初始化的引用类型变量。
JVM也支持类变量和类方法,这些成员无需通过类的实例就能使用,但它们可在类的所有实例之间共享。要定义类变量或类方法,必须在它们前面指定非访问限定符static:
public class StaticDemo {public static String staticVariable = "This is a static variable";public String instanceVariable = "This is a class instance variable";}
静态变量被称为类变量,在没有类的实例时也能访问。静态方法被称为类方法,在没有指向类实例的引用变量时也可调用。下面来看一个示例。为此,我们创建一个类,这个类创建了前述类的两个实例,并通过这两个实例修改了前述类中两个变量的值:
public class StaticDemoMain {public static void main(String[] args) {StaticDemo demo1 = new StaticDemo();demo1.staticVariable = "Demo 1 static";demo1.instanceVariable = "Demo 1 instance";StaticDemo demo2 = new StaticDemo();demo2.staticVariable = "Demo 2 static";demo2.instanceVariable = "Demo 2 Instance";System.out.println(StaticDemo.staticVariable);System.out.println(demo1.instanceVariable);System.out.println(demo2.instanceVariable);}}
如果你运行这个程序,将生成如下输出:
Demo 2 staticDemo 1 instanceDemo 2 instance
请注意,在前述代码中,通过引用变量(demo1或demo2)来访问staticVariable时,无法知道这是一个静态变量;但通过System.out.println(StaticDemo.staticVariable)来引用这个变量时,这一点显而易见,因为它是通过类名StaticDemo来访问这个静态变量的。
通过引用变量类访问静态成员被认为是糟糕的做法,因为这隐藏了访问的是静态成员这样的事实。如果通过类名来访问静态成员,就不会留下任何令人迷惑的地方。
静态方法只能访问其所属类的静态成员,而不能直接使用任何实例变量,也不能直接调用任何实例方法。
- 限定符
final——锁定类成员
要锁定类的方法或变量,可在它前面加上非访问限定符final。下面的Java示例演示了一个final静态int变量和一个final方法:
class FinalDemo1 {public final static int THIS_IS_A_CONSTANT_VALUE = 42;public final void thisMethodCanNotBeOverridden() { }}
将关键字final用于方法时,意味着任何类都不能重写它,而不管给这个方法指定的访问限定符是什么。如果有类依然试图重写这个方法,编译器将拒绝编译这个类。
将关键字final用于变量时,意味着它的值是不能修改的,这相当于将变量变成了常量。根据约定,final变量应声明为静态的,且名称应为全部大写。请注意,虽然final变量不可修改,但如果它指向的对象是可修改的,依然可以修改该对象的内容,下面的示例演示了这一点:
import java.util.ArrayList;class FinalDemo2 {private static final ArrayList<String> finalList = new ArrayList<>();public static final void main(String[] args) {finalList.add("Both strings can be added, because");finalList.add("the ArrayList itself is mutable.");}}
- 重载方法
在Java中,可定义方法的不同版本。当你调用这种方法时,编译器将寻找匹配的版本。如果找不到完全匹配的版本,编译器将检查是否有能够接受指定参数的版本。如果有多个版本与指定的参数完全匹配,或者没有任何版本匹配,编译器将报错;否则,编译器将调用匹配的版本。我们先来看一个真实的示例——方法java.lang.System.out.println(),再介绍方法重载规则。

本书前面多次使用了方法System.out.println(),它来自java.lang.System类,而System类位于唯一一个Java默认自动导入的包java.lang中。System类包含公有静态变量out,这是一个只读的final变量,指向一个java.io.PrintStream对象实例。println是java.io.Printstream类的方法之一,它有多个重载版本,如前面的类图所示。
重载规则如下。
- 每个版本的参数类型和/或顺序必须不同。例如,如果两个版本都只接受一个
long参数,编译器将无法确定该调用哪个。 - 理想情况下,同一个方法的所有重载版本的返回类型都应相同。如果两个版本只是返回类型不同,而参数类型和顺序相同,编译器将报错。
- 参数名一点都不重要。
- 如果没有找到完全匹配的版本,将拓宽基本类型值。鉴于
int总是可存储在long变量中,因此认为接受long参数的方法与之匹配。反过来则不正确,因为在不丢失数据的情况下,不会总是能够将long值转换为int,因此不会尝试这样的转换。 - 如果没有完全匹配的版本,且至少有一个参数为基本类型,将把它自动装箱为包装类,再尝试与每个版本匹配。每次尝试对一个参数做这样的处理,直到找到匹配的版本或执行完所有这样的尝试。
- 这种处理也适用于为基本类型包装类的参数——将它们自动拆箱为基本类型。
- 对于每个为类实例的参数,将其转换为父类(或其实现的接口),并尝试与方法的每个重载版本匹配,直到找到完全匹配的版本或转换成了
java.lang.Object类(本书前面说过,在JVM中,所有类都是从Object派生出来的)。
3.1.8 构造函数和终结方法
在Java中,可给类定义构造函数和终结方法(finalizer):
- 构造函数在创建类的实例时被调用;
方法
finalize()在对象被垃圾收集器收集时被JVM调用。构造函数
要定义构造函数,必须将其命名为与类同名,再加上用于包含参数的括号(()),如下所示:
public class ClassWithConstructor {public ClassWithConstructor() {}public ClassWithConstructor(int a, int b) {}}
对于构造函数,可使用的访问限定符与普通方法相同。与普通方法一样,构造函数也可重载。实例化前面的类时,可使用下面两个构造函数中的任何一个:
ClassWithConstructor c1 = new ClassWithConstructor();ClassWithConstructor c2 = new ClassWithConstructor(1, 2);
如果类没有定义任何构造函数,Java将隐式地生成一个。这样的构造函数类似于下面这样:
class ClassWithoutConstructor {public ClassWithoutConstructor() { }}
前面说过,可在构造函数前面指定访问限定符。用于构造函数时,访问限定符的含义与用于普通方法时完全相同。与普通方法一样,如果没有指定任何访问限定符,构造函数将被视为是包私有的,这种构造函数只能被当前类所属包中的类调用。
- 终结方法(finalizer)
不同于C++,Java没有真正意义上的析构函数,原因是所有著名的JVM实现都有垃圾收集器。无法保证对象一定会被垃圾收集器收集,因为这取决于程序中是否有指向它的引用以及其他一些随JVM实现而异的因素。
在对象即将被垃圾收集器收集时,垃圾收集器必须调用一个方法——finalize()。java.lang.Object类包含方法finalize(),任何类都可重写它:
@Overrideprotected void finalize() {}
其中的语法@Override稍后就会介绍。
这个方法可用来释放类占用的资源,但仅当万不得已时才应这样做——建议程序员尽早关闭资源。鉴于无法保证方法finalize()一定会被调用,因此在代码的其他地方关闭或释放资源要好得多。
有些程序员在方法finalize()必须释放占用的资源时记录一条日志消息(或使用简单的System.out.println()调用),因为这通常昭示着程序存在bug——原本应该早已在其他地方释放了这些资源。
- 扩展类
前面说过,JVM是一种只支持单继承的平台:

要继承类,可使用如下语法:
class SubClass extends ParentClass {}
别忘了,指定了非访问限定符
final的类是不能扩展的。
子类可访问它能够看到的父类的成员(变量和方法),而能否看到是由前面讨论类访问限定符时介绍的规则决定的。
(1) 重写方法
要访问父类的成员,可使用关键字super:
class TheParentClass {void aMethod() {}}class TheSubClass extends TheParentClass {@Overridepublic void aMethod() {super.aMethod();// 其他代码……}}
重写方法时,必须考虑其可见性。Java要求方法重写后的可见性不能降低;对于父类中受保护的方法,在子类中重写后可以是受保护的,也可以是公有的,但不能是私有或包私有的,因为这将降低可见性。前面的代码片段演示了这种概念:方法aMethod()在父类中是包私有的,而在子类中是公有的。
根据约定,应给重写的方法添加注解@Override,让人很容易知道它重写了一个既有的方法。将这个注解用于非重写方法时,编译器将报错。
注解旨在提供信号。就注解
@Override而言,它旨在提醒阅读源代码的开发人员。也有由编译器或框架进行处理的注解。
(2) 调用父类的构造函数
要调用父类的构造函数,也可使用关键字super:
class A {public A(int i) { }}class B extends A {B(int i) {super(i);}}
对于构造函数,Java有如下规则。
- 子类的每个构造函数都必须显式或隐式地调用父类的一个构造函数。
- 如果程序员没有给子类定义任何构造函数,父类必须包含一个不接受任何参数的构造函数。在这种情况下,Java将负责隐式地调用这个构造函数。
- 如果父类没有不接受任何参数的构造函数,子类的每个构造函数都必须使用有效的参数显式地调用父类的构造函数,且必须在子类构造函数的函数体中首先这样做。
- Java自动调用父类的不接受任何参数的构造函数(如果父类有这样的构造函数),但程序员也可手动这样做。这种调用必须是构造函数中的第一个非注释行。
下面通过一些示例来阐明上述规则。首先,来看父类和子类没有显式构造函数的情况:
class A { }class B extends A { }
这两个类都没有提供任何构造函数,因此Java将自动为它们创建一个不接受任何参数的公有构造函数:A()和B()。生成的构造函数B()将自动调用构造函数A(),程序员也可手动显式地调用这个构造函数,但这种调用必须是构造函数中的第一条语句,否则Java编译器将拒绝对代码进行编译。在这种情况下,Java将不会隐式地调用父类的构造函数:
class A { }class B extends A {public B() {super();}}
在下面的示例中,父类包含默认、公有、不接受任何参数的构造函数,而子类包含一个接受参数的公有构造函数:
class A { }class B extends A {public B(int i) {}}
A没有提供构造函数,因此Java将自动创建公有构造函数A()。B指定了一个构造函数,因此它不会有隐式、公有、不接受任何参数的构造函数。虽然B类的构造函数没有显式地调用构造函数A(),但Java依然会在使用构造函数B(int i)时调用构造函数A()。
在下面的示例中,父类有一个接受参数的构造函数:
class A {public A(String s) {}}class B extends A {public B() {super("Hello");}}
这里的情况比较有趣。父类只有一个带参数的构造函数,这意味着Java无法在子类中隐式地调用这个构造函数,因为它不想去猜测该将什么样的值传递给这个构造函数。现在,如果子类的构造函数没有显式地调用父类的构造函数,编译器将报错。另外,这种调用必须是子类构造函数的第一条语句,否则编译器也会报错。
- 抽象类
普通类被称为具体类。通过在类名前面加上非访问限定符abstract,可将类变成抽象类。
抽象类可被其他类扩展,但不能直接进行实例化。相比于具体类,抽象类的一个不同之处在于,它可以不给方法提供实现:
public abstract class AnAbstractClass {abstract public void thisIsAnAbstractMethod();}
必须给类本身以及每个没有实现的方法都指定限定符abstract。抽象类可以包含具体方法,且并非必须包含抽象方法。在扩展抽象类的具体类中,必须通过重写来为所有抽象方法提供实现。
与具体类一样,抽象类也可继承另一个类,而继承的类可以是具体类,也可以是抽象类,如下所示:

如果将这个示例转换为Java代码,结果将类似于下面这样:
abstract class AbstractClassA {public abstract void method1();}abstract class AbstractClassB extends AbstractClassA {public abstract void method2();}class ConcreteClass extends AbstractClassB {@Overridepublic void method1() { } // implementation code...@Overridepublic void method2() { } // Implementation code...}
由于抽象类AbstractClassB继承了抽象类AbstractClassA,因此ConcreteClass为这两个抽象类中的抽象方法提供实现。抽象类不能实例化,但引用变量可指向直接或间接扩展了抽象类的类:
AbstractClassA demo = new ConcreteClass();
demo只能用来访问AbstractClassA类的成员,而不能用来访问ConcreteClass的其他变量和方法。稍后你将看到,可将引用demo向下转换为ConcreteClass类型。
- 接口
接口有点像抽象类。在Java 8之前,接口与抽象类的最大不同在于,接口不能为其任何方法提供实现。类不是扩展接口,而是实现它们。
稍后你将看到,在Java 8中,接口可给其方法提供默认实现。
在下面的示例中,一个类实现了两个接口:

interface Interface1 {void method1();}interface Interface2 {public void method2();}class ConcreteClass implements Interface1, Interface2 {@Overridepublic void method1() { } // 实现代码……@Overridepublic void method2() { } // 实现代码……}
在源代码方面,接口遵守的规则与类相同。公有接口必须在与其名称完全相同的源代码文件中定义,因此一个源代码文件只能定义一个公有接口。另外,接口支持的访问限定符与类相同(公有和包私有)。
接口的成员默认为抽象和公有的。你可显式地添加关键字abstract和/或访问限定符public,但即便你省略限定符public,成员依然是公有的,而不是你可能认为的包私有的。接口成员只能是公有的,使用其他任何访问限定符都将导致编译器报错。
接口可包含方法和变量,但它们在接口中存在如下不同。
- 接口中的抽象方法总是公有的实例方法。
- 而接口中的变量总是
final和静态的。你也可以显式地指定限定符final和static。
无论是具体类还是抽象类,都可根据需要实现任意数量的接口,但只有具体类必须通过重写来为所有方法提供实现。
在前面的示例中,ConcreteClass实现了接口Interface1和Interface2,因此必须重写这两个接口中的方法。虽然在接口Interface1中,没有给方法method1()指定限定符public,但它默认为公有的。引用类型变量可指向实现了特定接口的类:
Interface2 i = new ConcreteClass();
前面说过,从Java 8起,接口可为其方法提供默认实现。之所以添加这项新功能,是因为给既有类添加方法时,将导致接口与实现了它的既有类不兼容,因此这些类必须修改才能通过编译。现在,可以提供默认实现了,这样修改接口后,实现了它的既有类依然将与其兼容。
下面是一个这样的示例:
interface ExistingInterface {public void methodWithoutImplementation();default public void methodWithImplementation() {// 实现……}}
另外,在Java 8中,可给接口添加静态方法。接口必须给静态方法提供实现,因为静态方法只能通过接口名来使用——通过引用类型变量无法访问接口的静态方法。
3.1.9 向上转换和向下转换
Java是一种静态类型的OOP语言。对象可转换为相关的类型。请看下面的类图:

由于在JVM中,java.lang.Object类是所有类的祖先类,因此包括这个类图中所有对象在内的每个对象都是java.lang.Object。同理,Book实例都是Product,Calculator和Phone实例亦如此。然而,Product实例不一定是Book实例:它可能是Book、Calculator、Phone或其任何一个子类的实例;如果不查看代码或文档,我们无法知道它是哪个子类的实例。
Java也面临这样的困境。我们说一个PaperBook实例是Product类的一个实例时,其实是将PaperBook向上转换成了Product实例。在代码中,转换类似于下面这样:
PaperBook paperBook = new PaperBook();Product product = paperBook;
Java编译器知道,PaperBook实例总是可向上转换为Product实例,因此第二行代码能够通过编译。如果编译器不认同转换,它将拒绝对代码进行编译。因此,编译器将拒绝编译下面的代码:
Product product = new PaperBook();PaperBook paperBook = product; // 这行代码不能通过编译
第一行没问题——PaperBook实例可自动向上转换为Product实例。但第二行不能通过编译,编译器显示的错误消息为incompatible types: Product cannot be converted to PaperBook。编译器只考虑变量product包含的是一个Product实例这一点,同时编译器不能保证Product实例都能成功地向下转换为PaperBook实例,因此它必须让程序员知道,这种转换可能失败。要让上述代码能够通过编译,必须做如下修改:
Product product = new PaperBook();PaperBook paperBook = (PaperBook)product;
现在,这些代码能够通过编译。在运行阶段,其中的Product实例将被向下转换为PaperBook实例。然而,如果我们犯了错,这样的转换将在运行阶段引发异常:
Product product = new PaperBook();Phone phone = (Phone)product;
由于引用类型变量product指向的是一个PaperBook实例,因此无法将其向下转换为Phone实例。在运行阶段,JVM将引发异常ClassCastException:
Exception in thread "main" java.lang.ClassCastException: PaperBookcannot be cast to Phone
如果你试图执行根本不可能的转换,编译器将拒绝编译代码,如下所示:
LandLinePhone landlinePhone = new LandLinePhone();CellularPhone cellularPhone = (LandLinePhone)landlinePhone;// 无法通过编译
Java编译器很聪明,知道LandLinePhone实例不可能是CellularPhone实例,因此拒绝编译这些代码。
