JavaSE基础(八)---继承的魅力

 ==================================

本章节将介绍Java中的“继承”与“多态”,两者与前一章中的“封装”作为面向对象程序的三大特性,都是非常重要的内容。简单回顾以下,封装是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

==================================

目录

一、继承

1、为什么要继承?

2、继承的含义

3、继承的语法 

4、访问父类成员 

4.1子类中访问父类的成员变量

4.2、子类中访问父类成员方法 

5、super关键字* 

二、继承中该注意的构造方法 *

1、子类构造方法(super调用)

 2、super和this之间的渊源

三、继承中代码块的执行顺序

四、继承方式

五、final关键字 

六、继承与组合


一、继承

1、为什么要继承?

在之前我们了解到,在Java中我们可以通过类来对现实世界中一些抽象的、具有一定属性和行为的实体进行描述。

类经过实例化后产生的对象,可以用来表示现实中的一些实体。

但现实世界错综复杂,一些定义的类之间可能会存在相同的属性或行为,那在设计程序时就需要考虑。

比如:猫、狗都作为动物(共性:都是动物),具有相同的特性,比如毛色、年龄、性别等等。

用Java语言来进行描述,就会设计出:

//定义Cat类
class Cat{
    public String name;
    public String hair;
    public int age;

    public void run(){
        System.out.println(this.name+"正在跑");
    }

    public void mew(){
        System.out.println(this.name+"喵喵叫");
    }
}
//定义Dog类
class Dog{
    public String name;
    public String hair;
    public int age;

    public void run(){
        System.out.println(this.name+"正在跑");
    }

    public void wan(){
        System.out.println(this.name+"汪汪叫");
    }
}

 从上面两个定义类,可以看出狗类和猫类都具有相同的属性,方法,只是存在些许的差异(叫声)。

🤔那我们是否可以把它们之间的共性抽取出来,不用再重复写相同的属性呢❓


2、继承的含义

面相对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用

【继承机制】:

        继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性 的基础上进行扩展,增加新功能,这样产生新的类,称派生类(也可称为子类)。

        继承的原有类,称为父类(又可称为基类、超类)

        继承呈现了面向对象程序设计的层次结构, 体现了 由简单到复杂的认知过程。

        继承主要解决的问题是:共性的抽取,实现代码复用

=========================================================================

例如:狗和猫都是动物的一种,那么我们可以把猫和狗作为动物的特性抽取出来,然后将其继承给猫和狗类。

上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。

从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态(后序讲)。


3、继承的语法 

语法格式:class A(父类) extends B(子类){}

在Java中如果要表示类之间的继承关系,需要借助extends关键字。

以上面的狗和猫为代码示例:

//定义父类
class Animal{
    public String name;
    public String hair;
    public int age;
    public void run(){
        System.out.println(this.name+"正在跑");
    }

}
//定义Cat这个子类
class Cat extends Animal{

    public void mew(){
        System.out.println(this.name+"喵喵叫");
    }
}
//定义Dog这个子类
class Dog extends Animal{

    public void wan(){
        System.out.println(this.name+"汪汪叫");
    }
}

public class TestDemo3 {
    public static void main(String[] args) {
        //实例化猫和狗
        Cat cat=new Cat();;
        Dog dog=new Dog();

        // dog类和cat类中并没有定义任何成员变量
        // name和hair属性肯定是从父类Animal中继承下来的
        System.out.println(dog.name);
        System.out.println(cat.hair);

        //run()方法也是从父类继承下来的
        dog.run();
        cat.mew();
    }
}

 运行结果:

由以上的代码可以得知,子类继承了父类的属性和方法,通过实例化狗和猫,可以访问父类(Animal)内的属性和方法,当然也可以访问自身的。                

 【注意】:

  •  子类会继承父类的成员变量和成员方法(包括静态成员)
  • 如果一个子类里面没有特属于自己的属性或行为,那就没有继承父类的必要,毕竟一个空的子类,还不如之间用一个父类来的实在。主要还是要体现子类之间的不同。

4、访问父类成员 

4.1子类中访问父类的成员变量

(1)在子类继承了父类后,其子类内部会有父类的成员变量和方法,在子类中可以直接访问父类里的继承下来的成员变量和成员方法.

