Java的继承(深刻版)

前言

前文咱们了解了面向对象的三大特征:封装、继承、多态。html

那么在Java中是如何展示继承的特性呢?对于子类继承于父类时,又有什么限制呢?数据结构

在此解答这些问题以后,咱们再了解下类的加载过程,加深对继承的了解。ide

(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)this

引入继承

假如咱们有两个类:生物类、猫类。spa

生物类:.net

class Animal{  
    private String name;  
    public void setName(String name){  
        this.name = name;  
    }  
    public String getName(){  
        return this.name;  
    }   
}

猫类:code

class Cat{  
    private String name;  
    private String sound;  
    public void setName(String name){  
        this.name = name;  
    }  
    public void setSound(String sound){  
        this.sound = sound;  
    }  
    public String getName(){  
        return this.name;  
    }   
    public String getSound(){  
        return this.sound;  
    }  
}

咱们知道,猫也是属于生物中的一种,生物有的属性和行为,猫按理来讲也是有的。但此时没有继承的概念,那么代码就得不到复用,长期发展,代码冗余、维护困难且开发者的工做量也很是大。htm

继承的概念

继承就是子类继承父类的特征和行为,使得子类对象(实例)具备父类的实例域和方法,或子类从父类继承方法,使得子类具备父类相同的行为。

简单来讲,子类能吸取父类已有的属性和行为。除此以外,子类还能够扩展自身功能。子类又被称为派生类,父类被称为超类。对象

在Java中,若是要实现继承的关系,可使用以下语法:blog

class 子类 extends 父类{}

继承的基本实现

继承的基本实现以下:

class Animal{  
    private String name;  
    public void setName(String name){  
        this.name = name;  
    }  
    public String getName(){  
        return this.name;  
    }   
}  
class Cat extends Animal{}  
​  
public class Test{  
    public static void main(String[] args){  
        Cat cat = new Cat();  
        cat.setName("猫");  
        System.out.println(cat.getName());  
    }  
}

运行结果为:

咱们能够看出,子类能够在不扩展操做的状况下,使用父类的属性和功能

子类扩充父类

继承的基本实现以下:

class Animal{  
    private String name;  
    public void setName(String name){  
        this.name = name;  
    }  
    public String getName(){  
        return this.name;  
    }   
}  
class Cat extends Animal{  
    private String sound;  
    public void setSound(String sound){  
        this.sound = sound;  
    }  
    public String getSound(){  
        return this.sound;  
    }   
}  
​  
public class Test{  
    public static void main(String\[\] args){  
        Cat cat = new Cat();  
        cat.setName("NYforSF")  
        cat.setSound("我不是你最爱的小甜甜了吗?");  
        System.out.println(cat.getName()+":"+cat.getSound()); 
    }  
}

运行结果为:

NYforSF:我不是你最爱的小甜甜了吗?

咱们能够看出,子类在父类的基础上进行了扩展,并且对于父类来讲,子类定义的范围更为具体。也就是说,子类是将父类具体化的一种手段

总结一下,Java中的继承利用子类和父类的关系,能够实现代码复用,子类还能够根据需求扩展功能。

继承的限制

1. 子类只能继承一个父类

为何子类不能多继承?举个栗子。

class ACat{  
 public void mewo(){...}  
}  
class BCat{  
 public void mewo(){...}  
}  
class CCat extends ACat, BCat{  
 @Override  
 public void mewo(){...?}  //提问:这里的mewo()是继承自哪一个类?  
}

虽然说Java只支持单继承,可是不反对多层继承呀!

class ACat{}  
class BCat extends ACat{}  
class CCat extends BCat{}

这样,BCat就继承了ACat全部的方法,而CCat继承了ACat、BCat全部的方法,实际上CCat是ACat的子(孙)类,是BCat的子类。

总结一下,子类虽然不支持多重继承,只能单继承,可是能够多层继承

2. private修饰不可直接访问,final修饰不可修改

private修饰

对于子类来讲,父类中用private修饰的属性对其隐藏的,但若是提供了这个变量的setter/getter接口,仍是可以访问和修改这个变量的。

class ACat {  
    private String sound = "meow";  
    public String getSound() {  
        return sound;  
    }  
    public void setSound(String sound) {  
        this.sound = sound;  
    }  
}  
​
class BCat extends ACat {  
}  
​  
public class Test {  
    public static void main(String[] args) {  
        BCat b = new BCat();  
        b.setSound("我不是你最爱的小甜甜了吗?");  
        System.out.println(b.getSound());  
    }  
}

final修饰

父类已经定义好的final修饰变量(方法也同样),子类能够访问这个属性(或方法),可是不能对其进行更改

