面向对象程序设计语言有三大特性:封装、继承和多态性。 继承是面向对象语言的重要特征之一,没有继承的语言只能被称作“使用对象的语言”。继承是非常简单而强大的设计思想,它提供了我们代码重用和程序组织的有力工具。 基于已有的设计创造新的设计,就是面向对象程序设计中的继承。 在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是public还是private。
4.1:继承
我们想要实现一个资料库,存放CD资料
使用ArrayList存储,CD作为一个对象放入ArrayList,资料库Database实现对CD的操作 CD应该具有某些属性:title标题,artist艺术家,playingTime时长等等 我们先创建CD的类
CD
我们需要一些属性,并需要一个构造函数,用来构造CD对象,再定义一个输出函数
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) {
this.title = title;
this.artist = artist;
this.numOfTracks = numOfTracks;
this.playingTime = playingTime;
this.comment = comment;
}
public void print() {
System.out.println(title+":"+artist);
}
}
接下来我们需要构造Database,并添加一些对CD对象的操作,如添加,输出等
Database
public class Database {
//创建一个数组容器,并声明里面的对象应该是CD
private ArrayList<CD> listCD = new ArrayList<CD>();
//添加记录
public void add(CD cd){listCD.add(cd);}
//输出数据库记录
public void list(){
for(CD cd : listCD){
cd.print();
}
}
//主函数测试
public static void main(String[] args) {
//测试代码填入
}
}
我们测试一下主函数,添加两个CD并输出
Database db = new Database();
db.add(new CD("晴天","周杰伦",1,230,"第一首单曲专辑"));
db.add(new CD("七里香","周杰伦",1,30,"第二首单曲专辑"));
db.list();
"输出结果":
晴天:周杰伦
七里香:周杰伦
这时候,我们突然想扩充一下Database,想在里面放入DVD视频信息,这时候该怎么做? 同样的,创建DVD类,重新在Database里声明ArrayList
,添加与输出DVD的函数。
思考:这个媒体资料库有没有问题?
代码复用,耦合性高。
对比DVD与CD的代码,非常相似,Database出现了大量的代码复制,是代码质量不良的表现,维护麻烦(想要修改输出格式,那么所有对象的print都要改),可扩展性低(想要添加新的对象时,又需要重复上面的操作)
我们想要提取CD与DVD中公共的东西,然后Database管理这个公共的对象。
Item
新建一个名为Item的类
package week4.Database;
public class Item {
}
public class DVD/CD extends Item 在DVD与CD的主类的函数名后加
extends Item
,表明CD、DVD继承至Item
Database
此时,Database就可以修改为管理Item
public class Database {
//创建数组容器
private ArrayList<Item> listItem = new ArrayList<Item>();
//添加函数add
public void add(Item item){listItem.add(item);}
//输出数据库记录
public void list(){
for (Item item:listItem){
item.print();
//我们给item类添加一个print函数
}
}
测试一下主函数
Database db = new Database();
db.add(new CD("晴天","周杰伦",1,230,"第一首单曲专辑"));
db.add(new CD("七里香","周杰伦",1,30,"第二首单曲专辑"));
db.add(new DVD("这就是街舞","zz",140,"这就是街舞第三季"));
db.list();
"输出结果":
晴天:周杰伦
七里香:周杰伦
标题:这就是街舞 导演:zz 时长:140
item类的print函数无任何操作,可以看出这里优先使用对象类的print函数 这时候我们理一下四个类的关系
Database管理着Item,Item派生出两个子类,子类继承了父类的所有东西
总结
我们把用来做基础派生其它类的那个类叫做父类、超类或者基类,而派生出来的新类叫做子类。Java用关键字extends表示这种继承/派生关系:
class ThisClass extends SuperClass { //… }
继承表达了一种is-a
关系,就是说,子类的对象可以被看作是父类的对象。 比如鸡是从鸟派生出来的,因此任何一只都可以被称作是一只鸟。但是反过来不行,有些鸟是鸡,但并不是所有的鸟都是鸡。如果你设计的继承关系,导致当你试图把一个子类的对象看作是父类的对象时显然很不合逻辑,比如你让水果黄瓜类从水果类得到继承,然后你试图说:水果黄瓜是一种水果,所以水果基本都像水果黄瓜。这显然不合逻辑,如果出现这样的问题,那就说明你的类的关系的设计是不正确的。Java的继承只允许单继承,即一个类只能有一个父类。
4.2:子类与父类的关系
子类到底从父类继承了什么?
答案是:所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法 但是得到不等于可以随便使用。每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同
父类成员访问属性 | 在父类中的含义 | 在子类中的含义 |
---|---|---|
public | 对所有人开放 | 对所有人开放 |
protected | 只有包内其它类、自己和子类可以访问 | 只有包内其它类、自己和子类可以访问 |
缺省 | 只有包内其它类可以访问 | 如果子类与父类在同一个包内:只有包内其它类可以访问 否则:相当于private,不能访问 |
private | 只有自己可以访问 | 不能访问 |
当子类与父类拥有相同名称的成员变量,该如何访问?
如果我们试图在子类中,重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量
在Item与CD中,都添加protected String name;
子类CD添加protected String name = "zhiwei";
CD cd1 = new CD("are you ok?","leijun",1,60,"india mifans");
cd1.print();
cd1.addname("雷军");
System.out.println(cd1.name);//直接访问cd1的name
cd1.printname();//使用继承至父类的printname()方法
"输出结果":
zhiwei
雷军
CD继承至Item,子类对象cd1调用继承自父类的addname方法将字符串
"雷军"
添加进了继承至父类对象中的成员变量name中,这时候输出cd1的name,却是子类自己先定义的"zhiwei"
,由此可知只能通过父类方法访问到父类的name数据
- 在子类中,我们可以直接访问子类的成员变量,访问父类的私有成员变量则需要通过父类的方法去操作
完善媒体资料库(Super函数)
在构造一个子类的对象时,父类的构造方法也是会被调用的,而且父类的构造方法在子类的构造方法之前被调用。在程序运行过程中,子类对象的一部分空间存放的是父类对象。因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化。
super 是当前对象的父对象的引用,super关键字的用法:
- super.父类属性名:调用父类中的属性
- super.父类方法名:调用父类中的方法
- super():调用父类的无参构造方法
- super(参数):调用父类的有参构造方法
Item
public class Item {
private String title;
private int playingTime;
private String comment;
protected String name;
private boolean gotIt = false;
//成员变量
public Item(String title, int playingTime, String comment) {
this.title = title;
this.playingTime = playingTime;
this.comment = comment;
}//带参数构造函数
public Item(){}//不带参数构造函数
public void addname(String name){
this.name=name;
}
private void printname(){
System.out.println(name);
}
public void print() {
System.out.print(title);
System.out.print(" ");
System.out.println(comment);
}//父类输出函数
}
CD
public class CD extends Item{
private String artist;
private int numOfTracks;
public CD(String title, String artist, int numOfTracks, int playingTime,String comment) {
super(title,playingTime,comment);
//调用父类构造函数,并传递参数
this.artist = artist;
this.numOfTracks = numOfTracks;
}
public void print(){
System.out.print("CD:");
super.print();
//调用父类输出函数
}
}
DVD
public class DVD extends Item{
private String director;
public DVD(String title, String director, int playingTime, String comment) {
super(title,playingTime,comment);
//调用父类构造函数,并传递参数
this.director = director;
}
public void print(){
System.out.print("DVD:");
super.print();
//调用父类输出函数
}
}
4.3:多态变量与向上造型
我们制造了CD与DVD的对象,把制造的对象交给了只接收Item对象的媒体资料库,媒体资料库的函数都指明:我需要接收类型为Item的对象。那么为什么将CD与DVD交给资料库可行呢?
子类与子类型
我们定义了一个类,就是定义了一个新的类型 类定义了类型,子类定义了子类型
子类的对象可以当作父类的对象使用:
- 子类的对象可以赋值给父类的变量
vehicle v1 = new vehicle();
vehicle v2 = new car();
vehicle v3 = new bike();
- 子类的对象传递给父类对象的函数 ```java public class Database{ public void add(Item i){…} }
DVD d1 = new DVD(…)
Database.add(d1)
//可以直接把子类的对象当作父类对象传递过去
- 子类的对象放入存放父类对象的容器里
> Item容器里可以放CD与DVD对象
![](https://cdn.nlark.com/yuque/0/2021/jpeg/1440517/1628672975372-4b52728e-1797-4768-a0b4-000f181d62ae.jpeg)
<a name="Eb7Au"></a>
### 多态变量
> 变量`i`:**静态**类型(声明类型`Item i`),**动态**类型(运行时所管理的类型,例如子对象赋值给父对象`i`)
> 多态变量:具体某一时刻,变量`i`所管理对象的类型是会变化的
- Java的对象变量是多态的(多种形态都可存在)可以保存的是声明类型的对象,或声明类型的子类对象
<a name="AE9T1"></a>
### 向上造型
> 把子类的对象赋给父类的变量的时候,就发生了**向上造型**
> - 拿一个子类对象,当作父类对象来用
> - 向上造型是默许的,不需要运算符
> - 向上造型总是安全的
- 对象变量的赋值,并不是一个对象的数据全部给另一个对象。而是让两个对象管理者,管理同一个对象
- 子类对象可以赋值给父类变量,父类对象不可以赋值给子类变量(可以用造型)
造型:一个类型的**对象**赋给另外一个类型的**变量(会改变动态类型)**
- 使用括号,围起类型放在值的前面(`c = (car)v;`仅仅是将v当作Car类来看待,并不会改变v的原类型)
- 对象本身未发生任何变化(类型转换会改变值,造型不会)
- 运行时有机制会检查转化是否合理(ClassCastException)
```java
Vehicle v;
Car c = new Car();
v = c ;//可以
c = v ;//编译错误
c = (car)v;
(只有当v实际管理的类型是Car或者空才可以)
4.4:多态
多态就是同一个接口,使用不同的实例而执行不同操作 多态性是对象多种表现形式的体现
多态存在的三个必要条件
- 继承(extends)
- 覆盖
- 父类引用指向子类对象:Parent p = new Child();
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
覆盖
当子类与父类存在同名的方法会怎样?
答:如果子类的方法覆盖了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。覆盖的新版本具有与老版本相同的方法签名:相同的方法名称和参数表。因此,对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被“覆盖”起来而看不见了。而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。注意:我们这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。
覆盖override:
- 子类和父类中存在名称与参数表完全相同的函数,那么这对函数就构成覆盖关系
- 通过父类变量调用存在覆盖关系的函数时,会调用变量当时所管理的对象所属类的函数 ```java Item i = new Item(“标题1”,1,”测试1”); Item icd = new CD(“标题2”,”测速2”,1,1,”测速2”); icd.print(); i.print();
“结果”: CD:标题2 测速2 标题1 测试1
> 两个相同声明类型Item变量,管理不同类型的对象,所调用的`Print()`函数结果也是不相同的
<a name="Xv1RA"></a>
### 绑定
> 当调用一个方法时,究竟应该调用哪个方法,这件事情叫做**绑定**。绑定表明了调用一个方法的时候,我们使用的是哪个方法。
绑定有两种:
1. 早绑定,又称**静态绑定**,根据**变量的声明类型**来确定,这种绑定在编译的时候就确定了
1. 晚绑定,即**动态绑定**,根据**变量的动态类型**来确定,动态绑定在运行的时候,根据变量当时实际所指的对象的类型动态决定调用的方法。Java缺省使用动态绑定。
> 在成员函数中,调用其他成员函数,也是通过This这个对象变量调用的(动态绑定,且Java默认动态绑定)
> 使用多态方式调用方法时:首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
---
<a name="MTF1o"></a>
# 4.5:类型系统
> Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,**子类可以使用 Object 的所有方法**。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1440517/1629627147619-2ec28200-abda-4a8e-9f2f-8c87da84b694.png#clientId=u43761360-3be4-4&from=paste&height=355&id=u6ff045c3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=709&originWidth=1086&originalType=binary&ratio=1&size=124114&status=done&style=shadow&taskId=uf342f326-d6c0-4c18-96af-7a60cdf3f1a&width=543)<br />Object类型提供了几个常用的方法,使得所有的对象都可以使用其方法
- toString() 直接输出某对象时会自动调用的方法,输出一串字符
- equals() `a.equals(b)`,判断a与b是否相等
- getClass() `a.getClass()`,输出a所属的类
例:为CD覆盖toString方法,就可以直接输出想输出的CD内容
```java
@Override
public String toString() {
return "CD{" +
"artist='" + artist + '\'' +
", numOfTracks=" + numOfTracks +
", name='" + name + '\'' +
'}';
}
System.out.println(cd1);
覆盖方法名称与参数表必须与父类完全相同
所以覆盖(重写)equals时,由于equals函数仅仅接收object对象,想要比较两个对象的某一参数,需要向下造型
@Override
public boolean equals(Object obj) {
return artist.equals(((CD) obj).artist);
}