继承和多态
1. 继承
继承使得你可以定义一个通用的类(即父类),之后扩充该类为一个更加特定 的类(即子类)。
Java 术语中,如果类 C1 扩展自另一个类 C2, 那么就将 C1 称为次类(subclass ), C2 称为超类(superclass )。超类也称为父类 ( parent class) 或基类 ( base class), 次类又称为子类 (child class)、扩展类 (extended class) 或派生类(derived class )。子类从它的父类中继 承可访问的数据域和方法,还可以添加新数据域和新方法。
继承应该注意的几个关键点:
- 和传统的理解不同,子类并不是父类的一个子集。实际上,一个子类通常比它的父类包含更多的信息和方法。
- 父类中的私有数据域在该类之外是不可访问的。因此,不能在子类中直接使用。 如果父类中定义了公共的访问器 / 修改器,那么可以通过这些公共的访问器 / 改器来访问和修改它们。
- 不是所有的 “是一种”(isa) 关系都该用继承来建模。例如:正方形是一种矩形, 是不应该定义一个 Square 类来扩展 Rectangle 类
- 继承是用来为 “是一种” 关系( isa) 建模的。不要仅仅为了重用方法这个原因而盲目地扩展一个类。例如:尽管 Person 类和 Tree 类可以共享类似高度和重量这样的通 用特性,但是从 Person 类扩展出 Tree 类是毫无意义的。
- 在 Java 中是不允许多重继承的。一个 Java 类只可能直接 继承自一个父类。这种限制称为单一继承(single inheritance)。
super关键字
构造方法用于构建一个类的实例。不同于属性和普通方法,父类的构造方法不会被子类继承。它们只能使用关键字 super 从子类的构造方法中调用。在子类中调用父类构造方法的名字会引起一个语法错误。调用父类构造方法的语法是:
super()或者super(parameters);
语句 super() 或super (arguments) 必须出现在子类构造方法的第一行,这是显式调用父类构造方法的唯一方式。
构造方法可以调用重载的构造方法或父类的构造方法。如果它们都没有被显式地调用, 编译器就会自动地将 super() 作为构造方法的第一条语句。
在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。 构造一个子类的对象时,子类构造方法会在完成自己的任务之前,首先调用它的父类的构 造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为为止。这就是构造方法链。
如果要设计一个可以被继承的类,最好提供一个无参构造方法以避免程序设计错 。
关键字 super 不仅可以引用父类的构造方法,也可以引用父类的方法。所用语法如下:
super.方法名(参数);
方法重写
子类从父类中继承方法。有时,子类需要修改父类中定义的方法的实现,这称作方法重 (method overriding)。要重写一个方法,需要在子类中使用和父类一样的签名以及一样的返回值类型 来对该方法进行定义。
需要注意:
- 仅当实例方法是可访问时,它才能被覆盖。因为私有方法在它的类本身以外是不能 访问的,所以它不能被覆盖。
- 与实例方法一样,静态方法也能被继承。但是,静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。 可以使用语法:父类名.静态方法名(SuperClassName.staticMethodName) 调用隐藏的静态方法。
方法重写与重载
重载意味着使用同样的名字但是不同的签名来定义多个方法。重写意味着在子 类中提供一个对方法的新的实现。
方法重写发生在通过继承而相关的不同类中;方法重载可以发生在同一个类中, 可以发生在由于继承而相关的不同类中。
为了避免错误,可以使用一个特殊的 Java 语法,称为重写标注 (override annotation), 在子类的方法前面放一个 @Override。该标注表示被标注的方法必须重写父类的一个方法。如果具有该标注的方法没有重写父类的方法,编译器将报告一个错误。
Object类及其toString方法
Java 中的所有类都继承自 java.lang.Object 。如果在定义一个类时没有指定继承性,那么这个类的父类就被默认为是 Object。
Object类中的 toString() 方法的签名是:
public String toString()
调用一个对象的 toStringO 会返回一个描述该对象的字符串。默认情况下,它返回一 个由该对象所属的类名、at 符号以及该对象十六进制形式的内存地址组成的字符串
这个信息不是很有用,或者说没有什么信息量。通常,应该重写这个 toString 方法,这样,它可以返回一个代表该对象的描述性字 符串。
2. 多态
一个类实际上定义了一种类型。子类定 义的类型称为子类型(subtype), 而父类定义的类型称为父类型(supertype)。
继承关系使一个子类继承父类的特征,并且附加一些新特征。子类是它的父类的特殊 ,每个子类的实例都是其父类的实例,但是反过来就不成立。例如:每个圆都是一个几何对象,但并非每个几何对象都是圆。因此,总可以将子类的实例传给需要父类型的参数。
使用父类对象的地方都可以使用子类的对象。这就是通常所说的多态 ( polymorphism)。简单来说,多态意味着父类型的变量可以引用子类型的对象。
动态绑定
Object o = new GeometricObject();
System.out.println(o.toString());
上面的o调用哪个对象的toString方法呢?
一个变量必须被声明为某种类型。变量的这个类型称为它的声明类 (declared type)。这里,o 的声明类型是 Object。变量的实际类型(actual type) 是被变量引用的对象的实际类。这里,o 的实际类型 GeometricObject, 因为 o 指向使用 new GeometricObject() 创建的对象。o 调用哪个 toString 方法由 o 的实际类型决定。这称为动态绑定(dynamic binding)。
匹配方法的签名和绑定方法的实现是两个不同的问题。引用变量的声明类型决定了编译 时匹配哪个方法。在编译时,编译器会根据参数类型、参数个数和参数顺序找到匹配的方法 。一个方法可能在沿着继承链的多个类中实现。Java 虚拟机在运行时动态绑定方法的实现,这是由变量的实际类型决定的。
对象转换和instanceof运算符
总是可以将一个子类的实例转换为一个父类的变量,称为向上转换(upcasting), 因为子类的实例永远是它的父类的实例。当把一个父类的实例转换为它的子类变量(称为向下转换 (downcasting)) ,必须使用转换记号 “(子类名)” 进行显式转换,向编译器表明你的意图。 为使转换成功,必须确保要转换的对象是子类的一个实例。如果父类对象不是子类的一个 实例,就会出现一个运行异常ClassCastException。
一个好的经验是,在尝试转换之前确保该 对象是另一个对象的实例。这是可以利用运算符 instanceof 来实现的。
为了能够进行通用程序设 ,一个好的经验是把变量定义为父类型,这样,它就可以接收任何子类型的值。
对基本类型值进行转换不同于对对象引用进行转换。转换基本类型值返回一个新的值。
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
Object类的equals方法
Object 类中定义的另外一个经常使用的方法是 equals 方法。它的签名是:
public boolean equals(Object o);
这个方法测试两个对象是否相等。调用它的语法是:
obj1.equals(obj2);
Object类的equals方法默认实现是:
public boolean equals(Object o) {
return (this == obj);
}
这个实现使用 == 运算符检测两个引用变量是否指向同一个对象。
一般而言,应该在自己的 客户类中重写这个方法,以测试两个不同的对象是否具有相同的内容。
比较运算符==用来比较两个基本数据类型的值是否相等,或者判断两个对象是否 具有相同的引用。如果想让 equals 方法能够判断两个对象是否具有相同的内容,可以在 定义这些对象的类时,重写equals方法。
在子类中,使用签名 equals(SomeClassName obj) 重写 equals 方法是一个常见错误,应该使用 equals(Object obj)。
ArrayList类
Java 提供 ArrayList 类来存储不限定个数的对象。ArrayList 是一种泛型类,具有一个泛型类型 E。创建一个 ArrayList ,可以指定一 个具体的类型来替换 E。其创建语法如下:
ArrayList<AConcreteType> list = new ArrayList<AConcreteType>();
也可以简化为:
ArrayList<AConcreteType> list = new ArrayList<>();
ArrayList主要方法如下:
ArrayList(); 创建一个空列表
add([index: int], o: E): void 添加一个元素
clear() 清除列表元素
contatins(o: Object): boolean 是否含有元素o
get(index: int): E 返回指定下标的元素
indexOf(o: Object): int 返回列表中第一个匹配元素的下标
isEmpty(): boolean 列表是否为空
lastIndexOf(o: Object): int 返回列表中匹配的最后一个元素
remove(o: Object): boolean 去除列表中的第一个出现的元素o,若被去除,则返回true
size(): int 返回列表中元素的个数
remove(index: int): boolean 去除指定下标位置的元素,若被去除,则返回true。
set(index: int, o: E): E 设置指定下标位置的元素
为存储在 ArrayList 中的元素必须是一种对象。不能使用诸如 int 的基本数据类型来代替一个泛型类型。然而,你可以创建一个存储 Integer 对象的 ArrayList, 如下所示:
ArrayList<Integer> list = new ArrayList<>();
可以在数组里使用 foreach 循环来遍历元素。数组列表中的元素也可以使用 foreach 环来进行遍历,语法如下:
for (elementType element: arrayList) {// process the element}
如果要对一个数组列表排序,使用 java.util.Collections.sort(arrayList)方法。
可以使用 java.util .Collections 类中的静态的 max 和min 方法来返回列表中的最大和 最小元素,可以使用 java.util .Collections 类中的静态的 shuffle 方法来随机打乱列表的元素。
从数组中创建一个数组列表,以及从一个数组列表来创建一个对象数组的例子:
String[] array = {"red","green","blue"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
String[] array1 = new String[list.size()];
list.toArray(array1);
protected数据和方法
经常需要允许子类访问定义在父类中的数据域或方法,但不允许非子类访问这些数据域 和方法。可以使用关键字 protected 完成该功能。父类中被保护的数据域或方法可以在它的子类中访问。
使用 protected 修饰符允许任何包中的子类或同一包中的类访问类的成员。
子类可以重写它的父类的 protected 方法,并把它的可见性改为 public。但是, 子类不能削弱父类中定义的方法的可访问性。例如:如果一个方法在父类中定义为 public, 在子类中也必须定义为 public。
修饰符 private 和protected 只能用于类的成员。public 修饰符和默认修饰符(也就是没有修饰符)既可以用于类的成员,也可以用于类。
防止扩展和重写
有时候,可能希望防止类扩展。在这种情况下,使用 final 修饰符表明一个类是最终 ,是不能作为父类的。Math 类就是一个最终类。String、StringBuilder 和StringBuffer 类也可以是最终类。最终类是不能被继承的。
也可以定义一个方法为最终的,最终方法不能被它的子类重写。
final 修饰符还可以用在方法中的局部变量上。方法内的最终局部变量就是常量。