小白学Java之 继承


前言

  懂了类与对象之后,就可以开始学习继承啦。继承可以节省大量代码的重复,如果类与类之间是is-a关系,那就可以放心继承。
  本文基于你掌握了基本类与对象的知识,慢慢向你展现继承思想,如何添加新的字段和方法,使新类描述更加精准
。移步换景,逐步深入,慢慢涉及到多态、抽象类。相信你按顺序读文章,认真理解,就可以基本掌握继承语法。


一、 父、子类

1.  先有一个类

  这个类是起源(其实Object才是,手动狗头),以这个类为基础,可以派生出许多子类,子类亦可派生子类,子子孙孙无穷尽也,直至子孙万代!
  打住!打住!打住!胡扯什么?一般子类层次不超过三代,子孙万代靠谁去维护?通过类new出来千千万万个对象还差不多!嘿嘿,话不多说,就是想告诉你子类不延续超过三代,开始学习之路吧~~~
  这里构造了一个普通员工类,有名字、薪水、雇佣日期等属性,一个涨薪的功能。但是公司里的经理还有额外的奖金。这时候需要一个新类Manager,比Employee多 1 个功能。可以看出,Manager和Employee存在着 is - a 关系。专业来说“ is - a 关系”是替代原则,程序中出现超类对象的任何地方,都可以用子类替换。


2.  实现子类

2.1 extends 关键字

  Manager通过extends关键字来表示继承,如下:

public class Manager extends Employee{
//add your methods and files
}

  注释部分就可以来丰满子类。可以看出,作为子类,它比父类拥有更多功能。有点长江后浪推前浪,世上今人胜古人的味道。所以我们一般把较为普通的方法 or 较为普遍的属性,都放在父类中。较为特殊和独特的,设置为子类。这种抽取手段,在面向对象编程中十分普遍。


2.2 新增字段和方法

  经理要有奖金,我们要给这个字段增加方法

  虽然Manager中没有显式定义getName和getHireday等方法,但是也可以对Manager对象使用这些方法。因为Manager自动继承了Employee的方法,大大减少了代码的复用。
  毫无疑问,对于Employee的name、salary、hireday这些字段,Manager同样拥有。因此,拓展子类的时候,我们只需要考虑特有的部分就足够。


2.3 覆盖方法

  Manager对工资的计算和Employee不同,需要加上奖金,所以需要对Employee的getSalary方法进行重写(也叫做“覆盖方法”)。如何重写呢?


看看重写(覆盖方法)的要求:
  1. 方法名相同
  2. 函数的参数相同(个数、类型)
  3. 返回值相同
  4. 仅仅限于父子类之间


这里我们和重载进行对比:
  1. 方法名相同
  2. 函数的参数不同
  3. 返回值不做要求
  4. 不仅父子类之间,还可以在同一个类中。


  既然明白了要求,我们可以试着来完成这个方法了:
你可能这么想:

    public double getSalary(){
        return salary + this.bonus;
    }

  但是并不可以,Manager不可以直接访问Employee的字段。salary在Employee类中是由private修饰的。private是类访问修饰符,通过private实现数据的封装,确保代码的安全性。既然连访问数据的资格都没有,这个方法自然是pass。


 既然访问不到数据,那么利用get方法就是咯。

    public double getSalary(){
    	double baseSalary = getSalary();
        return baseSalary + this.bonus;
    }

  这个想法相比于前面的思路已经有所改善,可还是有一个致命缺陷。getSalary()是在调用自身。没错!它不停使用自己的方法。程序陷入了一个死循环。我们就必须想办法去使用父类的getSalary()。


2.4 super关键字

  需要说的是,super代表父类实例的引用(不准确)。这就不得不让人联想到this了。其实这两个的用法还是非常类似的。


this的用法:
  1. 指示隐式参数的引用
  2. 该类的其他构造方法