class Animal{
    public String name;
    public String hair;
    public int age;
}

class Dog extends Animal{
    public String sex;
    public void set(){
//访问子类从父类继承下来的name、hair、age
        name="小黄";        
        hair="yellow";
        age=3;
//访问子类自身的sex
        sex="boy";
    }
}

(2)如果子类和父类中都有同名的成员变量或成员方法,那在子类中访问同名的成员时,会优先访问子类自身的成员.


(3)如果父类里的成员变量名a是子类里没有的,那当我们访问a时,自然是访问父类的;那如果父类和子类里都没定义过一个成员变量b时,你又去访问这个凭空出现的b,自然会编译错误!

(4)要是当子类和父类存在同名的成员变量时,不想访问子类的成员变量,反而要访问父类的同名成员变量,就得用到super关键字(后文会介绍)。 

 总结:

  • 如果访问的成员变量子类中有,优先访问自己的成员变量。
  • 如果访问的成员变量与父类中成员变量同名,则优先访问自己的,即:子类将父类同名成员隐藏了。
  • 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。

说白了,成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。比如:你和你父亲各自有一辆车,平时使用时你肯定优先用自己的,如果自己车坏了拿去维修,而自己又打算开车出远门,才会考虑使用父亲的。


4.2、子类中访问父类成员方法 

在子类中访问父类的成员方法跟前面的子类访问父类成员变量差不太多。

访问不同名时,则访问对应的方法名。

访问同名的方法时,则遵循就近原则,访问子类的。

而要想访问父类的,那么父类与子类同名的方法之间得构成“重载”,再根据所调用的参数列表不同,去进行方法的调用。


(1)访问同名的成员方法(重载*)

 通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法传递的参数选择合适的方法访问,如果没有则报错;

如果父类和子类同名方法的原型一致(重写-后 面讲),则只能访问到子类的,父类的无法通过子类对象直接访问到。


 (2)访问同名的成员方法

【注意】:

        通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,否则编译报错。 


(3)访问不同名的成员方法

此处跟前面的访问成员变量一致,简单带过。

通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,否则编译报错。


5、super关键字* 

子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?

直接访问是无法做到的,Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。

super代表父亲对象的引用,这种说法不太准确!

子类实例化后的对象,里面有一部分是从父类继承过来的,super是对那一部分的引用!

代码示例:

class A{
    public int a=1;
    public int b=2;
    public int c=3;
    public void func(){     // 与父类中func()构成重写(即原型一致)
        System.out.println("调用父类的成员方法");
    }
    public void add(){      // 与父类中methodA()构成重载
        System.out.println("add!");
    }
}
class B extends A{
   public int a=4;          // 与父类中成员变量同名且类型相同
   public int b=5;

    public void func(){
        System.out.println("调用子类的成员方法");
   }

   public void add(int a,int b){
       System.out.println("add="+(a+b));
   }

   public void show(){
       // 访问父类的成员变量时,需要借助super关键字
       // super是获取到子类对象中从父类继承下来的部分
       System.out.println("访问父类的成员变量a:"+super.a);

       // 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
       add();
       add(3,5);

       //如果要在子类中访问与父类构成重写的方法(返回类型,方法名,参数列表都一样的方法),可以用super关键字访问
       func();//访问的是子类的方法,就近原则
       super.func();//访问父类的方法
   }
}
public class TestDemo4 {
    public static void main(String[] args) {
        B b=new B();
        b.show();
    }
}

运行结果:

在子类方法中,如果想要明确访问父类中成员时,借助super关键字即可。

【注意】:

        1、绝对不能在静态方法内使用super关键字,super关键字(当前对象父类的引用)本质上也是依赖于对象的,与this关键字一样,不能再静态方法内使用。 

        2、super关键字是子类方法里面使用的。

        3、super关键字还可以用来调用父类的构造方法。(后文会介绍!)


二、继承中该注意的构造方法 *

1、子类构造方法(super调用)

众所周知,作为类,无论是子类还是父类,怎么说都得有构造方法吧!

那我们在实例化子类的时候,是只调用构造子类方法呢?还是子类和父类都有调用构造方法呢?

