以下为个人学习笔记和习题整理
课程:面向对象程序设计 ——Java 语言
- 浙江大学 - 翁恺 @ 中国大学 MOOC
https://www.icourse163.org/course/ZJU-1001542001
# 课堂笔记
面向对象程序设计语言有三大特性:封装、继承和多态性。
# 继承 / 派生 extends
用来做基础派生其它类的那个类称为父类、超类或者基类,派生出来的新类称为子类。
Java 用关键字 extends
表示这种继承 / 派生关系。
class ThisClass extends SuperClass { | |
//… | |
} |
# 子类父类关系
继承表达了一种 is-a
关系,就是说,子类的对象可以被看作是父类的对象。
Java 的继承只允许单继承,即一个类只能有一个父类。
哪些东西被继承了,子类从父类那里得到了什么?
答案是所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法。
构造方法是父类所独有的,因为它们的名字就是类的名字,所以父类的构造方法在子类中不存在。除此之外,子类继承得到了父类所有的成员。
但是得到不等于可以随便使用。
每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:
有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。
下表列出了不同访问属性的父类成员在子类中的访问属性:
父类成员访问属性 | 在父类中的含义 | 在子类中的含义 |
---|---|---|
public | 对所有人开放 | 对所有人开放 |
protected | 只有包内其它类、自己和子类可以访问 | 只有包内其它类、自己和子类可以访问 |
缺省 | 只有包内其它类可以访问 | 如果子类与父类在同一个包内:只有包内其它类可以访问。否则:相当于 private ,不能访问 |
private | 只有自己可以访问 | 不能访问 |
public
的成员直接成为子类的 public
的成员, protected
的成员也直接成为子类的 protected
的成员。
Java 的 protected
的意思是包内和子类可访问,所以它比缺省的访问属性要宽一些。
对于父类的缺省的未定义访问属性的成员来说,他们是在父类所在的包内可见。
如果子类不属于父类的包,那么在子类里面,这些缺省属性的成员和 private
的成员是一样的:不可见。
父类的 private
的成员在子类里仍然是存在的,只是子类中不能直接访问。
我们不可以在子类中重新定义继承得到的成员的访问属性。
如果我们试图重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量,在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个。
尽管它们同名但是互不影响。
在构造一个子类的对象时,父类的构造方法也是会被调用的,而且父类的构造方法在子类的构造方法之前被调用。
在程序运行过程中,子类对象的一部分空间存放的是父类对象。
因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化。在这个过程中,如果父类的构造方法需要参数,如何传递参数就很重要了。
# 多态变量
子类型类似于类的层次,类型也构成了类型层次。
子类所定义的类型是其超类的类型的子类型。
当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配,如:
Car myCar = new Car(); |
是一个有效的赋值,因为 Car
类型的对象被赋值给声明为保存 Car
类型对象的变量。但是由于引入了继承,这里的类型规则就得叙述得更完整些:
一个变量可以保存其所声明的类型或该类型的任何子类型。
对象变量可以保存其声明的类型的对象,或该类型的任何子类型的对象。
Java 中保存对象类型的变量是多态变量。
“多态” 这个术语(字面意思是许多形态)是指,一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象。
- 类定义了类型
- 子类定义了子类型
- 子类的对象可以被当作父类的对象来使用
- 赋值给父类的变量
- 传递给需要父类对象的函数
- 放进存放父类对象的容器里
Java 的对象变量是多态的,它们能保存不止一种类型的对象。 它们可以保存的是声明类型的对象,或声明类型的子类的对象。 当把子类的对象赋给父类的变量的时候,就发生了向上造型。
# 造型 cast
子类的对象可以赋值给父类的变量。
注意 java 中不存在对象对对象的赋值!
父类的对象不能赋值给子类的变量!
Vechicle V; | |
Car C= new Car(), | |
v=c; // 可以 | |
c=v; // 编译错误! |
可以用造型:
c = (Car) v, | |
// 只有当 v 这个变量实际管理的是 Car 才行 |
用括号围起类型放在值的前面。
对象本身并没有发生任何变化,所以不是 “类型转换”
运行时有机制来检查这样的转化是否合理 C1assCastException
# 向上造型
- 拿一个子类的对象,当作父类的对象来用
- 向上造型是默认的,不需要运算符
- 向上造型总是安全的
# 多态
所有的类都是继承自 Object
类
Object
类的函数,如
toString()
equals()
如果子类的方法覆盖 @Override
了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。
覆盖的新版本具有与老版本相同的方法签名:相同的方法名称和参数表。
子类和父类中存在名称和参数表完全相同的函数,这一对函数构成覆盖关系。
通过父类的变量,调用存在覆盖关系的函数时, 会调用变量当时所管理的对象所属的类的函数。
对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。
不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。
覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被 “覆盖” 起来而看不见了。
而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。
注意这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。
当调用一个方法时,究竟应该调用哪个方法,这件事情叫做绑定。
绑定表明了调用一个方法的时候,我们使用的是哪个方法。
绑定有两种:
一种是早绑定,又称静态绑定,根据变量的声明类型来决定,这种绑定在编译的时候就确定了;
另一种是晚绑定,即动态绑定。动态绑定在运行的时候,根据变量当时实际所指的对象的类型,动态决定调用的方法。即,根据变量的动态类型来决定。
Java 缺省使用动态绑定。
在成员函数中调用其他成员函数也是通过 this
这个对象变量来调用的。
# 课内项目
# 媒体数据库
public class Item { | |
private String title; | |
private int playingTime; | |
private boolean gotIt = false; | |
private String comment; | |
// 构造器 | |
public Item(String title, int playingTime, boolean gotIt, String comment) { | |
super(); | |
this.title = title; | |
this.playingTime = playingTime; | |
this.gotIt = gotIt; | |
this.comment = comment; | |
} | |
// 无参数构造器,子类 super () 调用 | |
public Item() { | |
} | |
public void setTitle(String title) { | |
this.title = title; | |
} | |
public void print() { | |
System.out.println(title); | |
} | |
} |
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, false, comment); // 调用 Item 的构造器 | |
// this.title = title; | |
this.artist = artist; | |
this.numofTracks = numofTracks; | |
// this.playingTime = playingTime; | |
// this.comment = comment; | |
} | |
public void print() { | |
System.out.print("CD:"); | |
super.print(); | |
System.out.print(artist); | |
} | |
public static void main(String[] args) { | |
CD cd = new CD("a","b", 2, 2, "..."); | |
cd.print(); | |
System.out.println(cd); | |
System.out.println(cd.toString()); | |
String s = "aa" + cd;//cd 会自动 toString () | |
System.out.println(s); | |
CD cd1 = new CD("a","b", 2, 2, "..."); | |
System.out.print(cd.equals(cd1)); | |
// 如果没有自己的 equals 方法,将显示 false | |
} | |
@Override | |
public String toString() { | |
return "CD [artist=" + artist + ", numofTracks=" + numofTracks + ", toString()=" + super.toString() + "]"; | |
} | |
@Override | |
public boolean equals(Object obj) { | |
CD cc = (CD) obj;// 向下造型 | |
return artist.equals(cc.artist); | |
} | |
} |
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(title, playingTime, false, comment); | |
// this.title = title; | |
setTitle("b"); | |
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); | |
System.out.print("DVD:"); | |
super.print(); | |
System.out.print(director); | |
} | |
} |
import java.util.ArrayList; | |
public class Database { | |
// 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(); | |
// } | |
for (Item item:listItem) { | |
item.print(); | |
System.out.println(); | |
} | |
} | |
public static void main(String[] args) { | |
Item item = new Item("a", 0, true, "..."); | |
CD cd = new CD("a", "a", 0, 0, "..."); | |
item = cd; | |
CD cc = (CD) item; // 强制转换 | |
Database db = new Database(); | |
db.add(new CD("abc", "abc", 4, 60, "...")); | |
db.add(new CD("efg", "efg", 4, 60, "...")); | |
db.add(new DVD("xxx", "aaa", 60, "...")); | |
db.list(); | |
} | |
} |
# 课堂讨论
- 关于
super()
:能在一个构造函数里调用两次super()
吗?super()
必须在构造函数的第一行吗?
不可以调用两次,且必须在第一行。
子类是从父类继承而来,继承了父类的属性和方法,如果在子类中先不完成父类的成员的初始化,则子类无法使用,因为在 java 中不允许调用没初始化的成员。在构造器中是顺序执行的,也就是说 super () 必须在第一行进行父类的初始化。
- 父类的私有的成员函数在子类中能使用吗?
子类不可直接调用父类的
private
方法,可以通过父类的public
(及其他子类可访问)方法和this
间接使用父类private
方法。
- 假设现有 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 = (Student)p1; | |
s1 = p2; | |
s1 = (PhDStudent)p2; | |
s1 = (Student)p2; | |
p1 = s1; // 子类型给父类型,安全 | |
t1 = s1; // 子类型给子类型,不可转换 | |
s1 = phd1; // 子类型给父类型,安全 | |
Phd1 = s1; | |
phd1 = (PhD Student)s1; |
4. Item
的 print
函数有几种可能的实现方式:
1. 和视频中一样, Item
的 print
输出 Item
的私有成员变量,然后 CD
等子类 override
这个 print
函数,实现完整的输出,并在其中通过 super.
调用 Item
的 print
;
2. Item
的 print
会输出完整的内容,但是其中子类的部分,会调用一个 protected
的 print_task
函数来输出。子类则不去 override
print
,而是 override
这个 print_task
函数,实现子类自己的成员变量的输出;
3. Item
的 print
函数什么也不做。另外给一个 format
函数,输出格式化的私有成员变量的内容表达。子类利用这个 format
函数得到内容后输出。
你觉得这些方案哪个更合适?为什么?如果你有更好的方案,也请提出来。