Java学习笔记(五)——继承
前言
该部分为面向对象编程(中级部分)中:继承部分的知识
继承
为什么需要继承?
继承可以解决代码复用,让我们的编程更加接近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的字类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可。
- 代码的复用性提高
- 代码的扩展性和维护性提高
继承的基本语法
1 | class 子类 extends 父类{ |
继承的细节
- 1、子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能在子类直接访问直接访问,要通过父类提供的公共方法去访问。(注意,如果是默认的属性和方法,如果在同包下也可访问,具体可以看访问修饰符的范围表)
- 2、子类必须调用父类的构造器,完成父类的初始化。
也就是说,当子类在创建对象的时候,会默认调用父类的无参构造器。这是因为子类的无参构造器中会隐藏一个super动作:
1 | public class Sub extends Base{ |
- 3、当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的每一个构造器中都用super去指定父类的哪个构造器完成对父类的初始化工作,否则编译不会通过。
1 | //父类 |
- 4、如果希望指定去调用父类的某个构造器,则显示的调用一下:super(参数列表)
- 5、super在使用时,只能在构造器中使用,且必须放在构造器的第一行 。(因为必须先有父类,才有子类)
- 6、super()和this()都只能放在构造器的第一行,因此这两个方法不能共存在一个构造器中。注意,这里this调用指的是不能用this来调用其他构造器,以下情况是可以的:
1 | public Pc(String cpu,int memory,int disk,String brand){ |
这里也体现了:继承涉及的基本思想,父类的构造器完成父类属性的初始化,子类构造器完成子类属性的初始化。
- 7、Java所有类都是Object的子类,Object是所有类的基类。
- 8、父类构造器的调用不限于直接父类!将一直往上追溯直到Object类(顶级父类)。
- 9、子类最多只能继承一个父类(指直接继承),即Java中是单继承机制。
思考如何让A类继承B类和C类?——让B类继承C类。
- 10、不能滥用继承,子类和父类之间必须满足is-a的逻辑关系。
继承的本质
当子类继承父类,创建子类对象的时候,内存中到底发生了什么?——当子类对象创建好后,建立查找的关系。
构造过程:
- 1、先根据继承关系加载我们的类,顺序从顶级父类到子类
- 2、加载完后会堆里面分配空间
- 3、如果有字符串则会指向常量池的某一个地址,基础数据类型直接存放在堆中
- 4、把son在堆中的地址返回给主方法的对象引用
访问过程:
1 | package com.learn.extend_; |
练习例子
下面代码输出为何?
例1
1 | package com.learn.extend_; |
结果是:
1 | a |
例2
1 | package com.learn.extend_; |
结果是:
1 | 我是A类 |
super的基本语法
1、访问父类的属性,但不能访问父类的private属性 【案例】super.属性名
2、访问父类的方法,但不能访问父类的private方法【案例】super.方法名(参数列表)
3、访问父类的构造器:super(参数列表);只能放在构造器的第一句,且只能出现一句。
super的使用细节
super给编程带来的便利:
- 1、调用父类的构造器的好处(分工明确,父类属性由父类初始化,子类属性由子类初始化)
- 2、当子类中有和父类中成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果。
1 | public class A{ |
1 | public class B extends A{ |
如果本类有cal()函数:
1 | public class B extends A{ |
- 3、super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员;如果多个基类中都有同名的成员,使用super访问遵循就近原则。当然也需要遵守访问权限的相关规则。
super和this的比较
no. | 区别点 | this | super |
---|---|---|---|
1 | 访问属性 | 访问本类中的属性,如果本类中没有此属性则从父类中继续查找 | 从父类开始查找属性 |
2 | 调用方法 | 访问本类中的方法,如果本类中没有此方法则从父类中继续查找 | 从父类开始查找方法 |
3 | 调用构造器 | 调用本类构造器,必须放在构造器的首行 | 调用父类构造器,必须放在构造器的首行 |
4 | 特殊 | 表示当前对象 | 子类中访问父类对象 |
方法重写
方法覆盖(重写)就是子类有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的那个方法。
方法重写的细节
方法重写也叫方法覆盖,需要满足以下条件:
- 1、子类的方法的参数、方法名称,要和父类方法完全一样。
- 2、子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类(反过来不行)。比如父类返回类型是Object,子类方法返回类型是String,例如以下两个也是重写:
1 | //父类方法 |
- 3、子类方法不能缩小父类方法的访问权限。意思就是
1 | //父类方法: |
这里的主要原因依赖于:里氏代换原则
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
把里氏代换原则解释得更完整一些:在一个软件系统中,子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。
方法重写和重载区别
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载(overload) | 本类 | 必须一样 | 类型、个数或者顺序至少有一个不同 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一样 | 必须相同 | 子类重写的方法,返回的类型和父类返回的类型一致,或者是其子类 | 子类不能缩小父类方法的访问范围 |