class ACat {  
    final String sound = "你是我最爱的小甜甜";  
    public String getSound() {  
        return sound;  
    }  
    public void setSound(String sound){  
        this.sound = sound;  //这句执行不了,会报错的  
    }  
}  
​  
class BCat extends ACat {  
}

总结一下,用private修饰的变量能够经过getter/setter接口来操做,final修饰的变量就只能访问,不能更改

3. 实例化子类时默认先调用父类的构造方法

在实例化子类对象时,会调用父类的构造方法对属性进行初始化,以后再调用子类的构造方法。

class A {  
    public A(){  
        System.out.println("我不是你最爱的小甜甜了吗?");  
    }  
    public A(String q){  
        System.out.println(q);  
    }  
}  
​  
class B extends A {  
    public B(){  
        System.out.println("你是个好姑娘");  
    }  
}  
​  
public class Test {  
    public static void main(String[] args) {  
        B b = new B();  
    }  
}

运行结果为:

我不是你最爱的小甜甜了吗?  
你是个好姑娘

从结果咱们能够知道,在实例化子类时,会默认先调用父类中无参构造方法,而后再调动子类的构造方法。

那么怎么调用父类带参的构造方法呢?只要在子类构造方法的第一行调用super()方法就好。

class A {  
    public A(String q){  
        System.out.println(q);  
    }  
}  
​  
class B extends A {  
    public B(){  
        super("我是你的小甜甜?");  
        System.out.println("你是个好姑娘");  
    }  
}  
​  
public class Test {  
    public static void main(String\[\] args) {  
        B b = new B();  
    }  
}

运行结果为:

我是你的小甜甜?
你是个好姑娘

在子类实例化时,默认调用的是父类的无参构造方法,而若是没有父类无参构造方法,则子类必须经过super()来调用父类的有参构造方法,且super()方法必须在子类构造方法的首行

总结一下,Java继承中有三种继承限制,分别是子类只能单继承、父类中private修饰的变量不能显式访问和final修饰的变量不能改变,以及实例化子类一定会先调用父类的构造方法,以后才调用子类的构造方法。

类是怎么加载的?

(此处只是粗略介绍类加载的过程,想了解更多可参考《深刻理解Java虚拟机》)

类加载过程包括三个大步骤:加载、链接、初始化

这三个步骤的开始时间仍然保持着固定的前后顺序,可是进行和完成的进度就不必定是这样的顺序了。
简:类加载过程.jpg

  1. 加载:虚拟机经过这个类的全限定名来获取这个类的二进制字节流,而后在字节流中提取出这个类的结构数据,并转换成这个类在方法区(存储类结构)的运行时数据结构
  2. 验证:先验证这字节流是否符合Class文件格式的规范,而后检查这个类的其父类中数据是否存在冲突(如这个类的父类是否继承被final修饰的类),接着对这个类内的方法体进行检查,若是都没问题了,那就把以前的符号引用换成直接引用
  3. 准备:为类变量(static修饰的变量)分配内存(方法区)并设置类变量初始值,而这里的初始值是指这个数据类型的零值,如int的初始值是0;
  4. 解析:在Class文件加载过程当中,会将Class文件中的标识方法、接口的常量放进常量池中,而这些常量对于虚拟机来讲,就是符号引用。此阶段就是针对类、接口、字段等7类符号引用,转换成直接指向目标的句柄——直接引用
  5. 初始化:这阶段是执行static代码块和类构造器的过程,有小伙伴可能会疑惑类构造器不是默认static的吗?详情请看这个博客:再议Java中的static关键字

总结一下,类加载的过程当中,首先会对Class文件中的类提取并转换成运行时数据结构,而后对类的父类和这个类的数据信息进行检验以后,为类中的类变量分配内存而且设置初始值,接着将Class文件中与这个类有关的符号引用转换成直接引用,最后再执行类构造器。

并且咱们能够从第二步看出,在加载类的时候,会先去检查这个类的父类的信息,而后再检查这个类的方法体,也就是说,在加载类的时候,会先去加载它的父类

结语

初学Java的时候知道这些概念,但只是浅尝而止。如今跟着Hollis大佬的《Java 工程师成神之路!》,从新回顾这些知识的时候,发现若是本身只是像之前同样片面了解,那岂不是没有成长?因此在写文章的过程当中,尝试写得更加深刻且尽可能易懂。固然,本人水平有限,若有不正之处,欢迎指正。

若是本文对你有帮助,请点一个赞吧,这对我来讲是最大的动力~

参考资料:

Java 继承(extends)详解

相关文章
相关标签/搜索