熟话说,“先有父,后有子”。在我们实例化子类对象时,会调用子类的构造方法,而其中会先调用完父类的构造方法,才会完成子类构造方法的调用。

但需要注意的时,我们在子类的构造方法,内得先调用父类的构造方法(如果父类的构造方法是无参的或者是编译器自动默认提供的,那子类构造方法内可以不用调用父类的构造方法,编译器也会自动默认提供),如果不是的话,得通过super关键字,对父类的构造方法进行调用。调用完后子类的构造方法内才会继续执行你所写的语句。

代码示例:

class Base{
    public String namem;
    public int age;

    public Base() {
        System.out.println("父类的构造方法");
    }
}
class Derived extends Base{
    public Derived() {
        super();
//super()会调用子类的构造方法
//只要父类构造方法是无参的或没有写(编译器自动默认的),super()其实可以不用写
//其生命周期只存在于本次实例化当中,只出现一次
//而且super()必须得在构造方法的第一条语句,毕竟得先完成父类构造方法的调用
        System.out.println("子类的构造方法");
    }
}
public class TestDemo5 {
    public static void main(String[] args) {
        Derived derived=new Derived();
    }
}

//运行结果:
//父类的构造方法
//子类的构造方法

 从上面代码可知,在子类的构造方法内,是通过super关键字来调用父类的构造方法

要是父类内没有写任何有关父类的构造方法或无参的构造方法,子类构造方法内也是会先执行父类的构造方法,再执行子类自己的构造方法。毕竟,子类对象中成员是有两部分组成的,一部分是:父类继承下来的,另一部分是:子类新增加的部分

即使我们没有写super关键字调用编译器也是会默认提供一个,我们可以认为super()被隐式定义。这也是为什么我们刚开始接触继承时,即使没有定义子类或父类的构造方法,程序依旧可以跑起来的原因,因为编译器默认为我们提供了。

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


(1)调用父类内带参数的构造方法。

【总结】:

        1、在子类中通过super关键字调用父类的构造方法。

        2、若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用(编译器会默认提供),即调用父类构造方法。

        3、父类构造方法是带有参数的,此时编译器不会再给子类生成默认的构造方法,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。

        4、在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句。

        5、super(...)只能在子类构造方法中出现一次(其生命周期只是本次实例化的对象),并且不能和this同时出现。

        this()在子类构造方法内是调用子类其他被重载的构造方法,也必须在构造方法内的第一行,和super()调用父类构造方法冲突了。【注意:是this()调用构造方法不能和super()同时存在,而不是this引用当前对象的属性,this访问当前对象还是可以用的】

我们在写构造方法时,通过IDEA自带的功能可以很方便的直接进行定义。

(1)按住键盘上Alt+Insert,会弹出Generate!

 

 (2)按住Shift键,选中想初始化的成员,再点击OK即可。

下面会着重介绍this和super之间的区别!


 2、super和this之间的渊源

super和this都可以在成员方法中用来访问:成员变量和调用其他的成员方法,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?

this和super之间的相同点:

        1、它们都只能在非静态方法中使用,(静态方法不是依赖对象的)用来访问非静态成员方法和字段。

        2、在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在super()和this()二者都是构造方法内的第一行,彼此发生冲突。


this和super之间的不同点:

        1、this是当前对象的引用(当前对象即调用实例方法的对象),是依赖于对象。而super是子类对象中从父类继承下来那一部分成员的引用

    

         2、在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性。

        3、. this是非静态成员方法的一个隐藏参数,super不是隐藏的参数。

        4、构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有。


三、继承中代码块的执行顺序

在前面,我们学过了有关不同代码块之间的执行顺序,静态代码块优先于实例代码块,而实例代码块又优先于构造方法。

简单回顾以下它们之间的执行顺序。

class Student{
    public String name;
    public int age;
    public static String sex;
 
    {
       this.name="李华";
       this.age=17;
        System.out.println("实例化代码块");
    }
 
    static {
        sex="boy";
        System.out.println("静态代码块");
    }
    
    public Student(String name,int age){
        this.name=name;
        this.age=age;
        System.out.println("调用构造方法");
    }
 
