JAVA核心技术笔记总结--第5章 继承

第五章 继承

继承是指基于已有的类构造一个新类,继承已有类就是复用(继承)这些类的成员变量和方法。并在此基础上,添加新的成员变量和方法,以知足新的需求。java不支持多继承java

5.1 类、超类和子类

5.1.1 定义子类

下面是由继承Employee类来定义Manager类的格式,关键字extend表示继承。数组

public class Manager extend Employee{
  //添加方法和成员变量
}

extend 代表定义的新类派生于一个已有类。被继承的类称为超类、基类或父类;新类称为子类或派生类。ide

在Manager类中增长了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:this

public class Manager extend Employee{
  private double bonus;
  ...
  public void setBonus(double bonus){
    this.bonus = bonus;
  }
}

尽管在Manager类中没有显式的定义getNamegetHireDay等方法,但Manager类的对象却可使用它们,这是由于Manager类自动继承了超类Employee中的这些方法。此外,Manager类还从父类继承了name、salaryhireDay这3个成员变量。从而每个Manager对象包含四个成员变量。spa

5.1.2 覆盖方法(Override)

子类除了能够定义新方法外,还能够覆盖(重写)父类的方法。覆盖要遵循 "两同两小一大"的原则:设计

两同指:code

  • 方法名相同。
  • 形参列表相同,即形参个数相同,各形参类型必须对应相同(新方法的形参类型不能够是父类方法形参类型的子类型)。

两小指:对象

  • 子类方法返回值类型应与父类方法返回值类型相同或是父类方法返回值类型的子类型。
  • 子类方法声明抛出的异常应与父类方法声明抛出的异常类型相同或是其子异常。

一大指:子类方法的访问权限应与父类方法的访问权限相同或比父类访问权限大。继承

今后处能够看出,覆盖和重载的区别:接口

方法重载对返回值类型和修饰符没有要求;实例方法能够重载父类的类方法,而方法覆盖要求返回值类型变小,修饰符变大且只能为实例方法,知足上述条件的静态方法,不叫覆盖,只是屏蔽。

尤为要指出的是:知足覆盖条件的方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法,不然会引起编译错误。

编译时,编译器会为每一个类创建一张方法表,表中包含的方法有:子类中定义的全部方法(包括私有方法,构造器,类方法和公有方法)。同时包含从父类继承的非私有方法,包含重载的方法,应该也包含类方法,但不包含被子类覆盖的方法。此外,编译器也会为每一个对象分配一个成员变量表,其中包含本类中定义的全部变量(成员变量和类变量),以及父类中非私有的变量。

当子类覆盖了父类方法后,子类对象将没法访问父类中被覆盖的方法,但子类方法中仍能调用父类中被覆盖的方法,调用格式为:

  • "super.方法名" (被覆盖的是实例方法),仅能够在子类非静态方法使用。编译时,编译器会根据super去查找父类的方法表,并替换为匹配方法的符号引用。
  • "父类类名.方法名"
public class Manager extend Employee{
  private double bonus;
  ...
  public double getSalary(){
    return super.getSalary() + bonus;
  }
}

若是父类方法的访问权限是private,则子类没法访问该方法,且没法覆盖该方法,当子类中定义了一个与父类 private 方法知足上述”两同两小一大“原则的方法时,并无覆盖该方法,而是在子类中定义了一个新方法。例如:

class BaseClass{
  //test()方法是private访问权限,子类不可访问
  private void test(){...}
}
class SubClass extends BaseClass{
  //此处不是覆盖,因此能够增长static关键字
  public static void test(){...}
}

从而,子类能够继承的内容有

  • 父类的成员变量(对于父类的私有成员变量,子类对象依然分配有内存,仅由于子类对象不能直接访问)。
  • 父类的非私有方法。

不能继承的有:

  • 父类中被覆盖的方法,不能继承。
  • 父类的私有方法。
5.1.3 super限定

super用于限定该对象调用它从父类继承获得的实例变量或方法。super不能出如今static修饰的方法中

若是在构造器中使用super,则super用于初始化从父类继承的成员变量。

对于非私有域而言,若是子类定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。默认状况下,子类里定义的方法直接访问的同名实例变量是子类的实例变量。但能够在使用"super.实例变量名"来访问被隐藏的父类的实例变量。例如:

