继承是面向对象三大特征之一,也是软件代码服用的重要手段。
Java只允许单继承,即每个子类只有一个直接父类。
C++中的多继承被Java舍弃了,原因是多继承一方面难以准确表述类之间的关系,另一方面很容易造成代码错误。总结起来就两个字:”难用“。
Java的继承有一个关键字extends
,实现继承的叫子类,被继承的类叫父类(也叫基类、超类)。从面向对象的角度,父类是更一般的概念,子类是更特殊的概念,例如”水果之于苹果“。
Java继承的通用代码框架为:
修饰符 class SubClass extends SuperClass
{
// 类定义
}
extends这个词翻译为”扩展“,这个含义其实更准确:子类是对父类的扩展,子类是父类的定制化、特殊化。
从”继承“的角度,子类继承了什么呢?父类的全部成员变量、方法和内部类(包括内部接口、枚举)。
从”扩展“的角度,子类是父类的扩充,所以当然拥有父类的全部内容。
但是,子类唯独不能获得父类的构造器
public class Fruit
{
public double weight;
public void info()
{
System.out.println(weight);
}
}
public class Apple extends Fruit
{
public static void main(String[] args)
{
var a = new Apple();
// Apple对象本身没有weight,但是它是Fruit的扩展,所以就拥有了weight这个成员变量
System.out.println(a.weight);
// 同时也继承了info这个方法
a.info();
}
}
注意Java继承和C++的区别
- Java摒弃了多继承
- Java类只能有一个直接父类,但是可以有多个间接父类(类似于数据结构中树的父节点和祖先节点)
如果一个Java类没有显示指定直接父类,默认扩展
java.lang.Object
(所有类的基类/父类)。因此,可以这么说:
java.lang.Object
要么是一个类的直接父类,要么是它的间接父类。
重写
大多数情况下,子类全盘接受父类的方法就行了。但是有时候,根据需求需要重写父类的方法。这时,子类和父类中出现了同名的方法,这种现象称为重写(Override),也翻译为”覆盖“:子类的方法覆盖了父类的方法。
和重载类似但有所不同,重写需要遵循”两同、两低、一高“规则:
- 方法名相同,形参列表相同(两同);
- 子类方法的返回值类型比父类方法的返回值类型低或相等;子类方法声明抛出的异常类应比父类方法声明抛出的异常类低或相等;
- 子类方法的访问权限要比父类方法的访问权限更高或相等。
无论是被覆盖的方法,还是覆盖的方法,都只能是实例方法,不能是类方法
想一想为什么?主要是从内存分配的角度来看的。
class BaseClass
{
public static void test(){}
}
class SubClass extends BaseClass
{
public void test(){} // 错误,子类试图覆盖一个类方法
}
子类覆盖了父类的方法后,**子类的对象不能再调用父类被覆盖的方法了。**但是,在子类的方法中,可以使用super
关键字来调用父类的方法(实例方法),或者直接使用父类类名.类方法
来调用(类方法)被覆盖的方法。
如果父类的方法设置了private
权限,子类本身就无法访问该方法(注意:即便是父类和子类的关系,父类也可以利用private
来隐藏一些内容)。继续无法访问,那么即使子类中定义了一个和父类同名、同形参列表的方法,也不算是重写,而是普通的一个新方法而已:
class BaseClass
{
private void test(){} // private权限
}
class SubClass extends BaseClass
{
public static void test() // 这是一个全新的方法,所以可以声明为类方法
}
super
关键字
(1)在子类的方法中可以使用super
来调用父类被覆盖的方法:
public class Bird
{
public void fly()
{
System.out.println("我在天空自在地飞翔");
}
}
// 鸵鸟
public class Ostrich extends Bird
{
// Override
public void fly()
{
System.out.println("我只能在地上奔跑")
}
public static void main(String[] args)
{
var os = new Ostrich();
// 执行鸵鸟类的实例方法
os.fly();
}
public void callOverrideMethod()
{
super.fly(); // 利用super调用父类中被覆盖的实例方法
}
}
注意:this
不能出现在static
修饰的方法中。因为this
调用的只能是实例方法;同理super
调用的也只能是实例方法。如果它们位于static
修饰的方法(即类方法)中,那这个方法可能被类调用。这时,this
和super
都没有定义了。所以不允许它们出现在类方法中。
(2)在子类的方法中也可以使用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不能出现在static方法中
System.out.println(super.a);
}
public static void main(String[] args)
{
var sc = new SubClass();
sc.accessOwner(); // 7
sc.accessBase(); // 5
}
}
如果子类没有和父类同名的实例变量,直接访问父类的变量即可,不需要使用super
或父类的类名。对没有显式制定调用的变量,系统会按照如下顺序查找这个变量的来源:
- 该方法中有没有叫该名字的局部变量;
- 当前类中是否包含叫该名字的成员变量;
- 当前类的直接父类中是否包含该名字的成员变量。如果没有,就向上回溯,直到
java.lang.Object
。 - 如果都没有找到,就会报编译错误。
如果被覆盖的是类变量,那就通过父类名.类变量
来访问父类的类变量。
Java中,创建一个子类对象时,不单会为该类中定义的实例变量分配内存,也会为它从父类(直接父类、间接父类)继承的所有实例变量分配内存,即使它的实例变量和父类重名。
例如,一个类有两个父类(直接父类A,间接父类B),A中有两个实例变量,B中有3个实例变量,当前类有2个实例变量,那么一共会分配7个实例变量的空间。
因此,所谓的变量隐藏,依然会为父类中被隐藏的实例变量分配内存空间。
调用父类的构造器
子类不会获得父类的构造器,但是可以调用父类构造器的初始化代码,类似前面使用this
来在构造器中调用重载的构造器,只不过这里用super
来完成。
class Base
{
public double size;
public String name;
public Base(double size, String name)
{
this.size = size;
this.name = name;
}
}
public class Sub extends Base
{
public String color;
public Sub(double size, String name, String color)
{
// 使用super来复用父类的构造器中的代码
super(size, name);
this.color = color;
}
}
和
this
调用重载构造器代码类似,super
调用父类构造器代码也要位于当前类构造器的第一行。
无论是否使用super
,子类构造器总会调用父类构造器一次:
- 第一种情况,子类构造器第一行使用
super
显式调用父类构造器。就是上面的情况。 - 第二种情况,子类没有使用
super
,这是系统在执行子类构造器前会隐式调用父类的无参构造器。 - 第二种情况,子类构造器使用
this
调用本类另一个构造器,但是执行另一个构造器的时候,可能遇到1或者2的情况。
上述调用方式会不断上溯,直到到达java.lang.Object
,也就是说,最先执行的永远是java.lang.Object
类的构造器。
对于下面的继承关系:
class Creature()
{
public Creature()
{
System.out.println("Creature无参构造器");
}
}
class Animal extends Creature
{
public Animal(String name)
{
System.out.println("Animal带一个参数的构造器");
}
public Animal(String name, int age)
{
this(name); // 使用this调用重载的构造器
System.out.println("Animal带两个参数的构造器");
}
}
public class Wolf extends Aminal
{
public Wolf()
{
// 使用super调用Animal带两个参数的构造器
super("灰太狼", 3);
System.out.println("Wolf无参数构造器");
}
public static void main(String[] args)
{
new Wolf();
}
}
程序执行的结果应该是:
Creature无参数构造器
Animal带一个参数的构造器
Animal带两个参数的构造器
Wolf无参数的构造器
创建任何对象都是从该类所在继承树的根所在的类的构造器开始执行,依次向下。如果遇到this
,则依次执行该层所在类的其他构造器。把上面的图换一种画法:
”万物之源“
java.lang.Object
类一直存在,但是没有存在感。因为他只有一个默认的构造器,且构造器里不输出任何内容。因此,我们感受不到调用它的构造器。