继承 与 多态 简单介绍Object类
一. 继承
继承是非常简单而强大的设计思想,它提供了我们代码重用和程序组织的有力工具。
类是规则,用来制造对象的规则。我们不断地定义类,用定义的类制造一些对象。类定义了对象的属性和行为,就像图纸决定了房子要盖成什么样子。
一张图纸可以盖很多房子,它们都是相同的房子,但是坐落在不同的地方,会有不同的人住在里面。假如现在我们想盖一座新房子,和以前盖的房子很相似,但是稍微有点不同。任何一个建筑师都会拿以前盖的房子的图纸来,稍加修改,成为一张新图纸,然后盖这座新房子。所以一旦我们有了一张设计良好的图纸,我们就可以基于这张图纸设计出很多相似但不完全相同的房子的图纸来。
基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是public还是private。当然,在这之后,程序员还可以加入自己的新的成员,包括变量和方法。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。继承是支持代码重用的重要手段之一。
类这个词有分类的意思,具有相似特性的东西可以归为一类。比如所有的鸟都有一些共同的特性:有翅膀、下蛋等等。鸟的一个子类,比如鸡,具有鸟的所有的特性,同时又有它自己的特性,比如飞不太高等等;而另外一种鸟类,比如鸵鸟,同样也具有鸟类的全部特性,但是又有它自己的明显不同于鸡的特性。
如果我们用程序设计的语言来描述这个鸡和鸵鸟的关系问题,首先有一个类叫做“鸟”,它具有一些成员变量和方法,从而阐述了鸟所应该具有的特征和行为。然后一个“鸡”类可以从这个“鸟”类派生出来,它同样也具有“鸟”类所有的成员变量和方法,然后再加上自己特有的成员变量和方法。无论是从“鸟”那里继承来的变量和方法,还是它自己加上的,都是它的变量和方法。
继承 is-a 而不是 has-a //判断分类树是否合理 采用is-a测试
1. 继承的好处
- 如果无法继承,程序代码重复的太多,一方面代码冗余,另一方面每次维护都要修改多处极其不方便,即可拓展性差
- 定义出了共同的协议(通过继承来定义相关类之间的共同协议)
2. 继承的概念
-
我们把用来做基础派生其它类的那个类叫做父类、超类或者基类(
SuperClass
),而派生出来的新类叫做子类(ThisClass
)。Java用关键字extends
表示这种继承/派生关系:class ThisClass extends SuperClass { // …… }
Java的继承只允许单根继承,即一个类只能有一个父类
Java中,支持多重继承,爷爷..爸爸..儿子..
3. 重点:子类与父类的关系 (继承 访问 构造 this与super)
- 哪些东西被继承了?继承了是否可以访问?
答: 继承了所有成员变量、成员函数/方法,包括其在父类中的访问属性,除了构造器
比喻: 继承 ——得到 访问——使用
我们不可以在子类中重新定义继承得到的成员的访问属性。
-
如果我们试图重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量,在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个。尽管它们同名但是互不影响。 (就近原则)
- 如果父子类有有同名属性,在子类方法中想要使用父类的属性值,使用super。
得到不等于可以随便使用。父类的private的成员在子类里仍然是存在的,只是子类中不能直接访问。每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。
-
this
与super
:-
this
: 当前(子类)对象的引用- 区分同名的局部变量与属性
- 调用其他构造方法。
this(参数列表)
-
super
: 代表当前类的父类的对象的引用- super不是对象,而是当前子类的对象有一个区域 可以指向父类的对象的成员
- 区分子类与父类的成员变量(属性)
- 区分同名的(覆盖的)父子类相同方法
-
子类中调用父类的构造方法
super(参数列表)
- 在子类构造方法中调用父类构造方法必须在第一句,而且只能有一个
-
-
子类与父类 构造方法与顺序
-
构造顺序: 父类初始化模块 -> 父类构造方法 -> 子类初始化模块 -> 子类构造方法
( 务必联想 debug时 代码执行的顺序)
class A{ static { System.out.println("父类静态代码块"); // No.1 } public A(){ System.out.println("父类构造方法"); // No.3 } { System.out.println("父类初始化块"); // No.4 } } public class B extends A{ static{ System.out.println("子类静态代码块"); // No.2 } public B(){ System.out.println("子类构造方法"); // No.5 } { System.out.println("子类初始化块"); // No.6 } public static void main(String[] args){ new B(); } }
-
构造方法:
-
当没有自定义的情况下,jvm会默认分配一个无参构造方法
public ThisClass () { super(); }
在子类构造方法中,若没有调用其他构造方法,第一句默认调用父类的无参构造
super()
-
若需要给父类子类共同的成员变量构造初始化,又给子类特殊的成员变量构造初始化,使用
super(参数列表)
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) { this.title = title; this.playingTime = playingTime; this.gotIt = gotIt; this.comment = comment; } } public class DVD extends Item { private String director; public DVD(String title, String director, int playingTime, String comment) { super(title, playingTime, false, comment); // 值得注意 this.director = director; } }
- 使用super(参数)调用父类的构造方法,必须在第一句。只能有一个。
-
-
-
覆盖/重写 (override) // 区分重载(overload)
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
而重载overload 是在一个类里面,方法名字相同,而参数列表必须不同。 最常见的是构造方法的重载
- 重写规则:
- 命名与参数列表必须与被重写方法完全相同
- 返回类型必须是父类返回类型的派生类(一般而言都是相同)
- 访问权限不能比父类更低(一般相同 或更高)
- 父类的方法只能被它的子类覆盖重写
- static不能被重写,但是可以另外再次声明(属于子类静态方法)
- 构造方法不能被重写
4. 继承的五大设计步骤
- 找到具有共同的属性和行为的对象
- 抽象设计出代表共同属性和行为的类
- 决定子类是否需要让某项行为(方法的实现)有特定不同的运作方式 // 是否需要覆盖重写
- 进一步抽象,寻找使用共同行为的子类
- 建立继承树, 完成类的继承层次
二. 多态
类定义了类型,DVD类所创建的对象的类型就是DVD。类可以有子类,所以由那些类定义的类型可以有子类型。在DoME的例子中,DVD类型就是Item类型的子类型。
子类型类似于类的层次,类型也构成了类型层次。子类所定义的类型是其超类的类型的子类型。
当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配,如:
Car myCar = new Car();
是一个有效的赋值,因为Car类型的对象被赋值给声明为保存Car类型对象的变量。但是由于引入 了继承,这里的类型规则就得叙述得更完整些:
一个变量可以保存其所声明的类型或该类型的任何子类型。
对象变量可以保存其声明的类型的对象,或该类型的任何子类型的对象。
Java中保存对象类型的变量是多态变量。“多态”这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象。
1. 多态变量
首先,明确变量与对象的区别,对象是实体,变量是引用(控制者/遥控器)
-
类Class定义了类型Type,子类定义了子类型; 子类的对象可以被“当作”父类的对象来使用
-
赋值给父类的变量(子类对象 赋值给 声明类型为父类类型的变量)(向上造型)
Vehicle v1 = new Car();
-
传递给需要父类对象的函数(子类对象 传递给函数参数为父类类型的函数)
public void add(Item item) {}
base.add(cd)
-
放进存放父类对象的容器里(子类对象 放入存放对象类型为父类类型的泛型容器)
ArrayList<Item> 可以存储 CD, DVD 等子类型的对象
-
变量的声明类型 称为 静态类型, 程序运行到这个变量时实际存储的类型 称为 动态类型
Java的对象变量都是多态变量,能保存其所声明的类型的对象,或者声明类型的子类型的对象
当把子类的对象赋给父类的变量时,就发生了向上造型(父类变量 引用 子类对象)
2.向上造型
-
造型 Cast :静态类型的变量 引用了一个 动态类型与静态类型不符合的对象
用括号围起类型放在值的前面(子类对象与父类引用变量连接 自动向上造型)
对象本身没有发生任何变化(所以不是“类型转换”)(实质上只是当成什么类型来看待的问题,而不是转变)
运行时有机制来检查这样的转化是否合理(Java只支持向上造型,不支持向下造型)否则抛出
ClassCastException
-
向上造型:
只要 父类引用变量(多态变量) 指向 子类对象实例 那么就发生向上造型(拿一个子类的对象 当成父类对象来用)
-
父类不能引用子类中的独有的属性或方法。只能由子类重写override父类中的方法或属性
(向上造型后 子类 将不再具备其自己定义的方法,只有父类的方法。但若重写了父类的方法,向上造型的对象的方法为重写后新的方法)
class Animal {
public void move() {
System.out.println("Animals can move.");
}
// public void bark() {}
}
class Dog extends Animal {
public void move() {
super.move();
System.out.println("Dog can run and jump.");
}
public void bark() {
System.out.println("bark! bark!");
}
}
public class TestDog {
public static void main(String[] args) {
Animal a = new Animal();
Dog d = new Dog();
Animal m = new Dog(); //向上cast
m.bark(); // 此时编译错误,除非在Animal中添加bark()方法;
// 添加之后正常运行,输出 bark!bark! (已override)
a.move();
d.move(); //override
d.bark();
Dog dog = new Dog();
Animal animal = new Animal();
animal = dog; // 向上造型,没问题
dog = animal; // 向下造型,错误
dog = (Dog)animal //强制cast,编译可以通过,但是由于animal引用的对象实例是Animal,抛出异常
animal = d; // 将d向上造型
dog = animal; // 虽然意义上可行,但是依然属于向下造型,错误
dog = (Dog)animal;// 强制cast,对象实例符合Dog,合理,正常
}
}
3. 覆盖/重写override 与 绑定
如果子类的方法覆盖了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。覆盖的新版本具有与老版本相同的方法签名:相同的方法名称和参数表。因此,对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。
覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被“覆盖”起来而看不见了。而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。注意我们这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。
当调用一个方法时,究竟应该调用哪个方法,这件事情叫做绑定。绑定表明了调用一个方法的时候,我们使用的是哪个方法。绑定有两种:一种是早绑定,又称静态绑定,这种绑定在编译的时候就确定了;另一种是晚绑定,即动态绑定。动态绑定在运行的时候根据变量当时实际所指的对象的类型动态决定调用的方法。Java缺省使用动态绑定。
-
当通过引用变量调用方法的时候,调用哪个方法这件事情叫做绑定。
- 静态绑定:根据引用变量的静态类型(声明类型)来决定
- 动态绑定:根据引用变量的动态类型(实际指向的对象实例类型)来决定
Java默认所有的绑定都是 动态绑定
-
所以 向上造型的时候,虽然只能调用父类有的方法,但是支持override,即只要override那么执行override之后的方法
(既然你是一个Animal,那你应该会bark(),那么,你就去bark()吧; 至于如何bark(),看着办)
三. Object类
- 所有的没有extends的类 默认继承于Object类(Object类是所有类的“上级”)
Object类的方法
-
toString()
- 返回该对象的地址的String; (除非进行了override,比如String进行了override)
- System.out.println(); 默认将()里的对象执行toString()方法
-
equals()
- 返回boolean 是否是指向同一个对象实例(String 进行了override 可以判断等值与否)
附: 简单介绍:可拓展性 可维护性
可拓展性: 代码不需要改动或很少改动,就可以扩展特性,适应新的内容
可维护性: 经过修改可以适应新的内容
@Author: nju_zzp
@Date: 2020/3/16