第6章 扩展类与继承
面向对象编程的一个重要特性: 允许基于已定义的类创建新的类;
6.1 使用已有的类
派生 derivation, 派生类 derived class, 直接子类 direct subclass; 基类 base class, 超类 super class;
1
2
3
4
5
6
|
class Dog {
// Members of the Dog class...
}
class Spaniel extends Dog {
// Members of the Spaniel class...
}
|
6.2 类继承
派生类中包含基类成员, 并且能从派生类中访问这些成员, 称为类继承 calss inheritance;
基类的继承成员 inherited member, 是指派生类内部可访问的成员; 如果基类成员在派生类中不可访问, 那么它不是派生类的继承成员, 但是那些没有被继承的基类成员仍然是派生类对象的一部分; 派生类对象包含基类的所有继承成员-域和方法;
6.2.1 继承数据成员
>和基类在同一包中的子类继承了除private数据成员之外的所有内容; 包外的派生类不会继承private成员和没有访问属性的数据成员;
Note 必须将基类声明为public, 否则包外无法派生类;
protected可以包内访问, 可以包外继承, 没有访问修饰符的类成员称为包私有 package-private;
继承规则适用于static的类成员和非static成员; static变量只有一份, 由类的所有对象共享, 每个对象则有属于自己的实例变量集合;
被隐藏的数据成员
在派生类中定义了与基类中某个数据成员同名的数据成员;(不推荐); 这种情况下, 基类的数据成员仍然被继承, 但是会被同名的派生类成员隐藏;
Note 不论类型和访问属性是否一样, 名称一样的情况下, 基类成员在派生类中被隐藏;
如果要指向基类成员, 必须使用关键字super限定, 表明超类成员; (通常在使用Java库类或第三方设计或维护的包时会需要使用super区分同名基类成员);
6.2.2 继承方法
基类的普通方法(非构造)被继承到派生类的方式和数据成员一样;
Note 不论构造函数的属性, 都不会被继承;
1) 派生类对象
基类成员都在派生类对象中, 但是有些被限定无法访问;
Note 如果没有从派生类构造函数中调用基类构造函数, 编译器会自动操作;
2) 派生类
1
2
3
4
5
6
7
8
9
|
public class Animal {
public Animal(String aType) {
type = new String(aType);
}
public String toString() {
return "This is a " + type;
}
private String type;
}
|
1
2
3
4
5
|
public class Dog extends Animal {
// constructors for a Dog object
protected String name; // Name of a Dog
protected String breed; // Dog breed
}
|
>子类使用extends关键字标识超类;
3) 派生类的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class Dog extends Animal {
public Dog(String aName) {
super ( "Dog" ); // Call the base constructor
name = aName; // Supplied name
breed = "Unknown" ; // Default breed value
}
public Dog(String aName, String aBreed) {
super ( "Dog" ); // Call the base constructor
name = aName; // Supplied name
breed = aBreed; // Supplied breed
}
protected String name; // Name of a Dog
protected String breed; // Dog breed
}
|
>在子类中使用super("Dog"); 调用超类构造函数; 使用super而不是基类构造函数的名称Animal;
4) 调用基类构造函数
Note 基类构造函数调用必须是派生类构造函数方法中的第一条语句;
如果派生类构造函数第一条语句不是基类构造函数调用, 编译器会自动插入默认无参数基类构造函数: super();
Note 如果基类中自定义了构造函数, 编译器就不会创建默认构造函数, 这时需要自己添加默认构造函数(无参数);
6.2.3 覆盖基类方法
在派生类中定义一个与基类方法的签名相同(方法名和参数列表中的参数数目和类型都相同)的方法.
方法的访问属性可以与基类中的一样或者限制更少; e.g. public -> public, 不能为 protected或private或无设定;
派生类方法会覆盖override基类方法, 调用基类方法可以使用 super.Function();
6.3 @Override 标记
在派生类中定义方法覆盖超类方法时, 容易因为方法签名的一致性出错;
如果派生类的方法签名和超类的不同, 那么会对基类方法进行重载overload而不是覆盖override; [C++中会变成重写/隐藏 overwrite]
@Override标记会试图防止发生这类错误; 编译器会检查方法的签名和超类中同名方法的签名是否一致; 覆盖继承方法时推荐使用@Override;
6.4 选择基类访问属性
-组成类外部接口的方法声明为public; 派生类可以继承公共基类方法;
-希望将类给别人使用, 保持成员为private, 可提供pulic方法访问和操作成员; 增加类的安全性, 防止派生类影响基类数据成员;
-将基类成员声明为protected, 允许包中的其他类访问, 防止其他包的类直接访问; protected的基类成员可以被继承, 因此在其他包内, 只有派生类可以操作他们;
-省略的访问属性在同一包中可访问, 不同包中不论是否派生类都不可访问; 在其他包中, 和private的效果一样;
6.5 多态
多态polymorphism机制, 对某个给定类型的变量可以引用不同类型的对象, 调用方法时根据对象的类型来实现不同的行为;
指向派生类对象的引用必须存储到直接或者间接基类类型的变量中来实现多态;
多态指方法调用由对象的实际类型来决定, 而不是由存储引用的变量类型来决定;
所调用的方法必须被声明为基类的成员函数, 派生类也需要包含这个方法, 拥有相同的签名, 限制访问修饰符不会有更小的权限;
派生类的方法和基类的方法的名称相同, 签名相同, 返回类型必须是协变的covaraint(基类返回类型的派生类);
多态条件:
-派生类对象的方法必须通过基类的变量来调用;
-调用的方法必须在派生类中重写;
-调用的方法必须为基类成员;
-方法的返回类型必须是一样或者协变的;
-派生类中方法的修饰符不能比基类的更多限制;
因为变量可引用任意派生类对象, 所以引用的对象类型直到执行时才知道, 编译时无法确定; 因此在运行才能时动态决定要执行的方法;
Note 多态只应用于实例方法, 不能用于数据成员, 当访问类对象的数据成员时, 只能根据变量类型获取相对应的数据成员; [C++也是]
使用多态
1
2
3
4
|
Animal theAnimal = null ; // Declare a variable of type Animal
theAnimal = new Dog( "Rover" );
//OR
Animal theAnimal = new Dog( "Rover" );
|
>使用基类类型的变量可以存储从基类直接或间接派生的类的对象;
1
2
3
4
5
|
Animal[] theAnimals = {
new Dog( "Rover" , "Poodle" ),
new Cat( "Max" , "Abyssinian" ),
new Duck( "Daffy" , "Aylesbury" )
};
|
>数组形式
不论是否定义, 每个类中都存在toString()方法(默认), 在println()方法中会隐式地调用toString(), 编译器会插入一个对方法的调用, 会实行多态机制;
6.6 多级继承
1
2
3
4
5
|
class Spaniel extends Dog {
public Spaniel(String aName) {
super (aName, "Spaniel" );
}
}
|
>Dog类是直接超类, Animal是Spaniel的间接超类;
6.7 抽象类
抽象类abstract class, 声明了一个或多个方法, 但是没有定义, 省略了方法体, 称为抽象方法;
public abstract和abstract public的效果是一样的, 都要放在返回类型之前;
1
2
3
4
5
6
7
8
9
10
|
public abstract class Animal {
public abstract void sound(); // Abstract method
public Animal(String aType) {
type = new String(aType);
}
public String toString() {
return "This is a " + type;
}
private String type;
}
|
Note abstract方法不能是private, private方法不能被继承和重写;
抽象类对象不能实例化(new), 但是可以声明抽象类类型的变量; Animal thePet = null;
Note 抽象类派生子类时, 不一定要在子类中实例化所有抽象方法, 这样子类也是抽象的(不能实例化), 而且在定义时必须使用abstract关键字;
6.8 通用超类
Note 在Java中定义的所有类默认都是Object的子类;
Object类的变量能存储指向任意类类型对象的索引;
Object类的public成员:
toString() 返回一个描述当前对象的String 对象; 在该方法的继承版本中, 也就是类的名称再加上一个'@'以及当前对象的十六进制表示; 当使用+将对象与String 变量连接时, 该方法会被自动调用; 在自己的类中覆盖该方法从而为自己的类对象创建自己的字符串;
equals() 对作为参数传入的对象的引用与指向当前对象的引用进行比较, 如果它们相等返回true. 如果当前对象和参数是同一个对象(而不仅仅是相等—它们必须是同一个对象)就返回true. 如果二者是不同的对象就返回false, 即使这两个对象对于它们的数据成员有完全一样的值也是如此;
getClass() 返回一个标识当前对象所属类的Class 类型的对象;
hashCode() 为对象计算哈希代码值并以int 类型返回; java.util 包中定义的类用哈希代码值在哈希表中存储对象;
notify() 用于唤醒一个与当前对象关联的线程;
notifyAll() 用于唤醒与当前对象关联的所有线程;
wait() 导致一个线程释放当前对象的锁, 直到其他线程调用当前对象的notify()或notifyALL()方法唤醒为前线程;
Note 在自定义类中, getClass() notify() notifyAll() wait()都无法被覆盖, 他们在Object类中被final关键字固定;
Object类的protected成员:
clone() 不论当前对象的类型为何, 均创建当前对象的一个副本对象, 可以是任意类型, 因为Object 变量可以指向任意类的对象. 注意clone()方法并不是对所有类对象都有用, 而且正如在本节后面所介绍的, done()方法并不总是能精确进行所期望的操作;
finalize() 当对象被销毁时, 这个方法可以用来进行清理工作(无需覆盖这个方法)
6.8.1 toString()
必须是public的(在Object中的定义为public)
如果没有自定义toString(), 输出将是Class@HashCode: Spaniel@b75778b2, 类名@对象的哈希码, 通过Object的hashCode()方法生成;
6.8.2 判定对象的类型
getClass()返回一个Class类型的对象, 表示该对象所属的类;
1
2
|
Class objectType = pet.getClass(); // Get the class type
System.out.println(objectType.getName()); // Output the class name
|
>直接println Class的对象可以输出class ClassName, toString()已经默认定义; getClass()是多态的()
>如果类在默认包中而且没有包名, 输出完全限定名称, 否则会将包名作为前缀; Package.ClassName;
程序中的数组类型和基本类型都对应一个Class对象; 程序加载时, JVM会生成这些内容. Class主要由JVM使用, 因此没有public构造函数, 不能自行创建Class对象;
getClass()可以获得特定类或特定接口类型对应的Class对象; 每个类型只有一个Class对象;
Note 直接的法官法是在类, 接口或基本类型的名称后面附加.class, 以获得对应的Class对象的引用; e.g. java.lang.String.class
1
2
3
|
if (pet.getClass() == Duck. class ) {
System.out.println( "By George – it is a duck!" );
}
|
>精确测试, 如果pet是Duck的子类, 比较返回false; [当pet引用Duck子类时会编译错误???]
6.8.3 复制对象
clone()创建对象作为当前对象的副本, 需要实现Cloneable接口;
clone()创建的对象与原始对象有相同类型, 对应的域有相同的值;
如果原始对象有引用类型的域, 那么这个域所对应的对象不会被复制-仅仅复制了原始对象的引用, 然后赋值给新对象对应的域; 通常情况下, 这不是推荐的做法, 这样原始对象和新对象对应的域引用了同一个对象;
使用clone()方法复制对象可能会变得更复杂, 简单的办法是在类中实现复制构造函数, 对作为参数传入的对象进行复制;
1
2
3
4
5
6
|
// Copy constructor
public Dog(Dog dog) {
super (dog); // Call base copy constructor
name = dog.name;
breed = dog.breed;
}
|
基类也需要实现对应的复制构造函数;
1
2
3
|
public Animal(Animal animal) {
type = animal.type;
}
|
一般来说, 可以将参数类型的任意子类类型对象传递给方法;
Note 确保在复制构造函数操作中将所有可变的数据成员都复制[new]; 如果只是复制对数据成员的引用, 那么对原始对象的修改会影响到复制对象; [C++深拷贝浅拷贝]
String对象是不可变的(final, StringBuffer), 所以不需要创建副本;
6.9 接受可变数目参数的方法
Varargs方法, 在调用时接收数目可变的参数, 参数不需要是相同类型的;
Object ... args, 在这个参数之前, 方法可以有任意多个参数(包括0个), 但这个必须是最后一个参数; 省略号允许编译器认为参数列表是可变的;
args代表Object[]类型的数组, 参数值是数组中Object类型可用的元素, 方法体中, args数组的长度指明可变参数的数目;
1
2
3
4
5
6
|
public static void printAll(Object ... args) {
for (Object arg : args) {
System.out.print( " " + arg);
}
System.out.println();
}
|
>参数可以是任何类型任何内容, 基本类型的值会自动装箱, 因为方法期望引用参数;
限制可变参数列表中的类型
可以将可变参数设置为任意类类型或接口类型; 参数可以是指定类型或指定类的派生类; 设置为Objec的灵活性最大, 限制最少;
1
2
3
4
5
6
7
8
9
10
|
public static double average(Double ... args) {
if (args.length == 0 ) {
return 0.0 ;
}
double ave = 0.0 ;
for ( double value : args) {
ave += value;
}
return ave/args.length;
}
|
>这时参数必须是Double或Double的派生类或double类型(编译器自动装箱转换)
6.10 转换对象
当对象类型和目标类类型都在同一个类层次结构中, 其中一个是另一个的超类时, 才能将对象转换类型;
向上转换成直接或间接超类 [C++需要指针或引用, 对象转换会发生slice或未定义的错误]
1
2
|
Spaniel aPet = new Spaniel( "Fang" );
Animal theAnimal = (Animal)aPet; // Cast the Spaniel to Animal
|
将对象引用赋给超类类型时,不需要显式转换;
1
|
Animal theAnimal = aPet; // Cast the Spaniel to Animal
|
对象引用转换成超类类型时, Java会保留对象实际类的信息, 实现多态;
1
|
Dog aDog = (Dog)theAnimal; // Cast from Animal to Dog
|
>基类到子类, 需要显式转换, 注意类型, 防止调用被转换类没有的成员;
无法在不相关的对象之间进行转换, 但是可以编写构造函数来实现转换, 只有在有意义的情况下才会这么做;
需要在被转换的类中编写一个接受其他类的对象参数的构造函数;
1
2
3
4
5
6
|
public Duck(Spaniel aSpaniel) {
// Back legs off, and staple on a beak of your choice...
super ( "Duck" ); // Call the base constructor
name = aSpaniel.getName();
breed = "Barking Coot" ; // Set the duck breed for a converted Spaniel
}
|
>与转换不同(将同样对象表示为不同类型), 这是创建了一个全新的独立的对象, 使用了其他对象的数据;
6.10.1 转换对象的时机
向上转换: 1) 多态, 基类对象调用派生类的覆盖方法; 2) 基类类型参数, 可以将任何子类对象作为参数;
向下转换: 执行某个特定类独有的方法; e.g. Animal类型的变量引用了Duck对象, 如果需要调用Duck的layEgg()方法, 需要向下转换(显式的), 因为Animal没有layEgg()接口;
1
2
3
4
|
Duck aDuck = new Duck( "Donald" , "Eider" );
Animal aPet = aDuck; // Cast the Duck to Animal
aPet.layEgg(); // This won't compile!
((Duck)aPet).layEgg(); // This works fine
|
>[C++使用dynamic_cast, 做类型检查]
Note 尽量避免显式转换对象, 这样增加了不可用转换的可能性, 导致程序可靠性下降;
6.10.2 识别对象
getClass()获得对应Class对象, 进行比较;
使用instanceof运算符也可以比较类型;
1
2
3
4
|
if (pet instanceof Duck) {
Duck aDuck = (Duck)pet; // It is a duck so the cast is OK
aDuck.layEgg(); // and You can have an egg for tea
}
|
>如果pet没有指向Duck对象, 转换会抛出异常, 导致程序失败;
instanceof和getClass()的区别:
instanceof运算符检查对象转换中, 左操作数引用的对象转换到右操作数设定的类型是否合法; 如果对象与右操作数类型相同或是派生类类型, 结果为true;
getClass()必须类型严格匹配, 派生类型因为类型不同也会返回false;
6.11 枚举进阶
枚举是特殊形式的类; java.lang.Enum, 每个枚举常量对应的对象将常量的名称存储在一个域中, 枚举类型从Enum中继承toString()方法, 返回名称;
枚举类型常量对象存储一个整数域, 枚举中每个常量都赋予了一个唯一的整数值, 整数值会按照设定的顺序赋给枚举常量, 从零开始, 每个递增1个int; 通过ordinal()方法可以获得整数值;
1
2
3
|
Season now = Season.winter;
if (now.equals(Season.winter))
System.out.println( "It is definitely winter!" );
|
>通过equal()方法比较枚举类型的整数值是否相等;
equal()方法从Enum类继承, 并且继承了基于枚举类型实例的整数值进行比较的compareTo()方法; 如果枚举对象的整数值小于参数, 返回负整数, 相等返回0, 否则返回正整数;
1
2
|
if (now.compareTo(Season.summer) > 0 )
System.out.println( "It is definitely getting colder!" );
|
>compare()方法比较大小;
values()方法是枚举类型的static方法, 返回一个包括基于集合的for循环中使用的所有枚举常量的集合对象;
添加成员到枚举类
枚举是类, 可以添加自定义方法和域, 以及自定义构造函数;
1
|
public enum JacketSize { small, medium, large, extra_large, extra_extra_large }
|
自定义:
1
2
3
4
5
6
7
8
9
10
11
12
|
public enum JacketSize {
small( 36 ), medium( 40 ), large( 42 ), extra_large( 46 ), extra_extra_large( 48 );
// Constructor
JacketSize( int chestSize) {
this .chestSize = chestSize;
}
// Method to return the chest size for the current jacket size
public int chestSize() {
return chestSize;
}
private int chestSize; // Field to record chest size
}
|
>枚举常量列表
>在每个常量名称后面放一对空括号, 会调用默认构造;
即使自定义了构造函数, 从基类Enum中级车的常整数域仍然会被依次设置;
Note 决不能将枚举类中的构造函数声明为public; 唯一允许的修饰符是private; 枚举构造只能在类内部调用;
1
2
3
|
for (JacketSize size: JacketSize.values()) {
System.out.print( " " + size);
}
|
>基于集合的for循环;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Override
public String toString() {
switch ( this ) {
case small:
return "S" ;
case medium:
return "M" ;
case large:
return "L" ;
case extra_large:
return "XL" ;
default :
return "XXL" ;
}
}
|
>switch中使用this作为控制表达式, 引用当前的实例(一个枚举常量);