super的用法:
  1. 调用父类的方法
  2. 调用父类的构造方法


  紧密的相同点:在调用构造方法时,调用的构造语句必须做为另一个构造器的第一条语句。
  至于为什么说super代表父类实例的引用不准确,是因为不可将super赋值给另一个对象变量。它只是调用超类方法的特殊关键字。


接下来完成getSalary方法:

很简单,调用父类方法即可。


2.5 子类构造方法

  例子最后,我们完成子类的构造方法
  需要注意的就是super调用父类构造方法时,必须在第一句。展示一下Manager的完整代码:

  在对子类进行构造时,会先完成父类的构造。
来看一个例子:

  结果为:

把例子复杂一些:

  结果为:

  首先执行的是静态代码块,再执行父类的普通代码块和构造代码块,最后是子类。概括起来就是朗朗上口的一句话:

由父及子,静态先行。


2.5.1 private修饰的构造方法

 可否这样写?在这里插入图片描述
  private是包访问修饰符,所以自然不能在类外new,private可以修饰构造方法,但只可以在当前类中进行调用,这是一种单例设计模式。


3.  实例测试

  写完了子父类,当然要试试效果:
在这里插入图片描述

  运行结果如下:

  staff[1]和staff[2]输出的是普通员工工资,因为其中存储的都是Employee对象,staff[0]输出的工资带有奖金,因为它是Employee对象。在for each循环中,我把Employee[ ]数组中的引用都赋值给了Employee e。因为e可以引用Employee的对象,也可以引用Manager的对象。在引用相关对象时,调用的就是对象对应的方法。
  一个对象可以指示多种实际类型的现象叫做多态(一个事物表现为多种形态),在运行时可以选择对应的方法,叫做动态绑定,也是我们马上要讲解的。


二、 多态

  Java程序语言设计中,对象变量是多态的。就像上面的Employee既可以引用本身类的对象,也可以引用其所有子类的对象。


1.  向上转型

  就使用来说,向上转型是子类引用赋值给父类。

Manager boss = new Manager("小张", 50000, 1988,12, 8);
Employee e = boss;

  上面的boss是一个Manager对象,此时Employee类的 e 引用指向子类的引用 boss。这种现象就是向上转型,打个比方,所谓向上,像一种成长,儿子终有成为爸爸的一天。很多儿子都会成为爸爸,这种现象十分常见,多态的体现在Java的编程中也是同样十分常见。


2.  向下转型

  明白了向上转型就说说向下转型吧,思路十分相似,它就是把父类的引用赋值给子类。
  看代码:

Manager boss = (Manager)staff[0];
Manager boss = (Manager)staff[1];//ERROR

  第一行是可以通过的,而第二行就会发生编译错误。在Java中,每个变量对象都有一个类型。类型描述了这个变量所引用和能够引用的对象类型(父类引用既可以引用自己也可以引用子类)。
  将一个值存入变量时,编译器会检查我们是否承诺过多。子类变量赋值给超类,这完全可以。但是超类引用赋值给子类时,我们就 “承诺过多”,无法通过。强制类型转换可以解决这个问题,正如第一行的方法,staff[0]是一个Employee类引用,无法赋值给子类Manager类引用,那么在赋值前就把Employee类引用转换为Manager类引用。
  而第二行会ERROR呢?因为我们像编译器 “撒谎”,staff[1]时Employee类引用,里面存的值仍然是Employee类对象,我们的“承诺”与真实情况不符,编译会抛出ClassCastException错误。


2.1   操作符instanceof

  操作符instanceof可以避免ClassCastException。

A instanceof B

  意思是A是否引用了B这个变量,返回值是boolean类型。通过instanceof来处理上面的error:

if (staff[1] instanceof Manager){
    Manager boss = (Manager)staff[1];
}

由此可以总结:
  1.在继承层次内进行强制类型转换
  2.超类强制类型转换为子类之前,用instanceof进行检查。


3.  动态绑定

  前面的向上or下转换都是为这个知识铺垫,动态绑定也是多态的体现。我们来回顾一下前面的问题:Employee e为啥在运行时可以选择对应的方法?
  程序运行并采用动态绑定调用方法(f)时,虚拟机会调用引用对象的实际类型对应的方法。比如 x 的实际类型是B,B是A的子类。若B中定义了方法(f),就执行此方法。否则,就去A类中搜索。


