11. 继承和多态
面向对象的编程允许你从已经存在的类中定义新的类,这称为继承。
11.1 父类和子类
继承使得你可以定义一个通用的类(即父类),之后扩充该类为一个更加特定的类(即子类)。
使用类来对同一类型的对象建模。不同的类也可能会有一些共同的特征和行为,这些共同的特征和行为都统一放在一个类中,它是可以被其他类所共享的。你可以定义特定的类继承自通用类。这些特定的类继承通用类中的特征和方法。
考虑一下几何对象。假设要设计类建模像圆和矩形这样的几何对象。几何对象有许多共同的属性和行为。它们可以是用某种颜色画出来的、填充的或者不填充的。这样,一个通用类 CeometricObject 可以用来建模所有的几何对象。这个类包括属性 color 和 filled , 以及适用于这些属性的 get 和 set 方法。假设该类还包括 dateCreated 属性以及 getDateCreated( ) 和 toString( ) 方法。toString( ) 方法返回代表该对象的字符串。由于圆是一个特殊类型的几何对象,所以它和其他几何对象共享共同的属性和方法。因此,通过继承自 CeometricObject 类来定义 Circle 类是合理的。同理,Rectangle 也可以定义为 GeometricObject 的子类。下图显示了这些类之间的关系,指向父类的三角箭头用来表相关的两个类之间的继承关系。
在 Java 术语中,如果类 C1 扩展自另一个类 C2, 那么就将 C1 称为次类( subclass ), 将C2 称为超类(superclass )。超类也称为父类 ( parent class) 或基类 ( base class), 次类又称为子类 ( child class)、扩展类 ( extended class) 或派生类(derived class )。子类从它的父类中继承可访问的数据域和方法,还可以添加新数据域和新方法。 Circle 类继承了 CeometricObject 类所有可以访问的数据域和方法。除此之外,它还有一个新的数据域 radius, 以 及 与 radius 相关的 get 和 set 方法。它还包括 getArea( ) 、 getPerimeter( ) 和 getDiameter( ) 方法以返回圆的面积、周长和直径。Rectangle 类从 CeometricObject 类继承所有可访问的数据域和方法。此外,它还有 width 和 height 数据域,以及和它们相关的 get 和 set 方法。它还包括 getArea( ) 和 getPerimeter( ) 方法返回矩形的面积和周长。
下面是关于继承应该注意的几个关键点:
- 和传统的理解不同,子类并不是父类的一个子集。实际上,一个子类通常比它的父类包含更多的信息和方法。
- 父类中的私有数据域在该类之外是不可访问的。因此,不能在子类中直接使用。但是,如果父类中定义了公共的访问器 / 修改器,那么可以通过这些公共的访问器 / 修改器来访问和修改它们。
- 不是所有的 “是一种”(is - a) 关系都该用继承来建模。例如:正方形是一种矩形,但是不应该定义一个 Square 类来扩展 Rectangle 类,因为 width 和 height 属性并不适合于正方形。应该定义一个继承自 CeometricObject 类的 Square 类,并为正方形的边定义一个 side 属性。
- 继承是用来为 “是一种” 关系( is - a) 建模的。不要仅仅为了重用方法这个原因而盲目地扩展一个类。例如:尽管 Person 类和 Tree 类可以共享类似高度和重量这样的通用特性,但是从 Person 类扩展出 Tree 类是毫无意义的。一个父类和它的子类之间必须存在 “是一种”(is - a) 关系。
- 某些程序设计语言是允许从几个类派生出一个子类的。这种能力称为多重继承 ( multiple inheritance)。但是在 Java 中是不允许多重继承的。一个 Java 类只可能直接继承自一个父类。这种限制称为单一继承( single inheritance)。如果使用 extends 关键字来定义一个子类,它只允许有一个父类。然而,多重继承是可以通过接口来实现的。
11.2 使用 super 关键字
关键字 super 指代父类,可以用于调用父类中的普通方法和构造方法。
关键字 super 可以用于两种途径:
1 ) 调用父类的构造方法。
2 ) 调用父类的方法。
11.2.1 调用父类的构造方法
构造方法用于构建一个类的实例。不同于属性和普通方法,父类的构造方法不会被子类继承。它们只能使用关键字 super 从子类的构造方法中调用。
调用父类构造方法的语法是:
super()或者super(parameters);
语句 super( ) 调用父类的无参构造方法,而语句 super(arguments) 调用与参数匹配的父类的构造方法。语句 super( ) 和 super (arguments) 必须出现在子类构造方法的第一行,这是显式调用父类构造方法的唯一方式。
警告:要调用父类构造方法就必须使用关键字 super, 而且这个调用必须是构造方法的第一条语句。在子类中调用父类构造方法的名字会引起一个语法错误。
11.2.2 构造方法链
构造方法可以调用重载的构造方法或父类的构造方法。如果它们都没有被显式地调用,编译器就会自动地将 super( ) 作为构造方法的第一条语句。例如:
在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类的对象时,子类构造方法会在完成自己的任务之前,首先调用它的父类的构造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为止。 这就是构造方法链(constructor chaining)。
警告:如果要设计一个可以被继承的类,最好提供一个无参构造方法以避免程序设计错误。
思考下面的代码:
public class Apple extends Fruit {
}
class Fruit {
public Fruit(String name) {
System.out.println("Fruit' s constructor is invoked");
}
}
由于在 Apple 中没有显式定义的构造方法,因此,Apple 的默认无参构造方法被隐式调用。因为 Apple 是 Fruit 的子类,所以 Apple 的默认构造方法会自动调用 Fruit 的无参构造方法。然而,Fruit 没有无参构造方法,因为 Fruit 显式地定义了构造方法。因此,程序不能被成功编译。
设计指南:一般情况下,最好能为每个类提供一个无参构造方法,以便于对该类进行扩展,同时避免错误。
11.2.3 调用父类的方法
关键字 super 不仅可以引用父类的构造方法,也可以引用父类的方法。所用语法如下:
super.方法名(参数);
可以如下改写 Circle 类中的 printCircle( )方法:
public void printCircle() {
System.out.println("The circle is created " + super.getDateCreated() + " and the radius is " + radius);
}
在这种情况下,没有必要在 getDateCreated ( ) 前放置 super, 因为 getDateCreated 是 GeometHcObject 类中的一个方法并被 Circle 类继承。
11.3 方法重写
要重写一个方法,需要在子类中使用和父类一样的签名以及一样的返回值类型来对该方法进行定义。
子类从父类中继承方法。有时,子类需要修改父类中定义的方法的实现,这称作方法重写(method overriding)。
以下两点值得注意:
- 仅当实例方法是可访问时,它才能被覆盖。因为私有方法在它的类本身以外是不能访问的,所以它不能被覆盖。如果子类中定义的方法在父类中是私有的,那么这两个方法完全没有关系。
- 与实例方法一样,静态方法也能被继承。但是,静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。可以使用语法:父类名 .静态方法名( SuperClassName.staticMethodName) 调用隐藏的静态方法。
11.4 方法重写与重载
重载意味着使用同样的名字但是不同的签名来定义多个方法。重写意味着在子类中提供一个对方法的新的实现。
方法重写是指该方法必须使用相同的签名和相同的返回值类型在子类中定义。
让我们用一个例子来显示重写和重载的不同。在图 a 中,类 A 中的方法 p(double 1)重写了在类 B 中定义的相同方法。但是,在图 b中,类 A 中有两个重载的方法 p(double i)和 P(int i)e 方法 p(double i)继承自类 B。
运行 a 中的 Test 类时,a.p(10)和 a.p(10.0)调用的都是定义在类 A中的 p(double i) 方法. 所以程序都显示;10.0。运行 b中的 Test 类时,a.p(10)调用类 A中定义的 p(int i) 方法,显示输出为 10, 而 a.p(10.0)调用定义在类 B 中的 P(double i)方法,显示输出为 20.0。
注意以下问题:
- 方法重写发生在通过继承而相关的不同类中;方法重载可以发生在同一个类中,也可以发生在由于继承而相关的不同类中。
- 方法重写具有同样的签名和返回值类型;方法重载具有同样的名字,但是不同的参数列表。
为了避免错误,可以使用一个特殊的 Java 语法,称为重写标注 (override annotation) , 在子类的方法前面放一个 @Override。例如:
public class CircleFromSimpleGeometricObject
extends SimpleCeometricObject {
// Other methods are omitted
©Override
public String toString() {
return super.toString() + "\nradius is " + radius;
}
}
该标注表示被标注的方法必须重写父类的一个方法。如果具有该标注的方法没有重写父类的方法,编译器将报告一个错误。例如,如果 toString 被错误地输人为 tostring, 将报告一个编译错误。如果没有使用重写标注,编译器不会报告错误。使用标注可以避免错误。
11.5 Object 类及其 toString( )方法
Java 中的所有类都继承自 java.lang.Object 类。
如果在定义一个类时没有指定继承性,那么这个类的父类就被默认为是 Object。例如, 下面两个类的定义是一样的:
诸如 String、StringBuilder、Loan 和 GeometricObject 这样的类都是 Object的隐含子类。熟悉 Object 类提供的方法是非常重要的,因为这样就可以在自己的类中使用它们。本节将介绍 Object类中的 toString( ) 方法。
toString( ) 方法的签名是:
public String toString()
调用一个对象的 toStringO 会返回一个描述该对象的字符串。默认情况下,它返回一个由该对象所属的类名、at 符号(@)以及该对象十六进制形式的内存地址组成的字符串。例如,考虑下面定义 Loan 类的代码:
Loan loan = new Loan();
System.out.println(loan.toString());
这些代码会显示像 LOan®15037e5 的字符串。这个信息不是很有用,或者说没有什么信息量。通常,应该重写这个 toString 方法,这样,它可以返回一个代表该对象的描述性字符串。例如,Object类中的 toString 方法在 GeometricObject 类中重写。
public String toString() {
return "created on " + dateCreated + "\ncolor: " + color + " and filled: " + filled;
}
注意: 也 可 以 传 递 一 个 对 象 来 调 用 System.out.println(object)或 者 System.out.print(object)。这 等价于调用 System.out.println(object.toString( ))或 者 System.out.print (object - toString( ))。因此,可以使用 System.out.println(loan)来替换 System.out.println(loan.toString( ))
11.6 多态
多态意味着父类的变量可以指向子类对象。
面向对象程序设计的三大支柱是封装、继承和多态。
首先,定义两个有用的术语:子类型和父类型。一个类实际上定义了一种类型。子类定义的类型称为子类型(subtype), 而父类定义的类型称为父类型(supertype)。因此,可以说 Circle 是 GeometricObject的子类型,而 GeometricObject 是 Circle 的父类型。
继承关系使一个子类继承父类的特征,并且附加一些新特征。子类是它的父类的特殊化,每个子类的实例都是其父类的实例,但是反过来就不成立。例如:每个圆都是一个几何对象,但并非每个几何对象都是圆。因此,总可以将子类的实例传给需要父类型的参数。考虑下列程序清单的代码。
PolymorphismDemo.java
public class PolymorphismDemo {
/** Main method */
public static void main(String[] args){
// Display circle and rectangle properties
displayObject(new CircleFromSimpleCeometricObject(1, "red",false));
displayObject(new RectangleFromSimpleCeometricObject(1, 1,"black", true));
}
/** Display geometric object properties */
public static void displayObject(SimpleCeometricObject object){
System out.println("Created on + object.getDateCreatedO + Color is " + object.getColor());
}
}
显示
Created on Mon Mar 09 19:25:20 EDT 2011.Color is red
Created on Mon Mar 09 19:25:20 EDT 2011.Color is black
方法 displayObject (第 12 行〉具有 GeometricObject 类型的参数。可以通过传递任何一个 GeometricObject 的实例(例如:在第 5~8 行的 new Ci rcleFromSimpleCeometricObject(1,"red".false)和 new RectangleFromSimpleGeometricObject(1,1,"black",false))来调用 displayObject。使用父类对象的地方都可以使用子类的对象。这就是通常所说的多态 ( polymorphism, 它源于希腊文字,意思是 “多种形式” )。简单来说,多态意味着父类型的变量可以引用子类型的对象。
11.7 动态绑定
方法可以在沿着继承链的多个类中实现。JVM 决定运行时调用哪个方法。
方法可以在父类中定义而在子类中重写。例如:toString( )方法是在 Object 类中定义的,而在 GeometricObject 类中重写。思考下面的代码:
Object o = new CeometricObject();
System.out.println(o.toStHng());
这里的 0 调用哪个 tostringo 呢?为了回答这个问题,我们首先介绍两个术语:声明类型和实际类型。一个变量必须被声明为某种类型。变量的这个类型称为它的声明类型(declared type)。这里,o 的声明类型是 Object。一个引用类型变量可以是一个 null 值或者是一个对声明类型实例的引用。实例可以使用声明类型或它的子类型的构造方法创建。变量的实际类型(actual type) 是被变量引用的对象的实际类。这里,o 的实际类型是 GeometricObject, 因为 o 指向使用 new GeometricObject( ) 创建的对象D o 调用哪个 toString( ) 方法由 o 的实际类型决定。这称为动态绑定(dynamic binding)。
动态绑定工作机制如下:假设对象 o 是类 C1, C2, … ,Cn - 1, Cn 的实例,其中 C1是 C2 的子类,C2 是 C3 的子类,… ,Cn - 1是 Cn 的子类,如下图所示。也就是说,Cn 是最通用的类,C1是最特殊的类。在 Java 中,Cn 是 Object 类。如果对象 o 调用一个方法 p, 那么 JVM 会依次在类 C1,C2, … ,Cn - 1,Cn中查找方法 p 的实现,直到找到为止。一旦找到一个实现,就停止査找,然后调用这个首先找到的实现。
11.8 对象转换和 instanceof 运算符
对象的引用可以类型转换为对另外一种对象的引用,这称为对象转换。
在上一节中,语句
m(new Student());
将对象 new Student( ) 赋值给一个 Object 类型的参数。这条语句等价于
Object o = new Student(); // Implicit casting
m(o);
由于 Student 的实例也是 Object 的实例,所以,语句 Object o = new StudentO 是合法的,它称为隐式转换(implicit casting)。
假设想使用下面的语句把对象引用 o 赋值给 Student 类型的变量:
Student b = o;
在这种情况下,将会发生编译错误。为什么语句 Object o = new Student( ) 可以运行,而语句 Student b = o 不行呢?原因是 Student 对象总是 Object 的实例,但是,Object 对象不一定是 Student 的实例。即使可以看到 0实际上是一个 Student 的对象,但是编译器还没有聪明到知道这一点。为了告诉编译器 0就是一个 Student 对象,就要使用显式转换 ( explicit casting)o 它的语法与基本类型转换的语法很类似,用圆括号把目标对象的类型括住,然后放到要转换的对象前面,如下所示:
Student b = (Student)o;// Explicit casting
总是可以将一个子类的实例转换为一个父类的变量,称为向上转换(upcasting),因为子类的实例永远是它的父类的实例。当把一个父类的实例转换为它的子类变量(称为向下转换 (downcasting)) 时,必须使用转换记号 “(子类名)” 进行显式转换,向编译器表明你的意图。为使转换成功,必须确保要转换的对象是子类的一个实例。如果父类对象不是子类的一个实例,就会出现一个运行异常 ClassCastException。例如:如果一个对象不是 Student的实例,它就不能转换成 Student 类型的变量。因此,一个好的经验是,在尝试转换之前确保该对象是另一个对象的实例。这是可以利用运算符 hstanceof 来实现的。考虑下面的代码:
Object myObject = new Circle();
... // Some lines of code
/** Perform casting if myObject is an instance of Circle */
if (myObject instanceof Circle){
System.out.println("The circle diameter is + ((Circle)myObject).getDiameter());
...
}
你可能会奇怪为什么必须进行类型转换。变量 myObject 被声明为 Object。声明类型决定了在编译时匹配哪个方法。使用 myObject.getDUmeterO 会引起一个编译错误,因为 Object 类没有 getDiameter 方法。编译器无法找到和 myObject.getDiameter( )匹配的方法。所以,有必要将 myObject 转换成 Circle 类型,来告诉编译器 myObject 也是 Circle 的一个实例。
为什么没有在一开始就把 myObject定义为 Circle 类型呢?为了能够进行通用程序设计,一个好的经验是把变童定义为父类型,这样,它就可以接收任何子类型的值。
注意:instanceof 是Java 的关键字。在 Java 关键字中的每个字母都是小写的。
提示:为了更好地理解类型转换,可以认为它们类似于水果、苹果、檷子之间的关系,其中水果类 Fruit 是苹果类 Apple 和橘子类 Orange 的父类。苹果是水果,所以,总是可以将 Apple 的实例安全地賦值给 Fruit 变量。但是,水果不一定是苹果,所以,必须进行显式转换才能将 Fruit 的实例赋值给 Apple 的变量。
警告:对象成员访问运算符( .)优先于类型转换运算符。使用圆括号保证在点运算符( )之前进行转换,例如:
((Circl e)object).getArea();
对基本类型值进行转换不同于对对象引用进行转换。转换基本类型值返回一个新的值。
例如:
int age = 45;
byte newAge = (byte)age; // A new value is assigned to newAge
而转换一个对象引用不会创建一个新的对象,例如:
Object o = new Circle();
Circle c = (Circle)o; // No new object is created
现在,引用变量 0 和 c 指向同一个对象。
11.9 Object 类的 equals 方法
如同 toString( ) 方法,equals(Object) 方法是定义在 Object 类中的另外一个有用的方法。
在 Object 类中定义的另外一个经常使用的方法是 equals 方法。它的签名是:
public boolean equals(Object o)
这个方法测试两个对象是否相等。调用它的语法是:
objectl.equals(object2);
Object 类中 equals 方法的默认实现是:
public boolean equals(Object obj) {
return (this obj);
}
这个实现使用 == 运算符检测两个引用变量是否指向同一个对象。因此,应该在自己的客户类中重写这个方法,以测试两个不同的对象是否具有相同的内容。
equals 方法在 Java API的许多类中被重写,比 如 java.lang.String 和 java.util .Date, 用于比较两个对象的内容是否相等。String类中的 equals 方法继承自 Object 类,然后在 String类中被重写,使之能够检验两个字符串的内容是否相等。
可以重写 Circle 类中的 equals 方法,基于圆的半径比较两个圆是否相等,如下所示:
public boolean equals(Object o) {
if (o instanceof Circle)
return radius == ((Circle)o) .radius;
else
return this == o;
}
注意:比较运算符一用来比较两个基本数据类型的值是否相等,或者判断两个对象是否具有相同的引用。如果想让 equals 方法能够判断两个对象是否具有相同的内容,可以在定义这些对象的类时,重写 Circle 类中的 equals 方法。运算符 == 要比 equals 方法的功能强大些,因为 == 运算符可以检测两个引用变量是否指向同一个对象。
警告:在子类中,使用签名 equals(SomeClassName obj) (例如:equals(Circle c)) 重写 equals 方法是一个常见错误,应该使用 equals(Object obj)。
11.10 ArrayList 类
ArrayList 对象可以用于存储一个对象列表。
现在,我们介绍一个很有用的用于存储对象的类了。可以创建一个数组存储对象,但是这个数组一旦创建,它的大小就固定了。Java 提供 ArrayList 类来存储不限定个数的对象。下图给出了 ArrayList中的一些方法。
ArrayList 是一种泛型类,具有一个泛型类型 E。创建一个 ArrayList时,可以指定一个具体的类型来替换 E。例如,下面语句创建一个 ArrayList, 并且将其引用赋值给变量 cities。该 ArrayList 对象可以用于存储字符串。
ArrayList<String> cities = new ArrayList<String>();
下面语句创建一个 ArrayList 并且将其引用赋值给变量 dates。该 ArrayList 对象可以用于存储日期。
ArrayList< java.util .Date> dates = new ArrayList< java.util .Date> ();
注意:从 JDK1.7 开始,语句
ArrayList<AConcreteType> list = new ArrayList<AConcreteType>();
可以简化为
ArrayList<AConcreteType> list = new ArrayList<>();
由于使用了称为类型推导的特征,构造方法中不再要求给出具体类型。编译器可以从变量的声明中推导出类型。
可以像使用数组一样使用 ArrayList 对象,但是两者还是有很多不同之处。下表列出了它们的异同点。
— 旦创建了一个数组,它的大小就确定下来了。可以使用方括号访问数组元素(例如: a[index])0 当 创 建 ArrayList 后,它的大小为 0。如果元索不在数组列表中,就不能使用 get(index)和 set(index .element)方法。向数组列表中添加、插人和删除元素是比较容易的,而向数组中添加、插人和删除元素是比较复杂的。为了实现这些操作,必须编写代码操纵这个数组。注意,可以使用;java.util.Arrays.sortUrray)方法来对一个数组排序。如果要对一个数组列表排序,使用 java.util.Collections.sort(arrayList)方法。
假设想创建一个用于存储整数的 ArrayList, 可以使用下面代码来创建一个列表吗?
ArrayList<int> list = new ArrayList<>();
答案是不行。这样行不通,因为存储在 ArrayList 中的元素必须是一种对象。不能使用诸如 int 的基本数据类型来代替一个泛型类型。然而,你可以创建一个存储 Integer 对象的 ArrayList, 如下所示:
ArrayList<Integer> list = new ArrayList<>();
使用 ArrayList 来实现程序将更简单,有以下两个原因。
- ArrayList 的大小是灵活的,所以无须提前给定它的大小。而当创建一个数组时,它的大小必须给定。
- ArrayList 包含许多有用的方法。比如,可以使用 contains 方法来测试某个元素是否在列表中。如果使用数组,则需要编写额外代码来实现该方法。
可以在数组里使用 foreach 循环来遍历元素。数组列表中的元素也可以使用 foreach 循环来进行遍历,语法如下:
for (elementType element: arrayList){
// Process the element
}
11.11 对于列表有用的方法
Java 提供了方法,用于从数组创建列表、对列表排序、找到列表中的最大和最小元素,以及打乱一个列表。
我们经常需要从一个对象数组中创建一个数组列表,或者相反。可以使用循环来实现,但是更容易的方法是使用 Java API中的方法。这里是一个从数组中创建一个数组列表的例子:
String[] array = {"red","green","blue"};
ArrayList<String> list = new ArrayListo(Arrays.asList(array));
Arrays 类中的静态方法 asList 返回一个列表,该列表传递给 ArrayList 的构造方法用于创建一个 ArrayList。反过来,可以使用下面代码从一个数组列表来创建一个对象数组。
String[] arrayl = new String[list.size()];
list.toArray(array1);
调用 list.toArray(arrayl)将 list中的内容复制到 array1中。
如果列表中的元素是可比较的,比如整数、双精度浮点数或者字符串,则可以使用 java.util .Collections 类中的静态的 sort 方法来对元素进行排序。这里是一些例子:
Intege「[] array = {3, 5, 95, 4, 15, 34, 3, 6, 5};
ArrayList<Integer> list = new ArrayListo(Arrays.asList(array));
java.util .Collections.sort(1ist);
System.out.println(list);
可以使用 java.util .Collections 类中的静态的 max 和 min 方法来返回列表中的最大和最小元素。这里是一些例子:
Integer[] array = {3, 5, 95, 4, 15, 34, 3, 6, 5};
ArrayList<Integer> list = new ArrayListo(Arrays.asList(array));
System.out.println(java.util.Collections.max(list));
System.out.println(java.util.Collections.min(list));
可以使用 java.util .Collections 类中的静态的 shuffle 方法来随机打乱列表的元素。这里是一些例子:
Integer[] array = {3, 5, 95, 4, 15, 34, 3, 6, 5};
ArrayList<Integer> list = new ArrayListo(Arrays.asList(array));
java.util .Col1ections.shuff1e(list);
System.out.println(list);
11.12 protected 数据和方法
一个类中的受保护成员可以从子类中访问。
至今为止,我们已经使用过关键字 private 和 public 来指定是否可以从类的外部访问数据域和方法。私有成员只能在类内访问,而公共成员可以被任意的其他类访问。
经常需要允许子类访问定义在父类中的数据域或方法,但不允许非子类访问这些数据域和方法。可以使用关键字 Protected 完成该功能。父类中被保护的数据域或方法可以在它的子类中访问。
修饰符 private、 protected 和 public 都称为可见性修饰符( visibility modifier) 或可访问性修饰符(accessibility modifier), 因为它们指定如何访问类和类的成员。这些修饰符的可见性按下面的顺序递增:
下表总结了类中成员的可访问性。
描述了C1类中的 public、protected、默认的和 private 数据或方法是如何被 C2、C3、C4 和 C5 类访问的,其中,C2 类与 C1类在同— 个包中、C3 类是 C1类在同一个包中的子类、C4 类是 C1类在不同包中的子类、C5 类与 C1 类在不同包中。
使用 private 修饰符可以完全隐藏类的成员,这样,就不能从类外直接访问它们。不使用修饰符就表示允许同一个包里的任何类直接访问类的成员,但是其他包中的类不可以访问。使用 Protected 修饰符允许任何包中的子类或同一包中的类访问类的成员。使用 public 修饰符允许任意类访问类的成员。
类可以以两种方式使用:一种是用于创建该类的实例;另一种是通过扩展该类创建它的子类。如果不想从类的外部使用类的成员,就把成员声明成 private。如果想让该类的用户都能使用类的成员,就把成员声明成 public。如果想让该类的扩展者使用数据和方法,而不想让该类的用户使用,则把成员声明成 Protected。
修饰符 private 和 protected 只能用于类的成员。public 修饰符和默认修饰符(也就是没有修饰符)既可以用于类的成员,也可以用于类。一个没有修饰符的类(即非公共类)是不能被其他包中的类访问的。
注意:子类可以重写它的父类的 protected 方法,并把它的可见性改为 public。但是,子类不能削弱父类中定义的方法的可访问性。例如:如果一个方法在父类中定义为 public,在子类中也必须定义为 public。
11.13 防止扩展和重写
一个被 final 修饰的类和方法都不能被扩展。被 final 修饰的数据域是一个常数。
有时候,可能希望防止类扩展。在这种情况下,使用 final 修饰符表明一个类是最终的,是不能作为父类的。Math 类就是一个最终类。String、StringBuilder 和 StringBuffer 类也可以是最终类。例如,下面的类 A 就是最终类,是不能被继承的:
public final class A {
// Data fields, constructors, and methods omitted
}
也可以定义一个方法为最终的,最终方法不能被它的子类重写。
例如,下面的方法是最终的,是不能重写的:
public class Test {
// Data fields, constructors, and methods omitted
public final void m(){
// Do something
}
}
注意:修饰符 public、protected、private、static、 abstract 以及 final 可以用在类和类的成员(数据和方法)上,只有 final 修饰符还可以用在方法中的局部变量上。方法内的最终局部变量就是常量。