继承和多态(以媒体资料库设计为例)
绪论
面向对象程序设计语言有三大特性:封装、继承和多态性。继承是面向对象语言的重要特征之一,没有继承的语言只能被称作“使用对象的语言”。继承是非常简单而强大的设计思想,它提供了我们代码重用和程序组织的有力工具。
类是规则,用来制造对象的规则。我们不断地定义类,用定义的类制造一些对象。类定义了对象的属性和行为,就像图纸决定了房子要盖成什么样子。
一张图纸可以盖很多房子,它们都是相同的房子,但是坐落在不同的地方,会有不同的人住在里面。假如现在我们想盖一座新房子,和以前盖的房子很相似,但是稍微有点不同。任何一个建筑师都会拿以前盖的房子的图纸来,稍加修改,成为一张新图纸,然后盖这座新房子。所以一旦我们有了一张设计良好的图纸,我们就可以基于这张图纸设计出很多相似但不完全相同的房子的图纸来。
基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是public还是private。当然,在这之后,程序员还可以加入自己的新的成员,包括变量和方法。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。继承是支持代码重用的重要手段之一。
类这个词有分类的意思,具有相似特性的东西可以归为一类。比如所有的鸟都有一些共同的特性:有翅膀、下蛋等等。鸟的一个子类,比如鸡,具有鸟的所有的特性,同时又有它自己的特性,比如飞不太高等等;而另外一种鸟类,比如鸵鸟,同样也具有鸟类的全部特性,但是又有它自己的明显不同于鸡的特性。
如果我们用程序设计的语言来描述这个鸡和鸵鸟的关系问题,首先有一个类叫做“鸟”,它具有一些成员变量和方法,从而阐述了鸟所应该具有的特征和行为。然后一个“鸡”类可以从这个“鸟”类派生出来,它同样也具有“鸟”类所有的成员变量和方法,然后再加上自己特有的成员变量和方法。无论是从“鸟”那里继承来的变量和方法,还是它自己加上的,都是它的变量和方法。
1.继承
1.1 媒体资料库的设计
package dome;
import java.util.ArrayList;
public class Datebase {
private ArrayList<CD> listCD = new ArrayList<CD>();
private ArrayList<DVD> listDVD = new ArrayList<DVD>();
public void add(CD cd) {
listCD.add(cd);
}
public void add(DVD dvd) {
listDVD.add(dvd);
}
public void list() {
for (CD cd:listCD) {
cd.print();
}
for (DVD dvd:listDVD) {
dvd.print();
}
}
public static void main(String[] args) {
Datebase db=new Datebase();
db.add(new CD("abc", "abc", 4, 60, "..."));
db.add(new CD("def", "def", 4, 60, "..."));
db.add(new DVD("xxx","aaa",60,"..."));
db.list();
}
}
package dome;
public class CD {
private String title;
private String artist;
private int numofTracks;
private int playingTime;
private boolean gotIt=false;
private String comment;
public CD(String title, String artist, int numofTracks, int playingTime, String comment) {
// super();
this.title = title;
this.artist = artist;
this.numofTracks = numofTracks;
this.playingTime = playingTime;
this.comment = comment;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void print() {
System.out.println("CD:"+title+":"+artist);
}
}
package dome;
public class DVD {
private String title;
private String director;
private int playingTime;
private boolean gotIt=false;
private String comment;
public DVD(String title, String director, int playingTime, String comment) {
super();
this.title = title;
this.director = director;
this.playingTime = playingTime;
this.comment = comment;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void print() {
System.out.println("DVD:"+title+":"+director);
}
}
CD和DVD的代码相似,属于代码重复使用,形式上冗余,操作上效率低。
1.2 继承
1.2.1 定义
我们把用来做基础派生其它类的那个类叫做父类、超类或者基类,而派生出来的新类叫做子类。Java用关键字extends表示这种继承/派生关系:
class ThisClass extends SuperClass {
//…
}
继承表达了一种is-a关系,就是说,子类的对象可以被看作是父类的对象。比如鸡是从鸟派生出来的,因此任何一只都可以被称作是一只鸟。但是反过来不行,有些鸟是鸡,但并不是所有的鸟都是鸡。如果你设计的继承关系,导致当你试图把一个子类的对象看作是父类的对象时显然很不合逻辑,比如你让鸡类从水果类得到继承,然后你试图说:这只本鸡是一种水果,所以这本鸡煲就像水果色拉。这显然不合逻辑,如果出现这样的问题,那就说明你的类的关系的设计是不正确的。Java的继承只允许单继承,即一个类只能有一个父类。
1.2.2 代码示例
package dome;
import java.util.ArrayList;
public class Datebase {
// private ArrayList<CD> listCD = new ArrayList<CD>();
// private ArrayList<DVD> listDVD = new ArrayList<DVD>();
private ArrayList<Item> listItem=new ArrayList<Item>();
// public void add(CD cd) {
// listCD.add(cd);
// }
// public void add(DVD dvd) {
// listDVD.add(dvd);
// }
public void add(Item item) {
listItem.add(item);
}
// public void list() {
// for (CD cd:listCD) {
// cd.print();
// }
// for (DVD dvd:listDVD) {
// dvd.print();
// }
// }
public void list() {
for(Item item:listItem) {
item.print();
}
}
public static void main(String[] args) {
Datebase db=new Datebase();
db.add(new CD("abc", "abc", 4, 60, "..."));
db.add(new CD("def", "def", 4, 60, "..."));
db.add(new DVD("xxx","aaa",60,"..."));
db.list();
}
}
package dome;
public class CD extends Item{
private String title;
private String artist;
private int numofTracks;
private int playingTime;
private boolean gotIt=false;
private String comment;
public CD(String title, String artist, int numofTracks, int playingTime, String comment) {
// super();
this.title = title;
this.artist = artist;
this.numofTracks = numofTracks;
this.playingTime = playingTime;
this.comment = comment;
}
public static void main(String[] args) {
CD cd=new CD("a","b",2,2,"...");
cd.print();//若无重载,将实现父类Item中的print()方法
}
// public void print() {
// System.out.println("CD:"+title+":"+artist);
//
// }
}
package dome;
public class DVD extends Item{
private String title;
private String director;
private int playingTime;
private boolean gotIt=false;
private String comment;
public DVD(String title, String director, int playingTime, String comment) {
super();
this.title = title;
this.director = director;
this.playingTime = playingTime;
this.comment = comment;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void print() {
System.out.println("DVD:"+title+":"+director);
}
}
package dome;
public class Item {
public void print() {
System.out.println("item");
}
}
子类能继承得到父类的所有成员。包括private修饰的私有成员。
但是,拥有不等于可以任意使用,子类不能访问继承得到的私有成员。
子类也不能继承父类的构造函数。因为构造函数的函数名必须和所在类的类名一致。
2.子类父类关系
对理解继承来说,最重要的事情是,知道哪些东西被继承了,或者说,子类从父类那里得到了什么。答案是:所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法。构造方法是父类所独有的,因为它们的名字就是类的名字,所以父类的构造方法在子类中不存在。除此之外,子类继承得到了父类所有的成员。
但是得到不等于可以随便使用。每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。下表列出了不同访问属性的父类成员在子类中的访问属性:
2.1 子类父类的关系
2.1.1 表格
父类成员访问属性 | 在父类中的含义 | 在子类中的含义 |
---|---|---|
public | 对所有人开放 | 对所有人开放 |
protected | 只有包内其他类、自己和子类可以访问 | 只有包内其他类、自己和子类可以访问 |
缺省 | 只有包内其他类可以访问 | 如果子类与父类在同一个包内:只有包内其他类可以访问;否则相当于private,不能访问 |
private | 只有自己可以访问 | 不能访问 |
2.1.2 阐释
public的成员直接成为子类的public的成员,protected的成员也直接成为子类的protected的成员。Java的protected的意思是包内和子类可访问,所以它比缺省的访问属性要宽一些。而对于父类的缺省的未定义访问属性的成员来说,他们是在父类所在的包内可见,如果子类不属于父类的包,那么在子类里面,这些缺省属性的成员和private的成员是一样的:不可见。父类的private的成员在子类里仍然是存在的,只是子类中不能直接访问。我们不可以在子类中重新定义继承得到的成员的访问属性。如果我们试图重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量,在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个。尽管它们同名但是互不影响。
在构造一个子类的对象时,父类的构造方法也是会被调用的,而且父类的构造方法在子类的构造方法之前被调用。在程序运行过程中,子类对象的一部分空间存放的是父类对象。因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化。在这个过程中,如果父类的构造方法需要参数,如何传递参数就很重要了。
2.2 子类继承了什么
package dome;
public class CD extends Item{
private String artist;
private int numofTracks;
private int playingTime;
private boolean gotIt=false;
private String comment;
public CD(String title, String artist, int numofTracks, int playingTime, String comment) {
super(title);//通过super()调用Item的构造器将title传递过来
// this.title = title;
this.artist = artist;
this.numofTracks = numofTracks;
this.playingTime = playingTime;
this.comment = comment;
}
public static void main(String[] args) {
CD cd=new CD("a","b",2,2,"...");//先做了父类的构造(初始化),再做自己的初始化
//再进自己的构造函数进行自己的赋值
cd.print();//若无重载,将实现父类Item中的print()方法
}
// public void print() {
// System.out.println("CD:"+title+":"+artist);
//
// }
}
package dome;
public class DVD extends Item{
private String title;
private String director;
private int playingTime;
private boolean gotIt=false;
private String comment;
public DVD(String title, String director, int playingTime, String comment) {
// super();//将调用父类一个没有参数的构造器
//在子类当中,如果去构造一个子类的对象,在该过程当中其会自动地先去调用父类的构造器
//若没有通过super传递参数给父类的构造器(没有super或只有super()),会自动地去寻找父类那个不带参数的构造器
//若给参数,则会自动寻找参数列表匹配的父类构造器
//所以当构造一个子类的对象的时候,首先确保父类所拥有那些成员变量拥有恰当的初始化
//(定义初始化和构造器,先定义初始化后构造器)之后才轮到子类的定义初始化和构造器
//不管是否主动地用super()去传递参数,去指定调用哪个父类构造器
this.title = title;
this.director = director;
this.playingTime = playingTime;
this.comment = comment;
}
public static void main(String[] args) {
DVD dvd=new DVD("a","b",1,"...");
dvd.print();
}
public void print() {
System.out.println("DVD:"+title+":"+director);
}
}
package dome;
public class Item {
private String title;
public Item() {
}
public Item(String title) {
super();
this.title = title;
}
public void print() {
System.out.println("item");
}
}
Datebase暂时不变
2.3 关于super()
2.3.1 能在一个构造函数里调用两次super()吗?
不可以。必须在第一行。
第二次时会出现 “error: call to super must be first statement in constructor“。
2.3.2 super()必须在构造函数的第一行吗?
是的,必须先初始化父类的成员变量。
2.4 子类和父类的关系2(继续修改代码)
package dome;
public class CD extends Item{
private String artist;
private int numofTracks;
private int playingTime;
private boolean gotIt=false;
private String comment;
public CD(String title, String artist, int numofTracks, int playingTime, String comment) {
super(title,playingTime,false,comment);//通过super()调用Item的构造器将title传递过来
// this.title = title;
this.artist = artist;
this.numofTracks = numofTracks;
// this.playingTime = playingTime;
// this.comment = comment;
}
public static void main(String[] args) {
CD cd=new CD("a","b",2,2,"...");//先做了父类的构造(初始化),再做自己的初始化
//再进自己的构造函数进行自己的赋值
cd.print();//若无重载,将实现父类Item中的print()方法
}
// public void print() {
// System.out.println("CD:"+title+":"+artist);
//
// }
package dome;
public class DVD extends Item{
// private String title;//如果子类中有父类名字完全相同的变量,
//那么父类的那个变量将被隐藏起来
//当在父类那边做操作时用的则是父类自己的那个变量
private String director;
// private int playingTime;
// private boolean gotIt=false;
// private String comment;
public DVD(String title, String director, int playingTime, String comment) {
// super();
// this.title = title;
super(title,playingTime,false,comment);
setTitle("b");
//父类的变量和方法都被子类继承,但父类中私有的,子类不可以碰
//但可以通过父类的函数去碰,在谁的函数里面指的成员变量就是谁的
//如果子类和父类当中出现同名的成员变量,在子类的函数中就是子类自己的,同理的,父类....,没有任何关系
this.director = director;
}
public static void main(String[] args) {
DVD dvd=new DVD("a","b",1,"...");
dvd.print();
}
public void print() {
System.out.println("DVD:");
super.print();//调用父类的print()
System.out.println(director);
}
}
}
package dome;
public class Item {
private String title;
private int playingTime;
private boolean gotIt=false;
private String comment;
public Item() {
}
public Item(String title, int playingTime, boolean gotIt, String comment) {
super();
this.title = title;
this.playingTime = playingTime;
this.gotIt = gotIt;
this.comment = comment;
}
public void setTitle(String title) {
this.title=title;
}
public void print() {
System.out.println(title);
}
}
datebase暂时未修改
2.5 子类的私有成员函数在子类中可以使用吗?
父类的私有的成员函数在子类中不能直接使用,在定义子类时需要通过super()函数间接访问,在main()函数中需要通过所继承的父类函数进行访问。
3.多态变量和向上造型
3.1 多态变量
3.1.1 定义
类定义了类型,DVD类所创建的对象的类型就是DVD。类可以有子类,所以由那些类定义的类型可以有子类型。在DoME的例子中,DVD类型就是Item类型的子类型。
子类型类似于类的层次,类型也构成了类型层次。子类所定义的类型是其超类的类型的子类型。
当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配,如:
Car myCar = new Car();
是一个有效的赋值,因为Car类型的对象被赋值给声明为保存Car类型对象的变量。但是由于引入 了继承,这里的类型规则就得叙述得更完整些:
一个变量可以保存其所声明的类型或该类型的任何子类型。
对象变量可以保存其声明的类型的对象,或该类型的任何子类型的对象。
Java中保存对象类型的变量是多态变量。“多态”这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象。
3.1.2 子类和子类型
3.1.2.1 定义
类定义了类型,子类定义了子类型。
子类的对象可以被当作父类的对象来使用。
1.赋值给父类的变量
2.传递给需要父类对象的函数
3.放进存放父类对象的容器里
3.1.2.1 子类型与赋值
子类的对象可以赋值给父类的变量
Vehicle v1=new Vehicle();
Vehicle v2=new Car();
Vehicle v3=new Bicycle();
3.1.2.2 子类和参数传递
子类的对象可以传递给需要父类对象的函数
public class Database{
public void add (Item theItem){
。。。
}
}
...
DVD dvd =new DVD(...);
CD cd=new CD(...);
database.add(dvd);
database.add(cd);
3.1.2.3 子类型和容器
3.1.3 多态变量
- JAVA的对象变量是多态的,它们能保存不止一种类型的对象
- 它们可以保存的是声明类型的对象,或者声明类型的子类的对象(e-g:声明类型Item——静态类型,运行中存储的类型DVD,CD——动态类型)
- 当把子类的对象赋给父类的变量的时候,就发生了向上造型
3.2 向上造型
3.2.1 造型cast
- 子类的对象可以赋值给父类的变量
-
注意:JAVA中不存在对象对对象的赋值!!!(而是让这两个对象的管理者去管理一个共同的对象)【oop语言除c++】
String s="Hello";
s="bye";//s先指向Hello,后指向bye,而不是用bye去替换Hello
- 父类的对象不能赋值给子类的变量
Vechicle v;
Car c=new Car();
v=c;//OK
c=v;//编译错误
- 可以用造型:
c=(Car) v;
//只有当v这个变量实际管理的是Car才行,也就是说v当时被向上造型过
-
用括号围起类型放在值得前面。对象本身没有发生任何变化,所以不是”类型转化“。运行时有机制来检查这样的转化是否合理()