前言
前面我们详细介绍了 封装 的特性,今天我们来介绍剩下的两大特性——继承和多态。也欢迎各位大佬对文章错误的部分斧正。
封装的部分,可回顾笔者之前的博客
什么是继承
通过前面的学习我们知道,Java中一切皆对象,创造一个对象则需要自定义一个类来规范这个对象的属性和功能。此时如果我们要自定义“狗”和“猫”这两个类的时候,我们发现它们之间有很多相似的部分——比如都具备名字,年龄这两个属性,都具备吃和睡觉这两个功能。我们知道,猫和狗都属于动物这个范畴,我们能否定义出动物这个类,规范动物这个类的属性和功能,狗和猫在动物这个基础上扩展出特有的属性和功能呢?这样极大的简化了代码,也在类和类之间创造了关联。显然这样是可以的,继承正是解决这个问题而被引入的语法概念。
面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用
继承的概念
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用
例如,我们前面举得列子,猫和狗都继承于动物这个类
继承的语法结构
在Java中如果要表示类之间的继承关系,需要借助extends关键字
class Parent {
// 父类的属性和方法
}
class Child extends Parent {
// 子类的属性和方法
}
例子
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭");
}
public void sleep() {
System.out.println(this.name + "正在睡觉");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void woof() {
System.out.println(this.name + "正在汪汪汪");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void miao() {
System.out.println(this.name + "正在喵喵喵");
}
}
- 子类会将父类中的成员变量或者成员方法继承到子类中了
- 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
- 父类静态的属性和方法会被继承,可以用类名.属性/方法名访问,子类和父类的类名均能访问。注意,本文不讨论访问修饰限定符的问题,这不是本文要讨论的重点,本文默认访问修饰符均为public,在任何位置均能访问到
关于第三个点笔者对能被继承这个说法的准确性不是很确定,但实验下来是可以运行的,姑且就先这么理解
我在Animal类中加入如上代码,运行测试用例,运行通过
继承后,如何对属性和方法进行访问
父类访问子类
静态属性和方法可用类名直接点出
非静态的属性和方法则需要先创建对象,再由对象点出。
这点是符合静态属于类,非静态属于对象的
子类访问父类
子类和父类不存在同名成员属性/方法
可以直接用属性/方法名访问。但这种方法有弊端,在存在重名的时候不加注意会有意想不到的结果
子类和父类存在同名成员变量
前面我们在封装中学过一个 this 的关键字,在这里我们在介绍一个和它及其类似的关键字——super。该关键字主要作用:在子类方法中访问父类的成员。
super的注意:1. 只能在非静态方法中使用 2. 在子类方法中,访问父类的成员变量和方法。
this访问子类的属性/方法,super访问父类的属性/方法。什么都不写默认是this。支持重载,在子类和父类方法名相同时可能会涉及重写,但在子类访问父类方法时不会发生。重写会在多态那部分在介绍
这就是问什么说不加修饰直接访问可能会有意想不到的结果
总结
- 如果访问的成员属性/方法子类中有,优先访问自己的成员属性/方法。
- 如果访问的成员属性/方法子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员属性/方法同名,则优先访问自己的
- this和super可实现具有针对的访问,笔者建议无论如何添加上,添加代码的可控性
成员属性/方法访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
再谈构造方法
前面,我们在封装中介绍过构造方法,在继承中我们要再谈构造方法
首先,有一个原则,子类在构造时要先帮父类完成构造
和this()调用其他构造方法类似,在子类构造方法中,可以用super()调用父类的构造方法,先帮助父类完成构造。
我们知道,子类中我们不写任何的构造方法,会默认有一个无参的构造方法,在这个无参构造方法中,会默认调用父类无参的构造方法,这些部分编译器会自动完成。如果在子类或者父类中提供任何构造方法,编译器不在提供默认的构造方法,需要程序员手动补齐剩下的逻辑
子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整。
- 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
- 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句。
- super(...)只能在子类构造方法中出现一次,并且不能和this同时出现
区分super与this
在前面的介绍中,我们发现super和this有很多相同点,也有不同点,我们在见识过它们俩之后再区分一下两者
相同
1. 都是Java中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同
1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
3. 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造方法中出现
4. 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有
创造一个子类对象都发生了什么
1.父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
再谈访问修饰限定符
我们在封装的部分介绍过访问修饰限定符,但当时我们对于protected并没有解释,现在在这里补充一下
在同一个包下,子类或者非子类均可访问 or 在不同包下,只有子类可以访问,非子类不可访问
注意:父类中private成员变量虽然在子类中不能直接访问,但是也继承到子类中了
继承方式
Java中支持的继承方式有:
单继承:A继承于B
多层继承:A继承于B,B继承于C
不同类继承于同一个类:A继承于C,B继承于C
注意不支持多继承,即A继承于B,A继承于C在语法上不支持
我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层
次太多, 就需要考虑对代码进行重构了
如果想从语法上进行限制继承, 就可以使用 final 关键字
final
修饰变量或字段,表示常量(即不能修改)
final int a = 10;
a = 20; // 编译出错
修饰类:表示此类不能被继承。我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 编译出错
修饰方法:表示该方法不能被重写(后序介绍)
组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车
汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的。
// 轮胎类
class Tire{
// ...
}
// 发动机类
class Engine{
// ...
}
// 车载系统类
class VehicleSystem{
// ...
}
class Car{
private Tire tire; // 可以复用轮胎中的属性和方法
private Engine engine; // 可以复用发动机中的属性和方法
private VehicleSystem vs; // 可以复用车载系统中的属性和方法
// ...
} /
/ 奔驰是汽车
class Benz extend Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合
什么是多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
多态的实现
在java中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭");
}
public void sleep() {
System.out.println(this.name + "正在睡觉");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void woof() {
System.out.println(this.name + "正在汪汪汪");
}
@Override
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void miao() {
System.out.println(this.name + "正在喵喵喵");
}
@Override
public void eat() {
System.out.println(this.name + "正在吃猫粮");
}
}
public class Main {
public static void main(String[] args) {
Animal cat = new Cat("mimi",12);
Animal dog = new Dog("wangwang",10);
dog.eat();
cat.eat();
}
}
重写
重写是实现多态的基础,可以说正是因为有重写才能得以实现不同对象调用的方法各不相同,才能实现多态
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
重写的规则
子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
被重写的方法返回值类型可以不同,但是必须是具有父子关系的
访问权限不能比父类中被重写的方法的访问权限更低
父类被static、private修饰的方法、构造方法都不能被重写。
重写的方法, 可以使用 @Override 注解来显式指定.。有了这个注解能帮我们进行一些合法性校验
原则上,我们对已经投入使用的类可拓展功能但不再修改功能,准确的做法应该是新写一个类继承老的类,对其共性的部分复用,重写需要修改的功能
区分重写和重载
重载是静态绑定/早绑定,在编译时,根据用户所传递实参类型就确定了具体调用那个方法 重写是动态绑定/晚绑定,在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法
向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
向上转型的三个场景:直接赋值,方法传参,方法返回值
向上转型是实现多态的基础条件,可以使代码更加灵活,但缺点是无法访问子类特有的方法
向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
向下转型我们一般认为是不安全的。但可以通过instanceof进行判断,在确认安全的情况下完成向下转型的操作。
多态的优缺点
1.降低代码的圈复杂度
简单介绍一下圈复杂度。我们认为一段代码中,分支或者循环 这样的结构出现的越多,代码圈复杂度越高,越难以阅读。我们推荐让代码尽量的“平铺直叙”
2.可拓展性增强
3.属性没有多态性,父类只能调用自己的属性,属性不存在多态,如果想要访问子类属性,需要向下转型
4.构造方法没有多态性。我们应该尽量避免在构造方法中调用重写方法,父类在调用构造方法时,子类还没有完成构造,可以说子类还没有进入可工作状态。此时调用子类的重写方法,可能会因为子类还未完成初始化,造成意想不到的结果。
抽象类/方法
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类
在抽象类可以定义抽象方法,因为抽象类不具备足够量的属性/方法,无法指向任何具体的对象,它无法实例化,同样抽象方法也因为过于模糊而难以定义。所以一般抽象方法不需要写方法体,抽象方法所在的类必须是抽象类,抽象类就是为了被继承,抽象方法就是为了被重写。
抽象类/属性语法
在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体
抽象类也是类,内部可以包含普通方法和属性,甚至构造方法
注意
- 抽象类不能实例化
- 抽象方法不能是 private 的
- 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
- 抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
- 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
- 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
抽象的意义
我们发现,即使是普通的类/方法,也能实现抽象类的功能,那我们为什么需要使用抽象类呢?
更多的是我们需要用抽象类,增加一层编译器的校验,有问题能及时报错,增加开发效率
接口
我们将抽象类再一步抽象,就会得到接口,接口是一种多个类的公共规范,是一种引用数据类型
语法
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口
public interface 接口名称{
// 抽象方法
public abstract void method1(); // public abstract 是固定搭配,可以不写
//……
}
阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
使用
接口不能直接使用,必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法。
public class 类名称 implements 接口名称{
// ...
}
注意:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。
注意
接口是抽象类的进一步抽象,不能实例化对象
接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是public abstract,其他修饰符都会报错)
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现
重写接口中方法时,不能使用默认的访问权限
接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量
接口中不能有静态代码块和构造方法
接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
jdk8中:接口中还可以包含default方法。
实现多接口
在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类
继承与接口
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性
接口的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字
结语
以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。
因为这对我很重要。
编程世界的小比特,希望与大家一起无限进步。
感谢阅读!