继承和多态
面向对象的编程允许从已经存在的类中定义新的类,这称为继承。
面向过程的范式重点在于方法的设计,而面向对象的范式将数据和方法结合在对象中。面向对象范式的软件设计着重于对象以及对象上的操作。面向对象的方法结合了面向过程范式的强大之处,并且进一步将数据和操作集成在对象中。
继承是 Java 在软件重用方面一个重要且功能强大的特征。
父类和子类
继承使得可以定义一个通用的类 (即父类) ,之后扩充该类为一个更加特定的类 (即子类) 。
使用类来对同一类型的对象建模。不同的类也可能会有一些共同的特征和行为,这些共同的特征和行为都统一放在一个类中,它是可以被其他类所共享的。可以定义特定的类继承自通用类。这些特定的类继承通用类中的特征和方法。
在 Java 术语中,如果类 Cl 扩展自另一个类 C2, 那么就将 Cl 称为次类 (subclass) 将C2 称为超类 (superclass) 。超类也称为父类 (parent class) 或基类 (base class) ,次类又称为子类 (child class) 、扩展类 (extended class) 或派生类 (derived class) 。子类从它的父类中继承可访问的数据域和方法,还可以添加新数据域和新方法。
避免命名冲突最好的方法应该是将这些类放到不同的包中。
下面是关于继承应该注意的几个关键点:
- 和传统的理解不同,子类并不是父类的一个子集。实际上,一个子类通常比它的父类包含更多的信息和方法。
- 父类中的私有数据域在该类之外是不可访问的。因此,不能在子类中直接用。但是,如果父类中定义了公共的访问器/修改器,那么可以通过这些公共的访问器/修改器来访问和修改它们。
- 不是所有的 “是一种” (is-a) 关系都该用继承来建模。
- 继承是用来为 “是一种” 关系 (is-a) 建模的。不要仅仅为了重用方法这个原因而盲目地扩展一个类。。一个父类和它的子类之间必须存在 “是一种” (is-a) 关系。
- 某些程序设计语言是允许从几个类派生出一个子类的。这种能力称为多重继承 (multiple inheritance)。但是在 Java 中是不允许多重继承的。一个 Java 类只可能直接继承自一个父类。这种限制称为单一继承 (single inheritance) 。如果使用 extends 关键字来定义一个子类,它只允许有一个父类。不过,多重继承是可以通过接口来实现。
使用 super 关键字
子类继承它的父类中所有可访问的数据域和方法。关键字 super 指代父类,可以用于调用父类中的普通方法和构造方法。
关键字 this 的作用,它是对调用对象的引用。关键字 super 是指这个 super 关键字所在的类的父类。
关键字 super 可以用于两种途径:
- 调用父类的构造方法
- 调用父类的方法。
调用父类的构造方法
构造方法用于构建一个类的实例。不同于属性和普通方法,父类的构造方法不会被子类继承。它们只能使用关键字 super 从子类的构造方法中调用。
调用父类构造方法的语法是:
super();
//或者
super(parameters);
语句 super() 调用父类的无参构造方法,而语句 super(arguments) 调用与参数匹配的父类的构造方法。语句 super() 和 super(arguments) 必须出现在子类构造方法的第一行,这是显式调用父类构造方法的唯一方式。
要调用父类构造方法就必须使用关键字 super ,而且这个调用必须是构造方法的第一条语句。在子类中调用父类构造方法的名字会引起一个语法错误。
构造方法链
构造方法可以调用重载的构造方法或父类的构造方法。如果它们都没有被显式地调用,编译器就会自动地将 super() 作为构造方法的第一条语句。
在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类的对象时,子类构造方法会在完成自己的任务之前,首先调用它的父类的构造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为止。这就是构造方法链 (constructor chaining) 。
设计指南: 一般情况下,最好能为每个类提供一个无参构造方法,以便于对该类进行扩展,同时避免错误。
调用父类的方法
关键字 super 不仅可以引用父类的构造方法,也可以引用父类的方法。所用语法如下:
super.方法名(参数);
方法重写
要重写一个方法,需要在子类中使用和父类一样的签名以及一样的返回值类型来对该方法进行定义。
子类从父类中继承方法。有时,子类需要修改父类中定义的方法的实现,这称作方法重写 (method overriding) 。
以下几点值得注意:
- 仅当实例方法是可访问时,它才能被覆盖。因为私有方法在它的类本身以外是不能访问的,所以它不能被覆盖。如果子类中定义的方法在父类中是私有的,那么这两个方法完全没有关系。
- 与实例方法一样,静态方法也能被继承。但是,静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。可以使用语法:
//调用隐藏的静态方法
父类名.静态方法名(SuperClassName.staticMethodName);
方法重写与重载
重载意味着使用同样的名字但是不同的签名来定义多个方法。重写意味着在子类中提供一个对方法的新的实现。方法重写是指该方法必须使用相同的签名和相同的返回值类型在子类中定义。
注意以下问题:
- 方法重写发生在通过继承而相关的不同类中;方法重载可以发生在同一个类中,也可以发生在由于继承而相关的不同类中。
- 方法重写具有同样的签名和返回值类型;方法重载具有同样的名字,但是不同的参数列表。
为了避免错误,可以使用一个特殊的 Java 语法,称为重写标注 (override annotation) ,在子类的方法前面放一个 @Override 。该标注表示被标注的方法必须重写父类的一个方法。如果具有该标注的方法没有重写父类的方法,编译器将报告一个错误。如果没有使用重写标注,编译器不会报告错误。使用标注可以避免错误。
Object 类及其 toString() 方法
Java 中的所有类都继承自 java.lang.Object 类。如果在定义一个类时没有指定继承性,那么这个类的父类就被默认为是 Object。熟悉 Object 类提供的方法是非常重要的,因为这样就可以在自己的类中使用它们。
Object类中的 toString() 方法。
toString() 方法的签名是:
public String toString()
调用一个对象的 toString() 会返回一个描述该对象的字符串。默认情况下,它返回一个由该对象所属的类名、at 符号 (@) 以及该对象十六进制形式的内存地址组成的字符串。这个信息不是很有用,或者说没有什么信息量。通常,应该重写这个 toString 方法,这样,它可以返回一个代表该对象的描述性字符串。
注意:也可以传递一个对象来调用 System.out.println(object) 或 者 System.out.print(object) 。这 等 价 于 调 用 System.out.println(object.toString()) 或 者 System.out.print(object.toString()) 。
多态
多态意味着父类的变量可以指向子类对象。
面向对象程序设计的三大支柱是封装、继承和多态。
首先,定义两个有用的术语:子类型和父类型。一个类实际上定义了一种类型。子类定义的类型称为子类型 (subtype) ,而父类定义的类型称为父类型 (supertype) 。
继承关系使一个子类继承父类的特征,并且附加一些新特征。子类是它的父类的特殊化,每个子类的实例都是其父类的实例,但是反过来就不成立。因此,总可以将子类的实例传给需要父类型的参数。
使用父类对象的地方都可以使用子类的对象。这就是通常所说的多态 (polymorphism) ,意思是 “多种形式” 。简单来说,多态意味着父类型的变量可以引用子类型的对象。
动态绑定
方法可以在沿着继承链的多个类中实现。JVM 决定运行时调用哪个方法。
方法可以在父类中定义而在子类中重写。
声明类型和实际类型。一个变量必须被声明为某种类型。变量的这个类型称为它的声明类型 (declared type) 。。一个引用类型变量可以是一个 null 值或者是一个对声明类型实例的引用。实例可以使用声明类型或它的子类型的构造方法创建。变量的实际类型 (actual type) 是被变量引用的对象的实际类。
动态绑定工作机制如下:假设对象 o 是类 Cl , C2 , … , Cn-1 , Cn 的实例,其中 C1 是 C2 的子类,C2 是 C3 的子类,… ,Cn-1 是 Cn 的子类。也就是说,Cn 是最通用的类,C1 是最特殊的类。在 Java 中,Cn 是 Object 类。如果对象 o 调用一个方法 p ,那么 JVM 会依次在类 Cl ,C2 , … ,Cn-1 ,Cn 中查找方法 p 的实现,直到找到为止。一旦找到一个实现,就停止査找,然后调用这个首先找到的方法的实现。被调用的方法是在运行时动态绑定的。
匹配方法的签名和绑定方法的实现是两个不同的问题。引用变量的声明类型决定了编译时匹配哪个方法。在编译时,编译器会根据参数类型、参数个数和参数顺序找到匹配的方法。一个方法可能在沿着继承链的多个类中实现 Java 虚拟机在运行时动态绑定方法的实现,这是由变量的实际类型决定的。
对象转换和 instanceof 运算符
对象的引用可以类型转换为对另外一种对象的引用,这称为对象转换。
隐式转换 (implicit casting) :自动转换。
显式转换 (explicit casting) :它的语法与基本类型转换的语法很类似,用圆括号把目标对象的类型括住,然后放到要转换的对象前面。
总是可以将一个子类的实例转换为一个父类的变量,称为向上转换 (upcasting) ,因为子类的实例永远是它的父类的实例。当把一个父类的实例转换为它的子类变量时,称为向下转换 (downcasting) ,必须使用转换记号 “(子类名)” 进行显式转换,向编译器表明你的意图。为使转换成功,必须确保要转换的对象是子类的一个实例。如果父类对象不是子类的一个实例,就会出现一个运行异常 ClassCastException 。因此,一个好的经验是,在尝试转换之前确保该对象是另一个对象的实例。这是可以利用运算符 hstanceof 来实现的。
声明类型决定了在编译时匹配哪个方法。为了能够进行通用程序设计,一个好的经验是把变童定义为父类型,这样,它就可以接收任何子类型的值。
instanceof 是 Java 的关键字。在 Java 关键字中的每个字母都是小写的。
只有源对象是目标类的实例时才能进行类型转换在执行转换前,程序使用 instanceof 运算符来确保源对象是否是目标类的实例。
警告: 对象成员访问运算符 (.) 优先于类型转换运算符。使用圆括号保证在点运算符 (.) 之前进行转换
对基本类型值进行转换不同于对对象引用进行转换。转换基本类型值返回一个新的值。而转换一个对象引用不会创建一个新的对象。
Object 类的 equals 方法
如同 toString() 方法,equals(Object) 方法是定义在 Object 类中的另外一个有用的方法。
在 Object 类中定义的另外一个经常使用的方法是 equals 方法。它的签名是:
public boolean equals(Object o)
Object 类中 equals 方法的默认实现是:
public boolean equals(Object obj)
{
return (this == obj);
}
这个实现使用 == 运算符检测两个引用变量是否指向同一个对象。因此,应该在自己的客户类中重写这个方法,以测试两个不同的对象是否具有相同的内容。equals 方法在 Java API 的许多类中被重写。
注意: 比较运算符一用来比较两个基本数据类型的值是否相等,或者判断两个对象是否具有相同的引用。如果想让 equals 方法能够判断两个对象是否具有相同的内容,可以在定义这些对象的类时,重写 equals 方法。运算符 == 要比 equals 方法的功能强,因为一运算符可以检测两个引用变量是否指向同一个对象。
警告: 在子类中,使用签名 equals(SomeClassName obj) 重写
equals 方法是一个常见错误,应该使用 equals(Object obj) 。
ArrayList 类
ArrayList 对象可以用于存储一个对象列表。
创建一个数组存储对象,但是这个数组一旦创建,它的大小就固定了。
Java 提供 ArrayList 类来存储不限定个数的对象。
ArrayList 是一种泛型类,具有一个泛型类型 E 。创建一个 ArrayList 时,可以指定一个具体的类型来替换 E 。
注意: 从 JDK1.7 开始,语句
ArrayList<AConcreteType> list = new ArrayList<AConcreteType>();
可以简化为
ArrayList<AConcreteType> list = new ArrayList<>();
由于使用了称为类型推导的特征,构造方法中不再要求给出具体类型。编译器可以从变量的声明中推导出类型。
方法 toString() 返回数组列表的字符串表示,其形式为 [e0.toString(), e1.toString(), …, ek.toString()] , 这里的 e0, e1, …, ek 都是数组列表中的元素。
可以像使用数组一样使用 ArrayList 对象,但是两者还是有很多不同之处。
一旦创建了一个数组,它的大小就确定下来了。可以使用方括号访问数组元素。当创建 ArrayList 后,它的大小为 0 。如果元索不在数组列表中,就不能使用 get(index) 和 set(index,element)方法。向数组列表中添加、插人和删除元素是比较容易的,而向数组中添加、插人和删除元素是比较复杂的。为了实现这些操作,必须编写代码操纵这个数组。
注意: 可以使用 java.util.Arrays.sort(array) 方法来对一个数组排序。如果要对一个数组列表排序,使用 java.util.Collections.sort (arrayList) 方法。
因为存储在 ArrayList 中的元素必须是一种对象。不能使用诸如 int 的基本数据类型来代替一个泛型类型。然而,你可以创建一个存储 Integer 对象的
ArrayList,如下所示:
ArrayList<Integer> list = new ArrayList<>();
使用 ArrayList 比数组更简单,有以下两个原因:
- ArrayList 的大小是灵活的,所以无须提前给定它的大小。而当创建一个数组时,它的大小必须给定。
- ArrayList 包含许多有用的方法。比如,可以使用 contains 方法来测试某个元素是否在列表中。如果使用数组,则需要编写额外代码来实现该方法。
可以在数组里使用 foreach 循环来遍历元素。数组列表中的元素也可以使用 foreach 循环来进行遍历,语法如下:
for (elementType element: arrayList)
{
// Process the element
}
对于列表有用的方法
Java 提供了方法,用于从数组创建列表、对列表排序、找到列表中的最大和最
小元素,以及打乱一个列表。
经常需要从一个对象数组中创建一个数组列表,或者相反。可以使用循环来实现,但是更容易的方法是使用 Java API 中的方法。
Arrays 类中的静态方法 asList 返回一个列表,该列表传递给 ArrayList 的构造方法用于创建一个 ArrayList 。反过来,可以从一个数组列表来创建一个对象数组。
如果列表中的元素是可比较的,比如整数、双精度浮点数或者字符串,则可以使用 java.util .Collections 类中的静态的 sort 方法来对元素进行排序。
可以使用 java.util.Collections 类中的静态的 max 和 min 方法来返回列表中的最大元素和最小元素。
可以使用 java.util.Collections 类中的静态的 shuffle 方法来随机打乱列表的元素。
protected 数据和方法
一个类中的受保护成员可以从子类中访问。
使用过关键字 private 和 public 来指定是否可以从类的外部访问数据域和方法。私有成员只能在类内访问,而公共成员可以被任意的其他类访问。
需要允许子类访问定义在父类中的数据域或方法,但不允许非子类访问这些数据域和方法。可以使用关键字 Protected 完成该功能。父类中被保护的数据域或方法可以在它的子类中访问。
修饰符 private 、protected 和 public 都称为可见性修饰符 (visibility modifier) 或可访问性修饰符 (accessibility modifier), 因为它们指定如何访问类和类的成员。这些修饰符的可见性按下面的顺序递增:
私有、默认(无修饰符) 、被保护、公共成员
类中成员数据和方法的可见性:
类中成员修饰符 | 在同一类中可访问 | 在同一包内可访问 | 在子类内可访问 | 在不同包可访问 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | — |
(default) | √ | √ | — | — |
private | √ | — | — | — |
使用 private 修饰符可以完全隐藏类的成员,这样,就不能从类外直接访问它们。不使用修饰符就表示允许同一个包里的任何类直接访问类的成员,但是其他包中的类不可以访问。使用 protected 修饰符允许任何包中的子类或同一包中的类访问类的成员。使用 public 修饰符允许任意类访问类的成员。
类可以以两种方式使用:一种是用于创建该类的实例;另一种是通过扩展该类创建它的子类。如果不想从类的外部使用类的成员,就把成员声明成 private。如果想让该类的用户都能使用类的成员,就把成员声明成 public。如果想让该类的扩展者使用数据和方法,而不想让该类的用户使用,则把成员声明成 protected。
修饰符 private 和 protected 只能用于类的成员。public 修饰符和默认修饰符 (也就是没有修饰符) 既可以用于类的成员,也可以用于类。一个没有修饰符的类 (即非公共类) 是不能被其他包中的类访问的。
注意: 子类可以重写它的父类的 protected 方法,并把它的可见性改为 public。但是,子类不能削弱父类中定义的方法的可访问性。例如:如果一个方法在父类中定义为 public,在子类中也必须定义为 public。
防止扩展和重写
一个被 final 修饰的类和方法都不能被扩展。被 final 修饰的数据域是一个常数。
有时候,可能希望防止类扩展。在这种情况下,使用 final 修饰符表明一个类是最终的,是不能作为父类的,不能被继承的。Math 类 ,String 、StringBuilder 和 StringBuffer 类也可以是最终类。也可以定义一个方法为最终的,最终方法不能被它的子类重写。
注意: 修饰符 public 、protected 、private 、static 、abstract 以及 final 可以用在类和类的成员 (数据和方法) 上,只有 final 修饰符还可以用在方法中的局部变量上。方法内的最终局部变量就是常量。