class BaseClass{
  public int a;
}
class SubClass extends BaseClass{
  public int a = 7;
  public void test1(){
    System.out.println(a);
  }
  public void test2(){
    System.out.println(super.a);
  }
}

若是子类中没有与父类同名的成员变量,那么在子类实例方法中访问该成员变量时,无须显式使用super或父类名。若是某个方法中访问了名为a的实例变量,但没有显式指定调用者,则系统查找a的顺序为:

  • 查找该方法中是否有名为a的局部变量
  • 查找当前类是否有名为a的成员变量
  • 查找直接父类中是否包含了名为a的成员变量,依次上溯全部父类,直至Object类,若是没有找到,则报错。

当程序建立一个子类对象时,系统不只会为子类中定义的实例变量分配内存,也会为从父类继承的全部实例变量分配内存,即便子类中定义了与父类中同名的实例变量。

因为子类的实例变量仅是隐藏了父类中同名的实例变量,不是覆盖。因此,访问哪一个实例变量是由调用者的类型决定的。在编译时,由编译器分派。从而,会出现以下情形:

class Parent{
  String tag = "parent";
}
class Child extends Parent{
  private String tag = "child";
}
public class Test{
  public static void main(String[] args){
    Child c = new Child();
    //报错,不可访问私有变量
    out.println(c.tag);///////1
    //输出:parent
    out.println(((Parent)c).tag);//////2
  }
}

当程序在代码1处试图访问tag时,因为调用者为子类,而子类的tag是私有变量,不能在外部被访问.。而代码2处访问的是父类的tag。此与方法的覆盖不一样,方法覆盖具备多态性。

综上,当子类中隐藏了父类的实例变量,或子类中覆盖了父类的方法时,父类被隐藏的实例变量,或被覆盖的方法,仅能在子类的方法里面经过super来访问,在其余类的方法中没法访问。

5.1.4 子类构造器

因为子类不能访问父类的私有域,因此须要利用父类的构造器来初始化这部分私有域,能够经过super实现对父类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。

不论是否使用super显式的调用了父类构造器,子类构造器总会调用一次父类构造器,子类调用父类构造器分以下几种状况:

  • 子类构造器执行体的第一行使用super显式调用了父类构造器,系统将根据super调用传入的实参列表调用父类构造器。
  • 子类构造器执行体的第一行代码使用this显式调用本类中重载的其余构造器,系统将根据this调用里传入的实参列表调用本类的另外一个构造器。执行本类中另外一个构造器时即会调用父类构造器。
  • 子类构造器执行体中既没有使用super调用,也没有使用this调用,系统将会在执行子类构造器以前,隐式调用父类的无参数构造器。(因此,定义类时,最好提供一个无参数构造器)。

综上,调用子类构造器时,父类构造器总会在子类构造器以前执行。以此类推,执行父类构造器时,系统会再次上溯执行其父类的构造器.....从而,建立任何java对象,最早执行的老是Object类的构造器。当父类有构造器,但不存在默认构造器时,程序出错。

实际上初始化块是一个假象,源文件编译时,初始化块中的代码会被"还原"到每一个构造器中,且位于构造器全部代码的前面。(super()调用的后面,this调用的前面??)。

