继承与多态
一、继承
面向对象程序设计语言(Object Oriented Programming)OOP 有三大特性:封装、继承和多态性。继承是 Java 中实现软件重用的重要手段,是 java 面向对象编程技术的一块基石。因为它允许创建分等级层次的类。继承是非常简单而强大的设计思想,它提供了我们代码重用和程序组织的有力工具。没有继承的语言只能被称作“使用对象的语言”。
类是规则,用来制造对象的规则。我们不断地定义类,用定义的类制造一些对象。类定义了对象的属性和行为,就像图纸决定了房子要盖成什么样子。一张图纸可以盖很多房子,它们都是相同的房子,但是坐落在不同的地方,会有不同的人住在里面。
假如现在我们想盖一座新房子,和以前盖的房子很相似,但是稍微有点不同。一般建筑师都会拿以前盖的房子的图纸来,稍加修改,成为一张新图纸,然后盖这座新房子。所以一旦我们有了一张设计良好的图纸(有共同的特征和动作放在一个类的通用类,让其它类共享),我们就可以基于这张图纸设计出很多相似但不完全相同的房子的图纸来(扩展为其他多个继承了通用类中的特征和动作的特定类)。父类更通用,子类更具体。
基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是 public还是 private(但不能访问)。当然,在这之后,程序员还可以加入自己的新的成员,包括变量和方法。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。
1.1 继承的使用规则
子类可以继承父类中所有可被子类访问的成员变量和成员方法,但必须遵循以下原则。
- 子类能够继承父类中被声明为public和protected的成员变量和成员方法,但不能继承被声明为private的成员变量和成员方法。
- 子类能够继承在同一个包中的由默认修饰符修饰的成员变量和成员方法。
- 如果子类声明了一个与父类的成员变量同名的成员变量,则子类不能继承父类的成员变量,此时称子类的成员变量隐藏了父类的成员变量。
- 如果子类声明了一个与父类成员方法同名的成员方法,则子类不能继承父类的成员方法,此时称子类的成员方法覆盖了父类的成员方法。
1.2 继承语法
我们把用来做基础派生其它类的那个类叫做父类、超类或者基类,而派生出来的新类叫做子类。Java用关键字extends表示这种继承/派生关系:
class 父类 { 父类属性;父类方法;
}
class 子类 extends 父类 {
子类扩展属性;
子类扩展方法;(可重写父类方法)
}
// 调用
子类 对象名称 = new 子类();
继承表达了一种 is-a 关系,就是说,子类的对象可以被看作是父类的对象。比如鸡是从鸟派生出来的,因此任何一只都可以被称作是一只鸟。但是反过来不行,有些鸟是鸡,但并不是所有的鸟都是鸡。
如果你设计的继承关系,导致当你试图把一个子类的对象看作是父类的对象时显然很不合逻辑。比如你让鸡类从水果类得到继承,然后你试图说:这只鸡是一种水果,所以这碗鸡煲就像水果色拉。这显然不合逻辑,如果出现这样的问题,那就说明你的类的关系的设计是不正确的。
Java的继承只允许单继承,即一个类只能有一个父类。
1. 关于构造方法
当我们去构造一个子类的对象的时候,首先要确保,它父类所拥有的那些成员变量,得到恰当的初始化。在程序运行过程中,子类对象的一部分空间存放的是父类对象。因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化。
这里的“恰当的初始化”,包含两件事情:
第一,定义初始化。
第二,构造器。
总是父类的成员变量得到恰当的初始化,再进行自己的。
在这个过程中,如果父类的构造方法需要参数,如何传递参数就很重要了。
如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。
如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用super父类的无参构造器。
2. 关于一般方法 print()
如果子类的方法覆盖了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。覆盖的新版本具有与老版本相同的方法签名:相同的方法名称和参数表。因此,对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。
覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被“覆盖”起来而看不见了。而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。注意我们这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。
当调用一个方法时,究竟应该调用哪个方法,这件事情叫做绑定。绑定表明了调用一个方法的时候,我们使用的是哪个方法。绑定有两种:一种是早绑定,又称静态绑定,这种绑定在编译的时候就确定了;另一种是晚绑定,即动态绑定。动态绑定在运行的时候根据变量当时实际所指的对象的类型动态决定调用的方法。
Java缺省使用动态绑定。所有成员函数的调用都应该被看作是动态绑定。
1.3 子类与父类的关系
对理解继承来说,子类从父类那里得到了什么?答案是:所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法,构造方法是父类所独有的。但是得到不等于可以随便使用。
每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。下表列出了不同访问属性的父类成员在子类中的访问属性:
父类成员访问属性 | 在父类中的含义 | 在子类中的含义 |
---|---|---|
public | 对所有人开放 | 对所有人开放 |
protected | 只有包内其它类、自己和子类可以访问 | 只有包内其它类、自己和子类可以访问 |
缺省 | 只有包内其它类可以访问 | 如果子类与父类在同一个包内:子类可以访问父类。否则:相当于private,不能访问 |
private | 只有自己可以访问 | 不能访问 |
对于父类的缺省的未定义访问属性的成员来说,他们是在父类所在的包内可见,如果子类不属于父类的包,那么在子类里面,这些缺省属性的成员和private的成员是一样的:不可见(属于一个包,可见)。
父类的private的成员在子类里仍然是存在的,只是子类中不能直接访问。
我们不可以在子类中重新定义继承得到的成员的访问属性。如果我们试图重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量。在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个。尽管它们同名但是互不影响,两者独立使用。
能在一个构造函数里调用两次super()吗?
如果super()两次想调用的都是父类构造函数,就不行;
如果super()两次调用的不都是父类的构造函数,两次可以是:父构造函数+父一般函数;父一般函数+ 父一般函数。
super()必须在构造函数的第一行吗?
如果子类构造函数,想调用父类的构造函数,super()就必须在第一行;
如果子类构造函数,想调用父类的非构造函数,super()不用在第一行;
二、多态
2.1 多态变量
当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配,如: Car myCar = new Car();是一个有效的赋值,因为 Car 类型的对象被赋值给声明为保存 Car 类型对象的变量。但是由于引入了继承,这里的类型规则就得叙述得更完整些:一个变量可以保存其所声明的类型或该类型的任何子类型,这个变量叫对象变量。
Java中保存对象类型的变量是多态变量。**“多态”**这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象。
2.2 子类的对象可以被看作是父类的对象来使用
2.2.1 传递给需要父类对象的函数
Car myCar = new Car();
myCar.add(new Car("奔驰", 50, "黑色", "四驱"));
add() 方法要求的是在myCar对象的 ArrayList 里面 new 进去一个 Car
2.2.2 赋值给父类变量(父类指针指向子类对象)
子类对象 myCar 可以将自己管理的内容,交给(赋)给父类的对象 Car管理。
反之,子类的变量无法管理父类的对象。
2.2.3 放进存放父类对象的容器里
在创建 ArrayList 的时候,这条里面装的是父类的类型的容器,它理应放父类对象管理者 Item1 item2 item3…但是它也放了子类对象管理者 CD1…DVD1…DVD2…
2.3 造型
将一个类型的对象赋给另外一个类型的变量,这个过程叫造型 CAST。
子类对象可赋值给父类变量(Java中不存在对象对对象的赋值!对象是具体实例,有明确定义的状态和行为)。
- 用括号围起来放在值的前面;
- 运行时有机制来检查这样的转化是否合理 ClassCastException;
- 对象本身并没有发生任何变化(所以不是类型转换(int)float 这是将那个 float 真实转换了);
造型是“把被造型对象当成那样的类型来看”。对于基本类型叫“类型转换”,对于对象类型叫“造型”。
向上造型
向上造型是默认的,不需要运算符;向上造型总是安全的;
以下哪些赋值是合规则的?
假设现有4个类:Person、Teacher、Student和PhDStudent。Teacher 和Student都是Person的子类,PhDStudent是Student的子类。以下的赋值语句哪些是合法的,为什么?:
Person p1 = new Student();
Person p2 = new PhDStudent();
PhDStudent phd1 = new Student();
Teacher t1 = new Person();
Student s1 = new PhDStudent();
s1 = p1;
s1 = p2;
p1 = s1;
t1 = s1;
s1 = phd1;
phd1 = s1;
// 自己的答案不一定对(按照程序一条一条往下走)
Person p1 = new Student();
Person p2 = new PhDStudent();
Student s1 = new PhDStudent();
s1=p1;
s1=p2;
p1=s1;
// 自己的答案不一定对(只看题干)
Person p1 = new Student();
Person p2 = new PhDStudent();
Student s1 = new PhDStudent();
s1 = phd1;
谁是谁的父类合适?
有时候事物本身比其第一印象更复杂。考虑一下,矩形和正方形的继承关系是怎样的?
因为从数学上,正方形是一种特殊的矩形。但是,从程序实现上考虑,正方形只要一个边长,而矩形可以从正方形继承后再增加一个边长成员变量。另一方面来说,如果正方形从矩形继承,那么就会出现冗余的第二个边长变量。你怎么看这个事情?
(引用)数学上的特殊情况是简化了对象的参数,而程序上反而是父类是提取了对象间相同的参数,而子类拓展了参数。
三、类型系统
3.1 Object 类
考虑在CD类里new CD,如果在CD子类里面,需要将我们送进去的信息,打印出来的话,显然直接打印,直接用toString()都不可以。返回一个带地址的东西 database.CD@30f39991。
CD cd = new CD("33", 10, false, "还行", "z", 10);
System.out.println(cd); // database.CD@30f39991
System.out.println(cd.toString()); // database.CD@30f39991
**需要怎么操作?**自己写 toString() 方法或者利用 application 提供的方法,自动 @Override 一个属于子类自己的 toString() / equals() 方法。
Sourse --> Generate–> toString() 选择(String concat (+) and super.toString())会将自己的成员变量先用上
@Override
public String toString() {
return "database.CD{" +
"artist='" + artist + '\'' +
", numofTracks=" + numofTracks +
"} " + super.toString();
}
这一步只会将自己独有的成员变量显示了。还需要到父类,再用一次上述方法,写出一个 String toString()的方法。
@Override
public String toString() {
return "database.Item{" +
"title='" + title + '\'' +
", playingTime=" + playingTime +
", gotIt=" + gotIt +
", comment='" + comment + '\'' +
'}';
}
3.2 equals() 比较两个对象内容是否相同
public static void main(String[] args) {
CD cd = new CD("33", 10, true, "还行", "z", 10);
CD cd1 = new CD("33", 10, true, "还行", "z", 10);
System.out.println(cd.equals(cd1));
}
// false
== 只能比较两个变量是否管理相同的对象,但是在这里即使用了 cd.equals(cd1) ,且它们都是相同内容的输入,我们的结果也依然是 false,为什么?
因为我们没有做自己的 equals,我们使用的是 Object 里面的 equals。 对于它来说 equals 就是**判断里面两个管理者是不是管理的同一个对象。**Sourse --> Override/Implement Methods
public boolean equals(Object obj) {
return (this == obj);
}
Override不能省略,否则编辑器不会检查,结果仍然是绕过写的CD equals,使用了object 的 equals。
@Override
public boolean equals(Object obj) { // 如果这个对象和obj参数相同,返回True;
CD cc =(CD)obj;// 因为上面参数Object 类型无法改变,所以需要向下造型
return artist.equals(cc.artist); //以artist作比
}
public static void main(String[] args) {
CD cd = new CD("33", 10, true, "还行", "z", 10);
CD cd1 = new CD("33", 10, true, "还行", "z", 10);
System.out.println(cd.equals(cd1));
}
3.3 DoME的新媒体类型
可扩展性:代码不需要经过修改,就可以扩展适应新的内容和数据。
package database;
public class VideoGames extends Item{
private int numberOfPlayers;
private String platform;
public VideoGames(String title, float playingTime, boolean gotIt, String comment,int numberOfPlayers,String platform) {
super(title, playingTime, gotIt, comment);
this.numberOfPlayers=numberOfPlayers;
this.platform=platform;
}
@Override
public void print() {
super.print();
System.out.println("Item:numberOfPlayers" + numberOfPlayers +"\nItem:platform"+platform);
}
}
以上代码加上善用 IJ提供的工具,我们就可以轻松增加一个新类型 VideoGames。
四、继承类型
五、其他关键字
5.1 implements
英*/ˈɪmplɪm(ə)nts/* 美*/ˈɪmplɪments; ˈɪmplɪmənts/ 实现。*变相的使 java 具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
5.2 final
修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。
把类定义为最终类,不能被继承,或者用于修饰方法,该方法不能被子类重写。
图片转存中…(img-AhBxNZWj-1698120284622)]
五、其他关键字
5.1 implements
英*/ˈɪmplɪm(ə)nts/* 美*/ˈɪmplɪments; ˈɪmplɪmənts/ 实现。*变相的使 java 具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
5.2 final
修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。
把类定义为最终类,不能被继承,或者用于修饰方法,该方法不能被子类重写。