继承是面向对象的三大特征之一,也是实现软件复用的重要手段,Java的继承具有单继承的特点即每个子类只有一个父类
Java的继承通过extends关键字来实现,父类和子类是一种【一般-特殊】的关系,例如水果和苹果的关系,苹果继承了水果,苹果是水果的子类,换种说法苹果是一中特殊的水果。
Java里子类继承父类的语法如下:
修饰符 calss SubClass extends SuperClass
{
//类定义部分
}
public class Fruit
{
public double weight;
public void info()
{
System.out.println("我是一个水果!重" + weight + "g!");
}
}
public class Apple extends Fruit
{
public static void main(String[] args)
{
// 创建Apple对象
var a = new Apple();
// Apple对象本身没有weight成员变量
// 因为Apple的父类有weight成员变量,也可以访问Apple对象的weight成员变量
a.weight = 56;
// 调用Apple对象的info()方法
a.info();
}
}
class Fruit extends PlantP{...}
class Apple extends Fruit{...}
通常情况下,子类扩展父类,子类就成了一个特殊的父类,大部分时候子类总是以父类为基础,额外再增加新的成员变量和方法,但这并不能完全满足我们,例如鸟类都包含了飞的方法,其中鸵鸟是一种特殊的鸟类,因此鸵鸟应该是鸟的子类,因此它继承了了鸟类飞的方法,但这个方法明显不适合鸵鸟,因此鸵鸟类就需要重写鸟类的方法
public class Bird
{
// Bird类的fly()方法
public void fly()
{
System.out.println("我在天空里自由自在地飞翔...");
}
}
public class Ostrich extends Bird
{
// 重写Bird类的fly()方法
public void fly()
{
System.out.println("我只能在地上奔跑...");
}
public void callOverridedMethod()
{
// 在子类方法中通过super来显式调用父类被覆盖的方法。
super.fly();
}
public static void main(String[] args)
{
// 创建Ostrich对象
var os = new Ostrich();
// 执行Ostrich对象的fly()方法,将输出"我只能在地上奔跑..."
os.fly();
}
}
这种子类包含与父类中同名方法的现象被称为方法重写(Override),也可以成为方法覆盖,方法的重写要遵循“两同两小一大”原则,即方法名相同,形参列表相同;子类方法返回值类型应比父类方法返回值类型更小或者相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或者相等;子类方法的访问权限应比父类方法的访问权限更大或相等。
尤其需要指出的是:覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法一个是实例方法,错误的例子:
class BaseClass
{
public static void test(){...}
}
class SubClass extends BaseClass
{
public void test(){...}
}
class BaseClass
{
//test()方法是private访问权限
private void test(){...}
}
class SubClass extends BaseClass
{
//此处并不是重写父类test方法,所以可以增加一个static关键字
public static void test(){...}
}
将这两个比较虽然没有太多意义,但是仍需要说明一下用于增强概念,重载主要发生在同一个类的多个同名方法之间,除了要求方法名相同外并没有太大的相似之处,当然父类方法和子类方法之间也可能发生重载,子类获得父类方法,如果这时候子类定义一个与父类方法名相同但形参列表不同的方法,就形成了子类方法重载父类方法的情况。
如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法
public void callOverridedMethod()
{
//在子类方法中通过super显示调用父类中被覆盖的实例方法
super.fly();
}
如此便实现了子类调用父类中被自己重写的fly()方法,同时自己还重写了该方法,两个方法就都可以使用了
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(); // 输出7
sc.accessBase(); // 输出5
}
}
代码中能够看到,父类和子类都定义了实例变量a,子类中的a会隐藏父类中的a,当为子类创建对象的时候,实际上会为子类分配两块内存,一块用于存储子类中定义的a,一块用于存储从父类中继承得到的a,程序中使用super.a来限定访问从父类得到的a,而不是当前类中ding定义的a。
如果在某个方法中访问成员变量a,但没有显示的指定调用者,则系统查找a的顺序为:
如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量
当系统创建一个Java对象时,如果该Java类有两个父类,父类A定义了2个实例变量,父类B定义了3个实例变量,当前类中定义了2个实例变量,那么这个Java对象将会保存2+3+2个实例变量,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。
class Parent
{
public String tag = "疯狂Java讲义"; // I
}
class Derived extends Parent
{
// 定义一个私有的tag实例变量来隐藏父类的tag实例变量
private String tag = "轻量级Java EE企业应用实战"; // II
}
public class HideTest
{
public static void main(String[] args)
{
var d = new Derived();
// 程序不可访问d的私有变量tag,所以下面语句将引起编译错误
// System.out.println(d.tag); // III
// 将d变量显式地向上转型为Parent后,即可访问tag实例变量
// 程序将输出:“疯狂Java讲义”
System.out.println(((Parent) d).tag); // IV
}
}
I:为父类Parent定义了一个tag实例变量
II:为子类定义了一个private的tag实例变量,子类中定义的这个实例变量将会隐藏父类中定义的tag实例变量
mian()方法创建了Derived对象,如图所示内存分配:
III:试图通过d来访问子类的实例变量tag,程序将会提示访问权限不允许
IV:先将d变量强制向上转型为Parent类型,再通过它来访问父类实例变量tag
子类不会获得父类的构造器,但子类的构造器里可以调用父类构造器的初始化代码,类似于一个构造器调用另一个重载的构造器。
在一个构造器中调用另一个重载的构造器使用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;
}
public static void main(String[] args)
{
var s = new Sub(5.6, "测试对象", "红色");
// 输出Sub对象的三个实例变量
System.out.println(s.size + "--" + s.name
+ "--" + s.color);
}
}
使用super调用和使用this调用很像,区别在于super调用的是其父类的构造器,this调用的是同一个类中重载的构造器,使用super调用父类的构造器必须出现在子类构造器执行体的第一行,因此this调用和super调用不会同时出现
不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次,子类构造器调用父类构造器分为如下几种情况:
无论上面哪种情况,当调用子类构造器初始化子类对象时,父类构造器总会在子类构造器之前执行,不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器…依此类推,创建任何java对象,最先执行的总是java.lang.Object类的构造器
class Creature
{
public Creature()
{
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature
{
public Animal(String name)
{
System.out.println("Animal带一个参数的构造器,"
+ "该动物的name为" + name);
}
public Animal(String name, int age)
{
// 使用this调用同一个重载的构造器
this(name);
System.out.println("Animal带两个参数的构造器,"
+ "其age为" + age);
}
}
public class Wolf extends Animal
{
public Wolf()
{
// 显式调用父类有两个参数的构造器
super("灰太狼", 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args)
{
new Wolf();
}
}
执行结果:
Creature无参数的构造器
Animal带一个参数的构造器,该动物的name为灰太狼
Animal带两个参数的构造器,其age为3
Wolf无参数的构造器
继承是实现类复用的重要手段,但继承带来了一个最大的坏处是破坏了封装,相比之下组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性
子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,确实带来了相当的便利。
每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用,这是封装的规范性,但在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法,从而造成子类和父类的严重耦合。
从这个角度分析,父类的实现细节对子类不再透明,子类可以访问父类的成员变量和方法,并可以改变父类方法的实现细节(例如通过方法重写的方式),从而导致子类可以修改父类的方法。
Bird b = new Ostrich();
b.fly();
如代码所示,Bird引用变量,实际引用的是Ostrich对象,所以调用b的fly()方法时执行的不再是Bird类提供的fly()方法,而是Ostrich类重写的fly()方法
为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:
class Base
{
public Base()
{
test();
}
public void test() // ①号test()方法
{
System.out.println("将被子类重写的方法");
}
}
public class Sub extends Base
{
private String name;
public void test() // ②号test()方法
{
System.out.println("子类重写父类的方法,"
+ "其name字符串长度" + name.length());
}
public static void main(String[] args)
{
// 下面代码会引发空指针异常
var s = new Sub();
}
}
如果需要复用一个类,除了继承,还可以把该类当成另一个类的组合成分,从而允许新类直接复用该类的public方法,无论是继承还是组合都允许在新类(子类)中直接复用旧类的方法。
对于继承而言,子类可以直接获得父类的public方法,程序使用子类的时候,可以直接访问该子类继承来的方法;而组合则是把旧类对象作为新类的成员变量组合进来,用以实现新类的功能,用户看到的是新类的方法,而不能看到被组合对象的方法,通常情况下需要在新类里使用private修饰被组合的旧类对象。
class Animal
{
private void beat()
{
System.out.println("心脏跳动...");
}
public void breathe()
{
beat();
System.out.println("吸一口气,吐一口气,呼吸中...");
}
}
// 继承Animal,直接复用父类的breathe()方法
class Bird extends Animal
{
public void fly()
{
System.out.println("我在天空自在的飞翔...");
}
}
// 继承Animal,直接复用父类的breathe()方法
class Wolf extends Animal
{
public void run()
{
System.out.println("我在陆地上的快速奔跑...");
}
}
public class InheritTest
{
public static void main(String[] args)
{
var b = new Bird();
b.breathe();
b.fly();
var w = new Wolf();
w.breathe();
w.run();
}
}
class Animal
{
private void beat()
{
System.out.println("心脏跳动...");
}
public void breathe()
{
beat();
System.out.println("吸一口气,吐一口气,呼吸中...");
}
}
class Bird
{
// 将原来的父类组合到原来的子类,作为子类的一个组合成分
private Animal a;
public Bird(Animal a)
{
this.a = a;
}
// 重新定义一个自己的breathe()方法
public void breathe()
{
// 直接复用Animal提供的breathe()方法来实现Bird的breathe()方法。
a.breathe();
}
public void fly()
{
System.out.println("我在天空自在的飞翔...");
}
}
class Wolf
{
// 将原来的父类组合到原来的子类,作为子类的一个组合成分
private Animal a;
public Wolf(Animal a)
{
this.a = a;
}
// 重新定义一个自己的breathe()方法
public void breathe()
{
// 直接复用Animal提供的breathe()方法来实现Wolf的breathe()方法。
a.breathe();
}
public void run()
{
System.out.println("我在陆地上的快速奔跑...");
}
}
public class CompositeTest
{
public static void main(String[] args)
{
// 此时需要显式创建被组合的对象
var a1 = new Animal();
var b = new Bird(a1);
b.breathe();
b.fly();
// 此时需要显式创建被组合的对象
var a2 = new Animal();
var w = new Wolf(a2);
w.breathe();
w.run();
}
}
继承关系中从多个子类里抽象出共有父类的过程和组合关系中从多个整体类里提取被组合类的过程很类似,同时继承关系中从父类派生子类的过程也类似于组合关系中把被组合类组合到整体类的过程。