前言:这篇博客是我经过网课和《Java核心技术-卷一》的阅读总结而来,基本总结了所有有关继承和多态的基础和相关注意点,还有一个Object类我会写另一篇博客来描述重写Object类方法的需要注意的点。这篇博客应该是目前为止我花时间最久的一篇博客,如果发现有让你学习的地方还请不吝惜点赞,十分感谢!
目录
继承
Java作为一门面向对象的语言,继承是其重要特性之一。Java用类来描述现实中的事物,这些事物很多时候并不是独立存在,很多时候类与类之间有许多共同点,那么是否有一种办法能让这些具有相似特征的类联系起来呢?
这即是继承的功能:抽取共性,实现代码的复用。
子类与超类
继承,在现实生活中的继承是子辈继承父辈的基因,而在Java中继承是子类(subclass)继承超类(superclass)的功能和属性。Java中用extends描述了这一种关系
举一个例子(这个例子也将贯穿整个博客),一个公司里有很多职位,如经理,BOSS,会计等等,但是他们具有一个共性——他们都是这个公司的一名工作者,因此我们可以创建一个Employee类来描述他们的共性:
class Employee{
private String name;
private double wage;//薪水
public String getName() {
return name;
}
public double getSalary() {
return wage;
}
}
class Manager extends Employee{//在这里Manager继承了超类Employee
}
经理和任何工作者一样都具有名字和薪水的属性,有都有获取这些属性的方法,有了继承之后Manager类就不用重新写这些东西了。但是经理必然也有他与众不同的地方,我们假设他与普通员工唯一的区别是他具有奖金,也理所应当的具有存储奖金的方法:
class Manager extends Employee{//在这里Manager继承了超类Employee
private double bonus;
public void setBonus(double bonus){
this.bonus = bonus;
}
}
此时, Manager类已经具有name、salary、bonus三个属性和三个方法。以此类推,之后如果我们还要创建会计类、老板类只要抽出他们的特性就可以了。
继承的使用往往遵循“is-a“原则,在这里每个经理都是(is a)Employee,所以把Manager设置为Empoyee的子类。
方法的覆盖
有时候虽然子类和超类具有同样的功能,但是功能的实现也许并不通用。例如在这里Manager由于有奖金,他的薪水就不止有普通工资了,应该还要加上薪水,此时就需要重写获取薪水的方法:
public double getSalary() {
return wage+bonus;
}
注意:这里代码并不会正常运行,因为wage是Manager继承过来的私有类型,子类无法访问,此时我们就需要使用父类获取薪水的方法获得wage,调用的语法如下:
public double getSalary() {
return super.getSalary()+bonus;
}
super就是指定父类的特殊关键字,使用 super.方法名 就可以调用父类的方法
protected修饰符
类中的成员变量往往用private修饰以对其他类密封,这对子类也是适用的,例如Manager类就不能直接操纵父类的name属性。但有时我们希望子类课以操作超类的成员变量,此时就可以用protected修饰。
class A{
private int a;
protected int b;
}
class B extends A{
void func() {
System.out.println(a);//error a为私有成员
System.out.println(b);
}
}
子类构造方法
由于子类构造方法并不能访问超类的属性,因此需要使用在子类中调用父类的构造方法。
注意:父类构造方法必须放在第一行!!!
public Manager(String name, double wage, double bonus) {
super(name, wage);//必须放在第一行,调用超类构造方法对name,wage赋值
this.bonus = bonus;
}
注意:由于如果子类使用默认构造方法时编译器会默认调用父类的默认构造方法,但是如果这时父类已经写了有参数的构造方法,那么编译器就会报错。这很容易理解,构造子类必须调用父类的构造方法给父类的属性初始化。如果父类没有不带参数的构造方法,且子类的构造方法中又没有显式调用父类构造方法,那么编译器就不知道该如何给父类属性初始化了。
多层继承和多继承
在Java中不允许多继承的存在,但可以使用接口实现(这点下一篇博客再谈)。
Java中可以使用多继承,如图,如果B继承了A,C又继承了B,C就可以使用A和B的所有可以被继承方法,同时具有A和B所有可以被继承的属性。
多态
多态就是建立在继承上的特性,我们用一些代码来展示多态:
public static void main(String[] args) {
Manager a = new Manager("经理",9600,1000);
Employee[] stuff = new Employee[3];
stuff[0] = a;
stuff[1] = new Boss("老板",18000);
stuff[2] = new Manager("副经理",7600,400);
}
在这里,我们使用Employee这个父类类型引用了子类,Manager类和Boos类发生了向上转型,这种一个对象变量指示多种实际类型的现象就被称为多态。
注意:Java中不允许向下转型,如下代码:
Manager b = stuff[0];//error,stuff本质上还是Employee类,不能向下转型
原因很简单,因为子类往往具有父类不具有的方法,如果该代码能够运行,Manage很有可能调用stuff[0]所不具有的方法,这是十分危险的!
多态需要满足三个条件:
- 必须在继承体系下
- 子类要对父类的方法进行重写
- 使用父类的引用调用重写的方法
这样我们就把所有职业(不同类型)放入了一个数组(Employee类),这是十分方便的,如果我们想要知道每个人的薪资只需要在该数组用一个for循环遍历即可。
for (int i = 0; i < stuff.length; i++) {
System.out.println(stuff[i].getSalary());
}
这段代码在运行时,虚拟机能够自动选择调用父类引用的子类方法(我们不要忘了stuff本质上时Employee类,它也可以调用Employee的getSalary()方法),这种现象被称为动态绑定。
动态绑定的内部实现
如果有一个类A,A的超类为D,他是从某一个类继承下来的,我们提前创建一个A类型对象a。
此时我们想调用a.func(int)的方法。
- 由于方法重载的存在,编译器需要找到所有名为func的方法和它的超类中访问属性为public的func方法。
- 之后编译器要找到所有func方法中参数为int的方法,由于允许类型转换,在发现没有int参数的func方法之后编译器又会去找参数int可以转化的func方法,如func(double),如果都没能找到编译器就会报错。
- 如果是private、static、final方法,编译器就可以立即知道应该调用哪个类型的方法,这种调用方式被称为静态绑定。
- 如果程序使用动态绑定调用的方法,编译器会选择最合适的类的方法,即A中没有就去A的超类D中寻找,如果还没有就去D的超类找,以此类推。由于每次的寻找太过耗时,虚拟机会提前创建一个方法表,其中存储了所有方法的名称、参数列表,之后查找方法时虚拟机只要查询这个表即可。例如在这虚拟机会先去A的方法表寻找,之后去D的方法表,以此类推。
final实现的封装类
如果我们想禁止别人继承某一个类,我们只需要把它设置为final类型即可。
同时,类中的方法也可以定义为final方法。
final class A{//A无法被继承
}
class B{
final void say(){
System.out.println("hello");//B可以被继承,但是say()无法被子类继承
}
}
人们普遍认为如果没有继承需求应当把每一个类都设置为final类,以免不必要的麻烦。
例如Java中的String就是一个final类,任何类都无法继承。
强制类型转换
我们知道int类型可以用(double) 转化为double类型,在Java中处在继承体系中的类型也可以进行强制转化。
例如:
Manager b = (Manager)stuff[0];//Employee类被强制转化为Manager类型
String str = (String)stuff[0];//错误,String和Employee不存在继承关系
强制类型转换有什么作用呢?
由于子类的引用可以直接赋给超类,但是超类的引用不能赋给子类(发生了向下转型)。如果要超类的引用赋给子类则需要强制类型转化,我先把之前的代码拿下来便于讲解。
public static void main(String[] args) {
Manager a = new Manager("经理",9600,1000);
Employee[] stuff = new Employee[3];
stuff[0] = a;
stuff[1] = new Boss("老板",18000);
stuff[2] = new Manager("副经理",7600,400);
}
在这里stuff[0]本质上还是一个Empolyee类,所以我们无法调用stuff[0]的setBonus(double bonus)方法,如果要调用则需要把suff[0]强制转化为Manager类型才能调用,如下:
((Manager) stuff[0]).setBonus(1000.0);
注意:在这里之所以可以强制类型转换是因为stuff[0]虽然是Employee类型,但存储的是Manager的引用,如果将stuff[1]强制转化为Manager,虽然可以编译,但是在运行时系统会抛出一个异常,如果没有捕获该异常程序就会终止。因此在进行类型转化时我们应当先用instanceof进行类型检查,这是每一个Java程序员应当养成的习惯。
if(stuff[0] instanceof Manager){如果内部引用类型为Manager则返回true否则为false
Manager b = (Manager) stuff[0];
}
综上:
- 只能在继承中使用类的类型转换
- 在将超类转化为子类前应当使用instanceof进行检查
实际上,通过类型转换调整对象的类型并不是一种好的做法。在我们列举的示例中,大多数情况并不需要将Employee对象转换成Manager对象,两个类的对象都能够正确地调用getSalary方法,这是因为实现多态性的动态绑定机制能够自动地找到相应的方法。
只有在使用Manager中特有的方法时才需要进行类型转换,例如,setBonus方法。如果鉴于某种原因,发现需要通过Employee对象调用setBonus方法,那么就应该检查一下超类的设计是否合理。重新设计一下超类,并添加setBonus方法才是正确的选择。请记住,只要没有捕获ClassCastException异常,程序就会终止执行。在一般情况下,应该尽量少用类型转换和instanceof运算符。
--------《Java核心技术-卷一》
抽象类
有的时候我们在建立一个祖先类时,只希望用它来派生其他类,并不需要它来实例化对象,这是我们就可以使用抽象类。
public abstract class Person {
}
这样就创建了一个抽象类。
由于抽象类无法实例化,因此我们可以不用写抽象类内部方法的具体实现,但是在抽象类中的抽象方法必须在子类中进行重写。
public abstract class Person {
private String name;
public Person(String name) {
this.name = name;
}
public abstract void func();//不需要具体实现
}
class Student extends Person{
public Student(String name) {
super(name);
}
public void func(){//必须重写
System.out.println(this.age);
}
//父类构造方法是具体方法,不用重写
}
注意:如果希望子类A不用重写抽象类的全部方法,那子类A必须也是抽象类,此时如果B继承了A,B需要重写A和A的超类的所有抽象方法。