在建立子类对象时,各模块的初始化流程以下:

  1. 在未执行任何初始化语句时,系统已经为子类的全部成员变量,以及父类的全部成员变量分配了内存空间,并默认初始化,当子类中存在成员变量隐藏父类成员变量状况时,这两个成员变量都会被分配内存。
  2. 而后初始化父类(先执行初始化语句和初始化块,再执行父类构造器,若子类构造器中有super调用父类构造器语句,则调用指定的父类构造器,不然调用父类的默认构造器。
  3. 按序执行子类的显式初始化语句或初始化块。
  4. 执行子类构造器内的语句。

示例以下:

public class Test{
  {
    a = 6;
  }
  int a = 9;
  public static void main(String[] args){
    //输出为9;若调换两个初始化语句,输出为6
    out.println(a);
  }
}

在首次使用子类时,类的初始化流程为:

若父类未初始化,则执行父类的类初始化,在执行父类初始化时,也要先判断其父类是否已初始化,若未初始化,则先执行其父类的初始化,……直至Object类,最后才执行本类的类初始化。

5.2 多态

Java中引用变量有两个类型:编译时类型和运行时类型。编译时类型由变量定义的类型决定,运行时类型由该变量引用对象的类型决定。若是编译时类型和运行时类型不一致,就可能出现多态(PolyMophism)。

由于子类是一种特殊的父类,因此Java容许把一个子类对象直接赋给一个父类变量,由系统自动完成,无须类型转换,称为向上转型。但不能将父类对象直接赋给子类变量。

两个同类型的变量,若一个引用父类对象,另外一个引用的是子类对象,且子类中覆盖了父类的方法,那么两个变量同时调用此方法时,将呈现出不一样的行为特征,这被称为多态。

当父类变量引用子类对象时,因为变量的编译时类型为父类,因此只能调用父类的方法和成员变量,以及子类中覆盖的方法(动态链接、动态绑定),不能调用子类中新定义的、以及父类中存在,但子类重载后的只属于子类方法。符号引用在编译时分派。

与方法不一样的是,对象的实例变量不具有多态性。老是访问编译时类型中定义的实例变量。

在继承链中对象方法的调用存在一个优先级:????

this.show(O),super.show(O),this.show((super)O),super.show((super)O)

警告,在java中,子类数组的引用能够赋给父类数组的引用,而不须要强制类型转换。例如:

Manager[] managers = new Manager[10];
//将它赋给Employee[] 数组是彻底合法的:
Employee[] staff = managers;

由于managers[i]是一个Manager,能够赋给Employee变量,可是编译器不容许让数组元素再引用其余类型的对象,不容许数组元素引用的类型不一致。以下面语句将会抛出ArrayStoreException异常:

staff[0] = new Employee(...);

由于若是容许staff[0] 和 managers[0] 都引用了这个Employee 对象,那么managers[0].getBonus()变的不合理。

即数组元素只能引用相同类型的对象,不然编译器会报告异常。

理解初始化流程和多态性的一个极好的例子:

public class Mytest {
    public static void main(String[] args){
        Dervied td = new Dervied();
        td.test();
    }
}
class Dervied extends Base {
    private String name = "dervied";
    public Dervied() {
        super();
        tellName();
        printName();
    }
    public void tellName() {
        System.out.println("Dervied tell name: " + name);
    }
    public void printName() {
        System.out.println("Dervied print name: " + name);
    }
}
class Base {
    private String name = "base";
    public Base() {
        tellName();
        printName();
    }
    public void tellName() {
        System.out.println("Base tell name: " + name);
    }
    public void printName() {
        System.out.println("Base print name: " + name);
    }
    public void test(){
        tellName();
    }
}
输出结果为:
//前两句输出,说明Base类中的tellName()和printName()仍然调用的是Dervied类的方法
//同时也说明,初始化流程为:先调用父构造器,而不是先执行显式初始化语句:private String name = "dervied";
Dervied tell name: null
Dervied print name: null
//3、四两句说明调用完Base类构造器后,初始化流程为:先执行显式初始化语句,而后再执行构造器中的语句。
Dervied tell name: dervied
Dervied print name: dervied
//最后一句再次验证了,Base类中的tellName()和printName()已经完全被覆盖
Dervied tell name: dervied

5.3 理解方法调用

假设要调用x.f(args),下面是调用过程的详细描述:

  1. 编译器查看对象的编译时类型类型和方法名。假设隐式参数 x 的编译时类型为 C 类。须要注意的是:有可能存在多个名字为 f,可是形参列表不一样的方法。例如可能存在方法 f(int) 和方法 f(String)。编译器从方法表中列举出全部方法名为f的方法,包括父类中非私有的且名为 f 的方法。至此,编译器已得到全部可能被调用的候选方法。

  2. 接下来,编译器将查看调用方法时提供的实参类型。若是在全部名为 f 的方法中存在一个与提供的参数类型彻底匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。因为容许类型转换(int 能够转换成 double,子类转换成父类,等等),因此过程很复杂,若是编译器没有找到与参数类型匹配的方法,或者发现通过类型转换后有多个方法与之匹配,就会报错。至此,编译器已肯定须要调用的方法,会将其替换为对应方法的符号引用。

5.4 阻止继承:final 类和方法

不容许继承的类被称为 final 类。在定义类时使用 final 修饰符,就代表这个类是 final 类,不能被继承,声明格式以下:

public final class Executive{
  ...
}

类中特定的方法也能够被声明为 final。被 final 修饰的非私有方法不能被子类覆盖,可是方法仍然能够被重载(由于重载不考虑修饰符)。子类中能够定义父类中被final修饰的私有方法。

域也能够被声明为 final,final 域代表域为常量。当一个类被声明为 final 时,只是其中的方法变为 final,不包括域。

5.5 强制类型转换

引用变量只能调用编译时类型中的方法,不能调用运行时类型中定义的方法。若是须要调用运行时类型中的方法,必须进行类型转换,将它强制转换成运行时类型。引用类型转换的语法和基本类型的强制转换相同。可是,若是父类变量实际类型是超类时,强制类型转换会引起ClassCastException。所以,能够在进行类型转换以前,先使用instanceof检测是否能进行转换:

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

若是检测返回false,编译器就不会进行转换。

综上所述:

  1. 只能在继承层次内进行类型转换。
  2. 在将超类转换成子类以前,应该使用 instanceof 进行检查。

只有在使用子类特有的方法时才须要进行类型转换。建议尽可能少用到类型转换和 instaceof 运算符。

5.6 instanceOf运算符

instanceOf运算符的前一个操做符一般是一个引用类型变量,后一个操做符是一个类或接口instanceOf用于判断前面引用变量的运行时类型是不是后面类或者其子类的实例。若是是,则返回true,不然返回false

使用instanceOf运算符时,要求前面的操做数的编译时类型与后面操做数的类型相同,或者前者是后者的父类,或者前者是后者的子类。不然会引发编译错误。

5.7 继承与组合

5.7.1 使用继承的注意点

容许子类继承父类,子类能够直接访问父类的成员变量和方法。可是继承破坏了父类的封装性:子类能够经过覆盖的方式改变父类方法的实现,从而致使子类能够恶意篡改父类的方法。而且,父类方法调用被覆盖方法时,调用的实际上是子类的方法。

为了保证父类良好的封装性,设计父类一般应该遵循以下规则:

  • 尽可能隐藏父类的内部数据,尽可能把父类的全部成员变量设置为private
  • 不要让子类能够随意访问、修改父类的方法。父类中做为辅助的方法应设置为private。父类中须要被外部类调用的方法,必须设置为public,若是不但愿子类重写该方法,能够用final修饰;若是但愿父类的某个方法被子类重写,但不但愿被其余类自由访问,则可使用protected来修饰。
  • 尽可能不要在父类构造器中调用将被子类重写的方法,容易出现逻辑混乱。
5.7.2 组合

组合是将待复用的类当成另外一个类的一部分,即在新类中,定义一个待复用类的私有成员变量,以实现对类方法和成员变量的复用。例如:

class Animal{
  public void breath(){
    System.out.println("吸气,吐气。。。");
  }
}
class Bird{
  private Animal a;
  public void breath(){
    a.breath();
  }
}

组合和继承均可以复用指定类的方法以及成员变量。继承更符合现实意义。组合能够避免破坏父类的封装性。

5.8 受保护的访问

若是但愿超类中的某些方法容许被子类访问,或容许子类的方法访问超类的某个域,此时,须要将这些方法或域声明为 protected

可是,将父类的域声明为 protected后,子类的方法只能访问子类对象的 protected 域,不能访问父类对象的 protected 域。这种限制有助于避免滥用受保护机制,使得子类只能得到受保护域的权利。

概括 java 用于修饰成员变量或方法的控制可见性的4个访问修饰符:

  1. 仅对本类可见 -- private
  2. 对全部类可见 -- public
  3. 对本包和全部子类可见 -- protected
  4. 仅对本包可见 -- 默认,不添加修饰符

类的可见性:

  1. 对全部类可见 -- public
  2. 仅对本包可见 -- 默认,无修饰符。
  3. 对于内部类而言,本包和子类可见 -- protected
  4. 对于内部类而言,本类可见 -- private
相关文章
相关标签/搜索