# 《JAVASE系列》继承与多态
文章目录
前言:
本章学习内容:
- 熟悉继承的语法
- 学会如何使用继承
- 理解多态以及背后知识
参考书籍:《java核心卷1》
你若盛开,清风自来。
1.继承
1.1 继承的作用
在面向对象的世界里,对象用来表示现实的事物,但是现实事物中必有很多共同之处与联系,所以java语言需要考虑这个问题:
例如:狗和猫都是动物,都有年龄体重
如果没有继承思想的话:
就会产生代码的重复
class Dog{
public String name;
public int age;
public float weight;
public void eat(){
System.out.println(name+"吃饭");
}
public void sleep(){
System.out.println(name+"睡觉");
}
public void bark(){
System.out.println(name+"汪汪");
}
}
class Cat{
public String name;
public int age;
public float weight;
public void eat(){
System.out.println(name+"吃饭");
}
public void sleep(){
System.out.println(name+"睡觉");
}
public void mew(){
System.out.println(name+"喵");
}
}
public class test {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
dog.name = "小王";
cat.name = "小咪";
dog.eat();
dog.sleep();
dog.bark();
cat.eat();
cat.sleep();
cat.mew();
}
}
运行结果:
我们可以从代码中看到:狗类与猫类有很多相同之处。我们可以用继承来解决这个代码重复的问题,面相对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。
1.2 继承概念
继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。 继承是面向对象 软件技术当中的一个概念。 这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。 子类的创建可以增加新数据、新功能,可以继承父类全部的功能,但是不能选择性的继承父类的部分功能。
简单来说:就是子类可以继承父类的属性与方法,实现代码的复用,子类抽取了父类,来实现代码的复用。
其中父类为Animal,具有狗和猫的共性。
狗和猫则作为子类,具有自己特有的特征。
1.3 继承语法
修饰符 class 子类 extends 父类 {
//.......
}
对代码以继承方式是实现:
class Animal{
public String name;
public int age;
public float weight;
public void eat(){
System.out.println(name+"吃饭");
}
public void sleep(){
System.out.println(name+"睡觉");
}
}
class Dog extends Animal{
public void bark(){
System.out.println(name+"汪汪");
}
}
class Cat extends Animal{
public void mew(){
System.out.println(name+"喵");
}
}
public class test {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
dog.name = "小王";
cat.name = "小咪";
dog.eat();
dog.sleep();
dog.bark();
cat.eat();
cat.sleep();
cat.mew();
}
}
- 子类会将父类中的成员变量或者成员方法继承到子类中了
- 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
1.4 如何访问父类成员
1.4.1 子类中访问父类的成员变量
-
子类和父类中不存在同名成员变量时,可以直接在子类中直接访问。(父类成员不被private修饰情况下)
public class Base { int a; int b; } public class Derived extends Base{ int c; public void method(){ a = 10; // 访问从父类中继承下来的a b = 20; // 访问从父类中继承下来的b c = 30; // 访问子类自己的c } }
-
子类和父类中存在同名成员变量时
class Base{ int a; int b; int c; } class Dire extends Base{ char a; char b; public void givenum(){ a = 97; b = 98; c = 99; } public void print(){ System.out.println(a+" "+b+" "+c); } } public class test { public static void main(String[] args) { Dire dire = new Dire(); dire.givenum(); dire.print(); } }
运行结果:
**可以明显看出代码赋值与打印的都是子类的成员变量,所以才会打印出a,b。**而c在子类中没用,所以访问的就是父类的
-
结论:
在子类方法中 或者 通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的,即:子类将父类同名成员隐藏了。
- 成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
- 同名情况下的访问见super关键字
1.4.2 子类中访问父类的成员方法
-
父类子类成员方法不同名时
public class Base { public void methodA(){ System.out.println("Base中的methodA()"); } } public class Derived extends Base{ public void methodB(){ System.out.println("Derived中的methodB()方法"); } public void methodC(){ methodB(); // 访问子类自己的methodB() methodA(); // 访问父类继承的methodA() // methodD(); // 编译失败,在整个继承体系中没有发现方法methodD() } }
成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
-
父类和子类成员方法名字相同
public class Base { public void methodA(){ System.out.println("Base中的methodA()"); } public void methodB(){ System.out.println("Base中的methodB()"); } } public class Derived extends Base{ public void methodA(int a) { System.out.println("Derived中的method(int)方法"); } public void methodB(){ System.out.println("Derived中的methodB()方法"); } public void methodC(){ methodA(); // 没有传参,访问父类中的methodA() methodA(20); // 传递int参数,访问子类中的methodA(int) methodB(); // 直接访问,则永远访问到的都是子类中的methodB(),基类的无法访问到 } }
- 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
- 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;如果父类和子类同名方法的原型一致,则只能访问到子类的,父类的无法通过派生类对象直接访问到。
1.5 super关键字
当子类与父类存在相同名称的成员,子类需要访问父类同名成员时,就需要使用super关键字。
java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。
class A {
public int a;
public int b;
public int c;
public A(){
a = 10;
b = 10;
c = 10;
}
public void func(){
System.out.println("父类方法");
}
}
class B extends A{
public int a = 100;
public int b = 100;
public void func(){
System.out.println("子类方法");
}
public void print(){
System.out.println(super.a);
System.out.println(this.a);
super.func();
this.func();
}
}
public class test {
public static void main(String[] args) {
B b = new B();
b.print();
}
}
运行结果:
通过子类调用super 关键字可以调用父类中的成员。即使不同名的情况也可以使用super关键字。
super是父类对象的引用是错误的,super只是一个关键字,用来向使用者说明这个成员变量是父类的,this是一个隐藏的参数,而super并不是。
即:
注意:
不能在静态方法中使用super关键字。
1.6 子类构造方法
在子类接受父类的继承以后,需要先调用父类的构造方法再调用子类的构造方法。
一个编译器默认实现的
class A {
public int a;
public int b;
public int c;
public A(){
a = 10;
b = 10;
c = 10;
}
public void func(){
System.out.println("父类方法");
}
}
class B extends A{
public int a = 100;
public int b = 100;
public void func(){
System.out.println("子类方法");
}
}
其中子类B中并没有构造方法,编译器为子类B默认实现了一个构造方法:
public B(){
super();
}
super() 相当于调用了父类的构造方法。
父类子类肯定是先有父再有子,所以在构造子类对象时候 ,先要调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。
当我们自己初始化构造方法时候,编译器不会再默认为我们生成一个构造方法,所以在自定义初始化构造方法的时候,要注意为子类构造方法加上super() ,同时super() 必须是子类构造方法第一条语句,super(…)只能在子类构造方法中出现一次,并且不能和this同时出现。
1.7 super与this
【相同点】
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
【不同点】
-
this是当前对象的引用,super不是。
-
在非静态成员方法中,this用来访问本类的方法与属性,super用来访问父类继承下来的方法与属性
-
this是非静态成员方法的一个隐藏参数,super不是一个隐藏的参数。
-
成员方法中直接访问本类成员时,编译之后会将this还原,即本类非静态成员都是通过this来访问的;在子类中如果通过super访问父类成员,编译之后在字节码层面super实际是不存在的。
-
在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现。
-
构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有。
1.8子类父类中的代码执行顺序
代码说明:
class A{
//实例代码块
{
System.out.println("父类实例代码块");
}
static {
System.out.println("父类静态代码块");
}
public A(){
System.out.println("父类构造方法");
}
}
class B extends A{
//实例代码块
{
System.out.println("子类实例代码块");
}
static {
System.out.println("子类静态代码块");
}
public B(){
super();
System.out.println("子类构造方法");
}
}
public class test {
public static void main(String[] args) {
B b = new B();
System.out.println("=============");
B b1 = new B();
}
}
运行结果:
结论:
-
优先级: 静态代码>实例代码>构造方法
-
并且创建多个对象时,静态代码只执行一次。
1.9 继承权限proteced
访问限定修饰符:
-
public : 程序中都可以访问
-
private : 只能在本类中访问
-
默认权限:也叫包访问权限,可以在该文件包低下的类中访问
-
protected: 除了拥有包访问权限,还可以在继承了该类的子类中访问。
如表格:
序号 | 范围 | private | 默认 | protected | public |
---|---|---|---|---|---|
1 | 同一个包的同一个类 | yes | yes | yes | yes |
2 | 同一个包的不同类 | yes | yes | yes | |
3 | 不同包中的子类 | yes | yes | ||
4 | 不同包中的非子类 | yes |
如何使用访问权限呢:
我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出必要的信息给类的调用者.因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public.另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private,将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 写代码的时候需要认真思考, 该类提供的字段方法到底是谁来使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).
1.10 继承方式
Java中只支持以下几种继承方式:
- 单继承
- 多层继承
- 不同类继承同一个类
但是java不支持多继承
1.11 final关键字
-
修饰变量或字段,表示常量(即不能修改)。
final int a = 10; a = 20; //编译失败
-
修饰类:表示此类不能被继承。
final public class Animal {
...
}
public class Bird extends Animal {
...
}
//编译失败
- 修饰方法:表示该方法不能被重写。
1.12 组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法,仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是is-a的关系
组合表示对象之间是has-a的关系
实现一个组合
class graphics{
public graphics(){
System.out.println("显卡");
}
}
class harddisk{
public harddisk(){
System.out.println("硬盘");
}
}
class mainboard{
public mainboard(){
System.out.println("主板");
}
}
class computer{
private graphics a;
private mainboard b;
private harddisk c;
//...............
public computer(){
System.out.println("电脑");
}
}
class lianxiang extends computer{
//..........
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择。
2.多态
2.1 多态思想
去完成某个行为,当不同的对象去完成时会产生出不同的状态,同一件事情,发生在不同对象身上,就会产生不同的结果。
例如: 都是吃,猫和狗是对象,它们一个吃的是猫粮,吃的是狗粮,即使是同一个动作吃,但是不同的对象去完成,就会出现不同的结果。
要理解多态就得想了解:向上转型和方法重写
2.2 向上转型
向上转型概念:
实际就是创建一个子类对象,将其当成父类对象来使用。
语法规则:
Animal animal = new Cat("元宝",2);
//父类 子类
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
这样的父类对象animal 虽然 new 的是子类,但是animal并不能访问子类cat的方法。
向上转型作用有三个:
-
直接赋值,也就是直接讲子类赋值给父类对象使用。
-
作为方法的参数使用
public static void eatFood(Animal a){ a.eat(); }
a可以接收 Animal 的所有子类对象以及本身 Animal 对象。
-
作为方法的返回值
public static Animal buyAnimal(String var){ if("狗" == var){ return new Dog("狗狗",1); }else if("猫" == var){ return new Cat("猫猫", 1); }else{ return null; } }
该方法可以返回 Animal 的所有子类 包括 本身 Animal。
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
2.3 方法重写
概念(看看就行)
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
class Animal {
public String name;
public int age;
protected void eat() {
System.out.println(this.name+" 吃饭!Animal");
}
}
class Cat extends Animal {
public String hair;
@Override
protected void eat() {
System.out.println(this.name+" 吃猫粮!");
}
}
重点
-
子类在重写父类的方法时,一般必须与父类方法原型一致:修饰符 返回值类型 方法名(参数列表) 要完全一致。
注意 返回值可以不同,但是返回值必须是子父类的关系
-
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected,也就是说子类的权限必须高于或者等于父类
-
父类被static、private、final修饰的方法都不能被重写。
-
重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有aet 方法, 就会编译报错, 提示无法成重写。
方法重载与方法重写:
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
绑定相关知识:
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
其中重写属于动态绑定,即使在字节码,调用的也是父类的方法而不是子类重写后的方法,而是在运行时才确定调用子类的方法。
2.4 向下转型(不安全)
public static void main(String[] args) {
Animal animal = new Bird();
Bird bird = (Bird)animal;//向下转型
bird.fly();
}
Animal 是 父类,父类引用子类对象,本身是没什么问题的这是向上转型,
anmial这个对象重新赋值给子类,就是向下转型。在这段代码中,animal本身是Bird类,重新赋值在Bird类的对象中,没有问题。
但是如果animal是Dog类型的,赋值在Brid类的对象中,就会发生异常。
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
public static void main(String[] args) {
Animal animal = new Bird();
if(animal instanceof Bird) //保证向下转型的安全性
{
Bird bird = (Bird)animal;//向下转型
bird.fly();
}
}
2.5 理解多态
通过了解向上转型,方法重写,实现一个多态。
class Animal {
public String name;
public int age;
protected void eat() {
System.out.println(this.name+" 吃饭!Animal");
}
}
class Cat extends Animal {
public String hair;
@Override
protected void eat() {
System.out.println(this.name+" 吃猫粮!");
}
public void mew() {
System.out.println(this.name+" 正在叫!");
}
}
class Bird extends Animal {
public void eat() {
System.out.println(this.name+" 吃鸟粮!");
}
public void fly() {
System.out.println("正在飞!!!!");
}
}
public class test {
public static void eating(Animal animal){
animal.eat();
}
public static void main(String[] args) {
Bird bird = new Bird();
bird.name = "mimi";
Cat cat = new Cat();
cat.name = "FIFI";
eating(bird);
eating(cat);
}
}
运行结果:
eating 是一个行为,但是子类与父类通过向上转型与重写,实现了由不同的对象去完成相同的行为来产生不同的结果。
2.6 多态的好处与缺陷
好处:
- 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
什么是圈复杂度:可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为 “圈复杂度”.如果一个方法的圈复杂度太高, 就需要考虑重构.
不使用多态:
Shape 是 Cycle,Rect,Flower的父类
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
使用多态:(避免if-else)
Shape 是 Cycle,Rect,Flower的父类
public static void drawShapes() {
// 我们创建一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
- 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
我们只需要利用继承父类并且重写父类的方法来实现。
缺陷:
代码的运行效率降低。
2.7 不要在父类的构造方法中调用重写的方法
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
该代码本意是父类去调用父类的func,但是由于子类重写了父类的func,触发了动态绑定,所以父类会调用子类的func。
所以:尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
总结:
学习完本章需要充分了解:
- 继承中如何访问父类成员以及注意事项
- 熟练掌握修饰限定符
- 理解super,this关键字
- 知晓final关键字的作用
- 了解组合
- 明白向上转型与方法重写
- 理解多态思想以及多态中的注意事项
感谢阅读,与君共勉!