Java中关于继承的概念介绍以及使用

4.继承

4.1.概念
  • 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。

    其中,多个类可以称为子类,单独那一个类称为父类、超类(superclass)或者基类。

  • 继承:就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。

4.2.继承的优点
  1. 提高代码的复用性。
  2. 类与类之间产生了关系,是多态的前提。
4.3.格式
  • 通过 extends 关键字,可以声明一个子类继承另外一个父类

Example:

   /*
    * 定义员工类Employee,做为父类
    */
class Employee {
    String name; // 定义name属性
    // 定义员工的工作方法
    public void work() {
        System.out.println("尽心尽力地工作");
    }
} 

    /*
     * 定义讲师类Teacher 继承 员工类Employee
     */
class Teacher extends Employee {
    // 定义一个打印name的方法
    public void printName() {
        System.out.println("name=" + name);
    }
}
       /*
     * 定义测试类
     */
public class ExtendDemo01 {
    public static void main(String[] args) {
        // 创建一个讲师类对象
        Teacher t = new Teacher();
        // 为该员工类的name属性进行赋值
        t.name = "小明";
        // 调用该员工的printName()方法
        t.printName(); // name = 小明
        // 调用Teacher类继承来的work()方法
        t.work(); // 尽心尽力地工作
    }
}
4.4.继承特点
  • 成员变量不重名:访问无影响
  • 成员变量重名:若不指定,则根据就近原则。若想访问指定变量,则应用super和this关键字来访问父类和子类重名变量

    super :代表父类的存储空间标识(可以理解为父亲的引用)。
    this :代表当前对象的引用(谁调用就代表谁)。

  • 成员方法不重名:调用无影响
  • 成员方法重名——重写(Override)
    • 方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。
    • 重写的本质还是JVM的检索机制,从子类开始检索,如果子类没有就开始向上追溯,父类没有就报错。
    • Example:
    class Fu {
        public void show() {
            System.out.println("Fu show");
        }
    }
    
    class Zi extends Fu {
        //子类重写了父类的show方法
        public void show() {
            System.out.println("Zi show");
        }
    }
    
    public class Test{
        public static void main(String[] args) {
            Zi z = new Zi();
            // 子类中有show方法,只执行重写后的show方法
            z.show(); // Zi show
        }
    }
    
  • 构造方法:
    • 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
    • 构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。
    • 子类的构造方法中默认有一个 super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。
  • 子类的构造方法中,super()this()必须是构造方法中的第一条语句,没有的话默认是super();,只会有一个,如果看不到,是因为有一个默认的隐式super(),并且创建对象时会向上追溯,直至超类Object()
  • Example:
    public class Demo {
        public static void main(String[] args) {
            PrimaryStudent primaryStudent = new PrimaryStudent(666,888,"hello");
            System.out.println(primaryStudent.psVar);
        }
    }
    class Person {
        public Person() {
            System.out.println("Person类的无参构造");
        }
    }
    class Student extends Person {
        int sVar;
        String sVarString;
        public Student() {
            System.out.println("Student类无参构造");
        }
        public Student(int sVar) {
            //super()  //缺省的super()
            System.out.println("Student int构造方法");
            this.sVar = sVar;
        }
        public Student(int sVar, String sVarString) {
            this(sVar);
            System.out.println("Student int String构造方法");
            this.sVarString = sVarString;
        }
    }
    class PrimaryStudent extends Student {
        int psVar = 10;
        public PrimaryStudent(int psVar, int sVar, String sVarString) {
            super(sVar, sVarString);
            System.out.println("PrimaryStudent类的 三参构造");
            this.psVar = psVar = 100;
        }
    }
    
    //输出结果:
        Person类的无参构造
        Student int构造方法
        Student int String构造方法
        PrimaryStudent类的 三参构造
        100
    
    • 解释:从 main 函数开始, main 函数的所在类首先要进行类加载,加载完毕后 main 方法栈帧进栈,创建 PrimaryStudent 对象之前先要进行类加载,而 JVM 发现 PrimaryStudent 类的父类是 Student 类, Student 的父类是 Person 类, Person 的父类是 Object 类,因此,JVM方法区里类的加载顺序为 Object类->Person类->Student类->PrimaryStudent类 ,加载完毕后,在堆里试图创建 PrimaryStudent 类的对象,创建之前,先看 PrimaryStudent 的构造方法,发现了一个显式的 super(sVar, sVarString) 方法,向上追溯 Student 类的构造方法,而在 Student 类的两参数构造方法里,有一个 this(sVar) ,因此继续在该类里寻找,在上边有个 Student(int sVar) 构造方法,继续执行,而在该方法里,没有 this()super() 的构造方法,所以,会有一个隐式的缺省的super() 构造方法,继续向上追溯到 Person 类里的无参构造方法, Person 类的父类为 Object 类,同样是空的无参构造方法,但是对输出没有影响。因此从上到下运行下来,运行顺序为 Object()->Person()->Student(int sVar)->Student(int sVar, String sVarString)->PrimaryStudent(int psVar, int sVar, String sVarString)->System.out.println(primaryStudent.psVar) ; 输出即为输出结果。
