类的继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点。每个子类只有一个父类。
-
继承的特点
Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称其为基类、超类。子类和父类的关系,是一种一般和特殊的关系。例如水果和苹果的关系,苹果继承了水果,苹果是水果的子类,则苹果是一种特殊的水果。
因为子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。
语法格式如下:
修饰符 class SubClasss extends SuperClass { //类定义部分 }
从上面的语法格式来看,定义子类的语法非常简单,只需要在原来的类定义上增加extends SuperClass即可,即表明该子类继承了父类的SuperClass类。
Java使用extends作为继承的关键字,extends关键字在英文中是拓展的意思,而不是继承。这个关键字很好地体现了子类和父类的关系,子类是父类的扩展,子类是一种特殊的父类。 子类只能从被扩展的父类获得成员变量、方法和内部类(包括内部接口、枚举)、不能获得构造器和初始化块。
例:
public class Fruit { public double weight; public void info(); { System.out.println("我是一个水果!重:"+weight+"g!"); } }
接下来再定义子类Apple
public class Apple extends Fruit { public static void main(String [] args) { //创建一个Apple对象 Apple a=new Apple(); //Apple对象本身没有weight成员变量 //因为Apple的父类有weight成员变量,也可以直接访问Apple对象的weight成员变量。 a.weight=56; //调用Apple对象的info()方法 a.info(); } }
上面的Apple基本是个空类,它包含了一个main()方法,但程序中创建了Apple对象之后,可以访问该Apple对象的weight实例变量和info()方法,这表明Apple对象也具有了weight实例变量和info方法,这就是继承作用。
-
重写父类的方法
子类扩展了父类,子类是一个特殊的父类…例:
public class Bird { //Bird类的fly方法 public void fly() { System.out.println("我在天空自由翱翔.."); } }
下面再定义一个Ostrich类,这个类扩展了Bird类,重写了Bird类的fly()方法。
public class Ostrich extends Bird { //重写Bird类的fly()方法 public void fly() { System.out.println("我只能在地上奔跑...") } public static void main(String [] args) { var os=new Ostrich(); //执行Ostrich对象的fly方法,将输出..... os.fly(); } }
执行上面的程序,os.fly()执行的是Ostrich类的fly()方法。
这种子类包含父类与父类同名方法的现象被称为重写,也被称为方法覆盖。遵循“两同两小一大”规则,两同:方法名相同、形参列表相同:两小;子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或者相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
如下代码会发生错误:
class BaseClass { public static void test() { //... } class Subclass extends BaseClass { public void test() { //... } } }
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法,如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
如果父类方法具有private访问权限则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。例如下面的代码是完全正确的。
class BaseClass { //test()方法是private访问权限,子类不可访问该方法。 private void test() { } } class SubClass extends BaseClass { //此处不是方法重写,所以可以增加extends关键字。 public static void test(){} }
方法重载和方法重写在英语中分别是overload和override,关于重载和重写的区别:重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。它们之间的联系很少,除二者都是发生在方法之间,并要求方法名相同之外,没有太大的相同之处。
-
super限定(实例方法)
如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法。接上例:
public void callOverrideMethod() { // 在子类方法中通过super显示调用父类被覆盖的实例方法 super.fly(); }
借助callOverrideMethod()方法的帮助,就可以让Ostrich对象即可以调用自己重写的fly()方法,也可以借用Bird类中被覆盖的fly()方法(调用callOverrideMethod()方法即可)。
super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法一样,super也不能出现在static修饰的方法中。static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,因而super限定也就失去了意义。
如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
如果子类里定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。在正常情况下,子类里定义的方法直接访问该实例变量会访问到子类中定义的实例变量,无法访问到父类中被一隐藏的实例变量。在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量,代码如下。
class BaseClass { public int a=5; } public class SubClass extends BaseClass { public int a=7; public void accessOwner() { System.out.println(a); } public void accessBase() { //通过super来限定访问从父类继承得到的a实例变量 System.out.println(super.a); } public static void main(String[] args) { var sc=new SubClass(); sc.accessOwner(); sc.accessBase(); } }
上面程序的BaseClass和Subclass中都定义了名为a的实例变量,则Subclass的a实例变量将会隐藏BaseClass的a实例变量。当系统创建了Subclass对象时,实际上会为Subclass对象分配两块内存,一块用于存储在Subclass类中定义的a实例变量,一块用于存储子类的a实例变量、一块是父类的a实例变量,使用super.a可以访问继承父类的实例变量。
如果子类里没有包含和父类同名的成员变量,那么在子类实例方法中访问该成员变量时,则无须显示使用super或父类名作为调用者。如果在某个方法中访问名为a的实例变量,但没有显示指定调用者,则系统查找a的方法为:
-
查找该方法中是否有名为a的局部变量
-
查找当前类中是否包含名为a的成员变量
-
查找a的直接父类是否包含名为a的成员变量,知道没有找到,会显示编译错误。
-
-
调用父类构造器
子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于前面所介绍的一个构造器调用另外一个重载的构造器。
在一个构造器里调用另一个重载的构造器使用this调用来完成,在子类构造器中调用父类构造器使用super调用来完成。
看下面程序定义了Base类和Sub类,其中Sub类是Base类的子类,程序在Sub类的构造器中使用super来调用Base构造器的初始化代码。