    public void show(){
        System.out.println(this.name+" is "+sex+" 今年"+this.age);
    }
}
public class TestDemo1 {
    public static void main(String[] args) {
        Student student=new Student("小明",19);
        student.show();
    }
}

 【注意】:

        1. 静态代码块先执行,并且只执行一次,在类加载阶段执行。

        2. 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行。


而继承中代码块执行顺序相比于之前代码块执行顺序有些许差异。

class Base{
    public String name;
    public int age;
    static {
        System.out.println("父类静态代码块");
    }
    {
        System.out.println("=============");
        System.out.println("父类实例代码块");
    }
    public Base() {
        System.out.println("父类构造方法");
    }
}
class Derived extends Base{
    static {
        System.out.println("子类静态代码块");
    }
    {
        System.out.println("=============");
        System.out.println("子类实例代码块");
    }
    public Derived() {
        System.out.println("子类构造方法");
    }
}
public class TestDemo5 {
    public static void main(String[] args) {
        Derived derived=new Derived();
    }
}


继承执行顺序还需注意的是:

        静态代码块的执行只会进行一次,当我们实例化第二个子类对象时,父类和子类的静态代码块不会再执行。

 通过分析执行结果,得出以下结论:

1、父类静态代码块优先于子类静态代码块执行,且是最早执行

2、父类实例代码块和父类构造方法紧接着执行

3、子类的实例代码块和子类构造方法紧接着再执行

4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行


四、继承方式

在我们的平常的生活当中,事物的方方面面基本都是由许多种类构成,每一种种类可以划分多中对象出来。而每一个划分出来的对象又可以再进行划分,往往复复。事物之间的关系错综复杂。

比如动物:

 而作为某一动物的品种,又可以将他们划分出毛色,斑纹等等特征。

但在Java中只支持以下几种继承方式:

1、单继承

class A{

}

class B extends A{

}

2、多层继承

class A{

}
class B extends A{

}
class C extends B{
//B是C的父类,A是B的父类,可以理解成A是C的爷类
//C也有继承A类的成员
}

3、不同类继同一个类

class A{

}
class B extends A{

}
class C extends A{

}

4、多继承(一个类不可以继承多个类)

//不能这么继承
class A{

}
class B{

}
class C extends A,B{

}

【注意】:不支持多继承,一次只能继承一个,(后续学到接口,可以利用接口来实现多继承)。

越是学到后面更深的层次,我们在写一些项目,可能会写很多个类,类之间的关系会随着你写的类的数量而变得更复杂,如果我们想对一些类的继承(一般我们不出现超过三层的继承关系)进行限制,就可以用到final关键字来修饰类。


五、final关键字 

final关键可以用来修饰变量、成员方法以及类。

1、被final修饰的变量或成员变量,会表示成为不变的量(常量)不能被修改.

final int a = 10;
a = 20; // 编译出错

2、修饰类:表示此类不能被继承

final class Animal {
...
}
class Bird extends Animal {
...
}
// 编译会出错,无法进行继承

3、 修饰方法:表示该方法不能被重写(后序介绍) 


六、继承与组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法 (诸如 extends 这样的关键字), 不过就是在一个类里面又实例化了其他类的对象。

继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物

组合表示对象之间是has-a的关系,比如:汽车

以汽车为例,汽车无非就是由许多零件构成的。

汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的 。

 

// 轮胎类
class Tire{
// ...
}
// 发动机类
class Engine{
// ...
}
// 车载系统类
class VehicleSystem{
// ...
}
class Car{
Tire tire=new Tire(); // 可以复用轮胎中的属性和方法
Engine engine=new Engine(); // 可以复用发动机中的属性和方法
VehicleSystem vs=new VehicleSystem(); // 可以复用车载系统中的属性和方法
// ...
}
// 奔驰是汽车
class Benz extend Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}

组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用 组合。


本章有关继承的一部分内容到这里就结束了,后续会对继承的多态进行介绍。

如果本文对各位兄弟们姐妹们,有所帮助,希望能点个👍支持一下。

要是有什么错误或模糊的点,希望在座的大佬们可以指点指点。

让我们一起努力,一起加油。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星河栀染

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值