4.5补充protected访问权限
4.5.1.protected四个等级的访问层次
  • 同类中,可以任意访问
  • 同包的子类或其它类中,始终都是可以访问该类的protected成员
    • 在同包的子类或其他类中,创建该类对象,可以访问该类的protected成员
    • 在同包的子类中,创建当前类子类对象,可以访问该类的protected成员
    • 在同包的子类或其他类中,创建该类不同包的子类对象时,可以访问访问该类的protected成员
  • 不同包的子类中,分情况
    • 创建父类对象,无法访问,该类的protected成员
    • 创建当前类子类对象,可以访问,该类的protected成员
    • 在该子类中创建另一个子类的对象,无法访问,该类的protected成员
    • 不同包的子类中,只有在子类中创建自身子类对象,才能访问从父类那里继承过来的protected成员,其他情况都不行,如创建父类对象,子类中创建别的子类对象等
  • 不同包的其他类中,始终都不可以访问该类的protected成员
    • 创建该类对象,不可以访问该类的protected成员
    • 创建该类的子类对象,不可以访问该类的protected成员
4.5.2.总结protected的访问权限
  • 简单版本
    • 同类中,同包中,都可以任意访问(创建父类对象,子类对象都可以)
    • 不同包的时候,必须在子类的那个类中,创建当前子类的对象才可以访问
      • 创建别的子类对象,不能访问
      • 创建父类对象,不能访问
    • 子类只能在自己的作用范围内访问自己继承的那个父类protected域
      • 而无法到访问别的子类(同父类的亲兄弟)所继承的protected域和父类对象的protected域
    • 说白了,子类继承自父类的protected成员,必须自己来处理,爸爸帮不了,兄弟姐妹更帮不了
      • 这是出于安全机制考虑,避免滥用受保护机制
4.5.3.要用对象名调用一个继承自父类protected成员时,应该怎么思考能不能访问?
  • 首先考虑是否同类,同包,如果同类或同包,创建父类对象,子类对象,必然都可以访问
  • 如果不同包,考虑是否是子类中,如果不在子类中,无论创建什么对象,必然都不可以访问
  • 如果在不同包的子类中,考虑调用protected成员的对象性质
    • 如果使用的对象是父类对象,必然不可以访问
    • 如果使用的对象是子类对象,仍然要考虑
      • 如果该子类对象就是当前类的对象,可以访问
      • 如果该子类对象不是当前类的对象,不可以访问

4.5.4.protected设置的这么复杂,有什么意义?
  • 如果没有继承,那么public、private两个权限修饰符足够我们使用了
  • 但是有了继承后,如果类中某个成员,非常有价值,我们希望这个成员总是被子类使用,而不会被滥用
    • 使用protect限制该成员,能够保证子类拥有对自己继承的protected成员最大的权限
      • 想想父母的财产,也不会随便就交给一个陌生人。而是希望交给子女吧?
      • 想想子女继承了父母的财产,它们就拥有了最大的权限,七大姑八姨就不能使用这个遗产
      • 但是子女如果允许,仍然可以让别人使用他继承过来的遗产(成员)
4.6.类加载机制详解
4.6.1.类加载概述
  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段
  • 类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
  • Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定。
    • 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对java,简单的可以理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。
    • 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在java中,几乎所有的方法都是后期绑定的。
4.6.2.类加载过程
  1. 加载
    • 加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
      • 通过一个类的全限定名来获取其定义的二进制字节流。
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
      • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
    • 注意,二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生(JSP应用)等。相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
    • 对于.class文件,查看其十六进制文件结构,例如用win hex打开,会发现所有class文件的前4个字节都是cafe babe,不同文件类型的前4个字节被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 8950 4e47。
    • 类加载器:
      • 类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。
      • 站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
        • 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
        • 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
      • 站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
        • 启动类加载器:Bootstrap ClassLoader,最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
        • 扩展类加载器:Extension ClassLoader,扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
        • 应用程序类加载器:Application ClassLoader,Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
        • 自定义类加载器:User ClassLoader
      • 应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
        • 在执行非置信代码之前,自动验证数字签名。
        • 动态地创建符合用户特定需要的定制化构建类。
        • 从特定的场所取得java class,例如数据库中和网络中。
      • 事实上当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息,应用服务器也大都使用了自定义的ClassLoader技术。
      • 这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
      • 双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
      • 使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
  2. 验证
    • 验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
      • 文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
      • 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
      • 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
      • 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
  3. 准备
    • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
      • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
      • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。3
  4. 解析
    • 解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
    • 解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。
      • 类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
      • 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,
  5. 初始化
    • 初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造()方法的过程。
    • 这里简单说明下()方法的执行规则:
      • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
      • ()方法与实例构造器()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
      • ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成()方法。
      • 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成()方法。但是接口鱼类不同的是:执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
      • 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

你可能感兴趣的:(Java)