四、继承、封装、多态
今天我们开始讲这个面试时老生常谈的问题。什么是继承?什么是封装?什么是多态?
我们一步步往前走:
对象?我们现实世界中,一切物体,一切能看得到摸得着的实体,都可以视之为一个对象。而我们java中也是一样,只不过java中的对象是一个逻辑实体,它可以和现实进行对应,也可以是一个虚拟的概念。
比如我们有一支笔,它在现实生活中是一个对象。那么接下来,我们把它映射到java的世界中:
public class Pen {
private float length;
private float color;
public float getLength() {
return length;
}
public void setLength(float length) {
this.length = length;
}
public float getColor() {
return color;
}
public void setColor(float color) {
this.color = color;
}
}
可以见到,我们新建了一个笔的对象。它跟其他的笔一样,都至少是有两个属性的,一个是它的长度,一个是它的颜色。这样,我们是不是就把它和现实中的这支笔关联了起来呢。
我们知道如何建立一个类之后,接下来就是java面向对象的核心概念,继承:当一个对象获取父对象的所有属性和行为时,称为继承。它提供代码可重用性,它用于实现运行时多态性。在java中使用extends关键字来实现继承:
public class Pencil extends Pen {
@Override
public float getLength() {
return length - currentTime;
}
@Override
public void setLength(float length) {
super.setLength(length);
}
@Override
public float getColor() {
return super.getColor();
}
@Override
public void setColor(float color) {
super.setColor(color);
}
}
这里我们看到,我们新建了一个类,继承了Pen,这个类代表了Pencil。是不是很形象?笔是这一类实体的总称,而铅笔只是其中一种。我们可以通过继承笔的一些特性,同时也可以覆写笔的一些方法。那么假设,我们有两支笔,一只是铅笔,另一只是钢笔呢?他们有着共同的特质,但又有着不同的点。那么这就要讲到java的多态:
public class FountainPen extends Pen {
@Override
public float getLength() {
return length;
}
@Override
public void setLength(float length) {
super.setLength(length);
}
@Override
public float getColor() {
return super.getColor();
}
@Override
public void setColor(float color) {
super.setColor(color);
}
}
细心的同学应该已经发现了差别,铅笔中,它的长度是随着时间递减的,因为铅笔会越用越短。而钢笔呢,我们发现他的长度是与时间无关的。这就是我们常说的多态。他们都继承自Pen,但他们又各自重写了获取长度的方法。好,相信大家这时已经明白了对象、继承和多态的意义。
我们理解了上面的概念之后,我们再回过头来看第一段代码。我们发现,它的长度length,颜色color两个属性,都是private私有的。我们在外部新建一个Pen对象后,并不能直接对这两个属性进行操作或者访问,这时,我们就只能通过Pen实例暴露出来的set或者get方法来实现操作和访问。
private void showDifferent(){
Pen pen = new Pen();
//pen.color = 2f;
pen.setColor(2f);
}
我们发现,我们做到了将颜色和长度甚至是其他各种各样的属性整合在一起,成为一个java类,而外部不能直接对它的属性进行直接操作,只能通过Pen对象暴露出来的具体方法达到一定的目的。这是不是就好像我们把一堆糖果包装了起来,并且不让别人获得?这个时候,如果外部需要拿你的糖果,是不是只能通过你的手,让你拿给他们?而这个让我们感觉到安全的模式,我们称之为封装。
关键问题?
1、什么是抽象类?什么是接口?抽象类和接口有什么区别?
首先,抽象类和抽象方法都使用 abstract 关键字进行声明,如果一个类中包含抽象方法,那么这个类必须声明为抽象类。我们在前面已经能看到什么是类,我们可以通过一个类实例化成一个对象。那抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。为了方便理解,我们再举一个例子:普通类实例化获得一个对象,那么普通类就好像是一个实际的生产者,可以通过它不断地生产新的对象,而抽象类,就好比是一张图纸,它不能从事建造,只是定义了一部分的建造规范和特征。我们必须要有一个生产者继承自这张图纸,才能把图纸里面的内容发挥出来。
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,让它们都实现新增的方法。接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。接口的字段默认都是 static 和 final 的。
比较:从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
那么该如何选择:在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。使用接口的话,需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;需要使用多重继承。使用抽象类的话,需要在几个相关的类中共享代码。需要能控制继承来的成员的访问权限,而不是都为 public。需要继承非静态和非常量字段。
2、开闭原则
开闭原则的命名被应用在两种方式上。这两种方式都使用了继承来解决明显的困境,但是它们的目的,技术以及结果是不同的。
梅耶开闭原则这一想法认为一旦完成,一个类的实现只应该因错误而修改,新的或者改变的特性应该通过新建不同的类实现。新建的类可以通过继承的方式来重用原类的代码。衍生的子类可以或不可以拥有和原类相同的接口。
多态开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用。已存在的接口对于修改是封闭的,并且新的实现必须,至少,实现那个接口。
3、动态绑定
动态绑定(后期绑定)是指:在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。例子:我们假设Father ft = newSon();ft.say();Father中包含say方法,Son继承自Father,重写了say(),此时执行Son中重写的say方法。向上转型时,用父类引用执行子类对象,并可以用父类引用调用子类中重写了的同名方法。但是不能调用子类中新增的方法。
4、里氏替换原则
定义:任何基类可以出现的地方,子类一定可以出现。
5、重写和重载
符合里氏替换重写有以下三个限制:子类方法的访问权限必须大于等于父类方法;子类方法的返回类型必须是父类方法返回类型或为其子类型。子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
重写和继承会导致一个调用权限的问题。java的实现机制是:比如在调用A类的一个方法时,先从A类中查找看是否有对应的方法,如果没有再到A的父类中查看,看是否从父类继承来。否则就要对参数进行转型,比如参数是对象B,那么B转成父类之后看A类中是否有对应的方法,没有的话再去A的父类中查看。总的来说,方法调用的优先级为:
1.this.func(this) 2.super.func(this) 3.this.func(super) 4.super.func(super)