浅谈继承和多态
上篇文章已谈到封装,接下来就继续来浅谈下继承和多态吧。
当我们对一个类进行实例化对象后,我们实例化对象就可以表达现实中的实体。
但是我们仔细想一想,世界东西繁多复杂,每个东西都是都不可能是独善其身的,总归和其他东西有些关联的。
请看以下代码:
就会发现,两个类中就有一个共同的属性和方法。
那能否将这些共性抽取呢?面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。
讲到这里,那什么是继承?
继承的概念:
是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性 的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了 由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
例如上面的狗和猫的那样的类,我们将其共性抽取,创建一个新的类(例如:Animal)。
让我们的猫类和子类继承这个Animal类,然后我们就将这个猫类和狗类称为:子类/派生类。Animal类称为:父类。
子类可以复用父类的成员,而子类只需要关心自己的新增成员或方法。
从刚刚的讲述中得出,继承最大的作用就是:实现代码复用,还有就是来实现多态(后序讲)
讲完概念,就来介绍下语法吧:
当然class前也可以加关键字。
注意:
- 子类会将父类中的成员变量或者成员方法继承到子类中了。
- 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承。
到这里,又有一个问题啦。那我继承父类的方法了,那我子类怎么访问父类的方法呢?
那么请看下接下来的代码吧。
看完代码,我们细想,子类和父类中是不是共同存在一个相同类型和相同名字的变量?
那这样的话,那就又有问题了,那么一旦访问,先访问谁的呢?要是不同名字,也是这样的访问吗?
那好,我们分两种情况回答:
◎ 子类和父类的成员变量名字相同时:
1.通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,否则编译报错。
2.通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错。
◎ 子类和父类的成员变量名字不相同时:
成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时 再到父类中找,如果父类中也没有则报错。
那么这个问题解决完了后,我又手痒了,我还想在子类方法中一步访问父类成员于方法,该如何做呢?
那么就要介绍这个super关键字了。
那么就看看它是怎么用的吧:
所以:在子类方法中,如果想要明确访问父类中成员时,借助super关键字即可。
注意事项:
- 只能在非静态方法中使用 2. 在子类方法中,访问父类的成员变量和方法。
那前面的类和继承讲到构造方法。
那么这里有没有呢?
那是当然的,那么这个在子类中就叫子类的构造方法:
在这里需要说明的是:
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执 行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子 肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整 ,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整。
然后我们再来看这下这个代码:
在这个图片中,我在父类加入了构造方法,而且在子类的的构造方法中添加了super关键字。
所以这里要说明以下内容:
1.若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构 造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的 父类构造方法调用,否则编译失败。
- 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句。
- super(...)只能在子类构造方法中出现一次,并且不能和this同时出现
那么问题又来了,这里讲了super和this关键字,那么它们有什么区别呢?
相同点:
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在。
不同点:
- this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成 员的引用
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性。
- 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造 方法中出现。
- 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有。
既然区别讲完了,那就讲下这个初始化吧,之前也同样讲了初始化,之前的初始化是这样的:
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行。
而这里的涉及到的初始化就有些不一样了,直接上结论:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行 2、
- 父类实例代码块和父类构造方法紧接着执行 3、
- 子类的实例代码块和子类构造方法紧接着再执行 4、
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行。
那么接下来讲讲类的继承方式吧。
单继承:
多层继承:
不同类继承同一个类:
多继承(java中不支持):
那如此看来,继承方式虽不多样,但我们依然可以写出各种各样的,其实我们不愿类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层 次太多, 就需要考虑对代码进行重构了.
那么我们就可以用到final关键字。
语法很简单,直接在方法的修辞词前面加或者成员变量前加,两者分别表示为:
修饰类:表示此类不能被继承,. 修饰变量或字段,表示常量(即不能修改)。
那么讲完这个就讲下组合吧,其实它与继承还是有点类似的。
但这个组合他又不想继承那样的语法,那么它具体就是这样写的:
// 发动机类
class Engine {
private int horsepower;
public Engine(int horsepower) {
this.horsepower = horsepower;
}
// 启动发动机的方法
public void start() {
System.out.println("Starting engine with " + horsepower + " horsepower...");
}
}
// 汽车类 - 使用组合包含发动机
class Car {
private Engine engine;
// 构造函数接收一个发动机对象
public Car(Engine engine) {
this.engine = engine;
}
// 通过组合对象的方法来操作汽车
public void startEngine() {
engine.start();
}
}
// 主程序
public class Main {
public static void main(String[] args) {
// 创建一个发动机对象
Engine myEngine = new Engine(200);
// 创建一个汽车对象,并组合(包含)上述发动机对象
Car myCar = new Car(myEngine);
// 通过汽车对象调用发动机的方法
myCar.startEngine();
}
}
注意:组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。
讲完这个,那么就讲讲目前最重要的一点了,就是多态。
那么什么是多态呢?
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
那么怎样实现多态呢?
- 必须在继承体系下。
- 子类必须要对父类中方法进行重写。
- 通过父类的引用调用重写的方法。
代码实现如下:
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
这里遇到重写:
那么就讲讲什么是重写吧:
重写:也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程 进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定 于自己的行为。 也就是说子类能够根据需要实现父类的方法。
其规则如下:
1.子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
2.被重写的方法返回值类型可以不同,但是必须是具有父子关系的
3.访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方 法就不能声明为 protected
4.父类被static、private修饰的方法、构造方法都不能被重写。
5.重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法 构成重写。
当然,这里的重写设计原则参考如下:
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容, 并且添加或者改动新的内容。
其实这里也设计到了动态绑定,它是一种技术手段来的。
其定义为:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体 调用那个类的方法。
还有一个向上转型,这也是能体现多态的特性之一:
那什么是向上转型呢?
实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
代码如下:
Animal animal = new Cat("元宝",2);
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
向上转型的优点:让代码实现更简单灵活。 向上转型的缺陷:不能调用到子类特有的方法。
那么有了向上转,当然也有向下转型。
那什么是向上转型呢:
向下转型(Downcasting)是指将父类类型的引用转换为子类类型,以便访问子类特有的属性和方法
代码范例入下:
class Animal {
void walk() {
System.out.println("Animal is walking");
}
}
class Dog extends Animal {
@Override
void walk() {
System.out.println("Dog is walking");
}
void bark() { // 子类特有的方法
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 向上转型
myAnimal.walk(); // 输出 "Dog is walking"
// 向下转型,检查并调用子类特有的方法
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // 强制向下转型
myDog.bark(); // 输出 "Dog barks"
} else {
System.out.println("The animal is not a dog.");
}
}
}
至此,已完。