3.1  方法表

  方法表是虚拟机的地图。
  搜索是相当浪费时间的,我们每new一个对象,都会在堆上开辟空间。这块内存里有一个方法表,其中列出了所有的方法的签名和要调用的实际方法。这样调用方法时,虚拟机看看表就可以了。
  拿之前的Employee和Manager来看:
Employee:
  getName( )  - > Employee.getName( )
  getSalary( )  - > Employee.getSalary( )
  getHireday( )  - > Employee.getHireday( )
  raiseSalary(double)  - > Employee.raiseSalary(double)

Manager:
  getName( )  - > Employee.getName( )
  *getSalary( )  - > Manage.getSalary( )
  getHireday( )  - > Employee.getHireday( )
  raiseSalary(double)  - > Employee.raiseSalary(double)
  *setBonus(double) - > Manager.setBonus(double)

  Manager拥有Employee的所有方法(继承)。在调用e.getSalary( )时,获取e的实际类型的方法表。接着查找定义了getSalary方法的类,最后执行对应方法。
  注意点:子类方法的可见性不能低于父类方法的可见性。(private < default < protectted < public)


三、 终止继承:final

  我们有时候会想终止继承,阻止人们对这个类进行定义:String类就是final修饰的。类中的方法也可以通过final修饰,这样,子类就不可以重写这个方法。
  特别注意:final修饰类的时候,里面的所有方法会默认带上final,而类中的属性不包括。
  将方法或类由final修饰的主要原因是:确保他们不会在子类中改变语义,不允许子类添乱。还是拿String类举例,这意味着任何人不可定义String的子类,确保了String引用的一定是一个String类对象。


1.  内联

  早期Java中,有的程序员为了避免动态绑定的内存开销而使用final。如果一个方法没有被覆盖而且很短,编译器就能进行优化处理。例如:boss.getName( )就会处理为boss.name。因为CPU处理当前指令时,分支会干扰指令的预测,内联的意义在此。若getName在别的类中重写,内联也会取消。
  虚拟机的即时编译器处理能力很强,明确地知道类之间的继承关系,明白是否有其他类覆盖了指定方法。若是方法很简短、被频繁调用、没有覆盖方法,即时编译器就会进行内联处理。


四、 抽象类

  抽象类既是为了被继承。抽象类更为基础,是我们对后面创造特定实例地特征提取。就像Employee和Student,都是Person。每个Worker都有name等属性,getName等对应方法也可以放在最高层次。
  接下来拓展我们的类层次来加入Person类和Student类。再加入一个getDescription方法,它返回一个人地简短描述。
  在Employee和Student实现这个方法是容易的。Person类中如何设计?Person.getDescription( )返回一个null?不过还有更好的办法。


1. abstract关键字

  包含抽象方法地类也必须是抽象的。

  除了抽象方法之外,抽象类还可以包含字段和具体方法。看代码:

  可见,抽象方法中没有任何内容,说白了,抽象方法充当占位方法的角色,具体都在子类中实现。
抽象类有两种拓展方法:

1.子类中保留抽象类中部分或所有抽象方法仍未定义,这样子类也要为抽象类。
2.子类定义抽象类中全部方法,如此,子类不再抽象。

  抽象类是不能实例化的,我们无法new Person 。不过Person可以引用非抽象子类的对象。

Person p = new Student();

2. 拓展抽象类

  接下来完成子类Student:
  还有子类Employee:


2. 实例调试

在这里插入图片描述
  执行结果:

  p.getDescription调用的都是Employee和Student中的方法,而不是Person中,这里发生了动态绑定。有人问是否可以省略Person中的方法?当然不可以,这样p就调用不到getDescription。


小编的话

  继承还有很多知识,学到这里才刚刚算得上入门,再继续学习下去,多敲敲代码,我相信你会有不一样的理解。

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值