入门Java继承与多态

前言

前面我们详细介绍了 封装 的特性,今天我们来介绍剩下的两大特性——继承和多态。也欢迎各位大佬对文章错误的部分斧正。

封装的部分,可回顾笔者之前的博客

详解类和对象——入门Java封装-CSDN博客

什么是继承

通过前面的学习我们知道,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 + "正在喵喵喵");
    }

}
  1. 子类会将父类中的成员变量或者成员方法继承到子类中了
  2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
  3. 父类静态的属性和方法会被继承,可以用类名.属性/方法名访问,子类和父类的类名均能访问。注意,本文不讨论访问修饰限定符的问题,这不是本文要讨论的重点,本文默认访问修饰符均为public,在任何位置均能访问到

关于第三个点笔者对能被继承这个说法的准确性不是很确定,但实验下来是可以运行的,姑且就先这么理解

我在Animal类中加入如上代码,运行测试用例,运行通过

继承后,如何对属性和方法进行访问

父类访问子类

静态属性和方法可用类名直接点出

非静态的属性和方法则需要先创建对象,再由对象点出。

这点是符合静态属于类,非静态属于对象的

子类访问父类

子类和父类不存在同名成员属性/方法

可以直接用属性/方法名访问。但这种方法有弊端,在存在重名的时候不加注意会有意想不到的结果

子类和父类存在同名成员变量

前面我们在封装中学过一个 this 的关键字,在这里我们在介绍一个和它及其类似的关键字——super。该关键字主要作用:在子类方法中访问父类的成员。

super的注意:1. 只能在非静态方法中使用 2. 在子类方法中,访问父类的成员变量和方法。

this访问子类的属性/方法,super访问父类的属性/方法。什么都不写默认是this。支持重载,在子类和父类方法名相同时可能会涉及重写,但在子类访问父类方法时不会发生。重写会在多态那部分在介绍

这就是问什么说不加修饰直接访问可能会有意想不到的结果

总结

  1. 如果访问的成员属性/方法子类中有,优先访问自己的成员属性/方法。
  2. 如果访问的成员属性/方法子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
  3. 如果访问的成员变量与父类中成员属性/方法同名,则优先访问自己的
  4. this和super可实现具有针对的访问,笔者建议无论如何添加上,添加代码的可控性

成员属性/方法访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。

再谈构造方法

前面,我们在封装中介绍过构造方法,在继承中我们要再谈构造方法

首先,有一个原则,子类在构造时要先帮父类完成构造

和this()调用其他构造方法类似,在子类构造方法中,可以用super()调用父类的构造方法,先帮助父类完成构造。

我们知道,子类中我们不写任何的构造方法,会默认有一个无参的构造方法,在这个无参构造方法中,会默认调用父类无参的构造方法,这些部分编译器会自动完成。如果在子类或者父类中提供任何构造方法,编译器不在提供默认的构造方法,需要程序员手动补齐剩下的逻辑

子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整。

  1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
  2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
  3. 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句。
  4.  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 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体

抽象类也是类,内部可以包含普通方法和属性,甚至构造方法

注意

  1. 抽象类不能实例化
  2. 抽象方法不能是 private 的
  3. 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
  4. 抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
  5. 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
  6. 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量

抽象的意义

我们发现,即使是普通的类/方法,也能实现抽象类的功能,那我们为什么需要使用抽象类呢?

更多的是我们需要用抽象类,增加一层编译器的校验,有问题能及时报错,增加开发效率

接口

我们将抽象类再一步抽象,就会得到接口,接口是一种多个类的公共规范,是一种引用数据类型

语法

接口的定义格式与定义类的格式基本相同,将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 关键字
 

结语

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!


 

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值