本文是《Java学习指南》原书的网络版,作者邵发,拥有本书的全部权利。相关视频课程在此查看。
目录
11章 继承
11.1 类的继承
本章描述继承关系在Java语言中的表示。那什么叫继承关系呢?还是先用自然界面的例子说起。
11.1.1 引例1
在自然界中,树可以称为一个类,而苹果树也是一个类。它们之间的关系可以用下表示。
其中,树作为一类,具有树叶、主干和根(属性),能进行光合作用(方法)。而苹果树也是一种树,所有也有树叶、主干和树,也能进行光合作用。除此之外,苹果树还有自己的特性:结苹果。
这就是继承关系,苹果树作为一种树,继承了树的所有共性。另外,苹果树也有自己的特性。
11.1.2 引例2
下面,再给出一个贴近计算机编程的例子,来进一步说明继承关系。在计算机上有文件类,而有些文件是视频文件类。这两个类就是继承关系。
所有的文件都有大小、创建时间等属性,都可以读操作、写操作等方法。
视频文件作为文件的一种,自然也具备大小、创建时间等属性,也能读写。除此之外,视频文件还具有时长属性,能进行播放操作。
可以说,视频文件类继承了文件类的所有属性和方法。
11.1.3 继承 extends
在Java语言里,用关键字extends表示类与类之间的继承关系。一般写法为,
public class B extends A { }
其中,A,B是两个类。把A称为父类(superclass),把B称为子类(subclass)。整体上可以读作B继承于A。
当B继承于A时,那么父类A中的所有public的属性和方法,都被自然的继承拥有。还是以视频文件和文件类为例,
先定义一个父类MyFile表示文件类,
public class MyFile
{
public long size; // 文件大小
public String name; // 文件名
public void info() // 显示文件信息
{
System.out.println("文件:"+name+",大小:" + size);
}
}
父类具有:
- 2个属性:size, name
- 1个方法info() ,用于显示文件的信息
下面,再添加一个MyVideoFile 表示视频文件类,注意下面的extends关键字的写法,
public class MyVideoFile extends MyFile
{
}
作为子类,MyVideoFile已经自动地继承了父类的属性和方法。所以共有的东西就不必再写一遍了,只要写上自己特有的东西就可以了,示例代码如下,
public class MyVideoFile extends MyFile
{
public int duration ; // 时长
public void play()
{
System.out.println("播放视频"+ this.name);
}
public void stop()
{
System.out.println("停止播放"+ this.name);
}
}
再来看一下怎么样使用子类MyVideoFile,
MyVideoFile f = new MyVideoFile();
f.size = 1293034; // 继承于父类
f.name = "abc.mp4";// 继承于父类
f.duration = 130;
f.info(); // 继承于父类
f.play();
f.stop();
可以看到,在MyVideoFile里并没有定义size, name, info(),但却可以直接使用它们。原因就在于extends,使用extends就可以把父类的属性和方法继承过来。
11.2 重写
重写 ( Override ):在继承的时候,如果觉得父类的方法不满足要求,可以把这个方法在子类里重写一遍。
例如,在先前的例子中,父类MyFile表示一般性的文件,子类MyVideoFile表示视频文件。在父类中,有一个info() 方法,用来打印输出文件的一般信息性息。但对子类来说,这个info() 方法就不够用了,因为它没有显示出视频的时长信息的,所以可以把info()方法重写。示例如下,
public class MyVideoFile extends MyFile
{
public int duration ; // 时长
@Override
public void info()
{
System.out.println("文件名:" + this.name + ",文件大小: " + this.size
+ ",视频时长: " + this.duration );
}
}
然后再看一下调用,
MyVideoFile f = new MyVideoFile();
f.size = 1293034;
f.name = "abc.mp4";
f.duration = 130;
f.info(); // 子类重写了这个方法
由于子类把info()重写了一遍,所以最终输出时调用的是子类的代码,控制台显示如下,
11.2.1 部分重写
部分重写:就是觉得父类的方法写得还行,只需补充修改就可以。还是以上述场景为例,在父类MyFile的info()里,已经打印显示了文件名和文件大小;对于子类MyVideoFile来说,只需要把时长补充打印一下即可。把MyVideoFile的代码稍做更改,如下,
@Override
public void info()
{
super.info();
System.out.println("视频时长"+ this.duration);
}
其中,super.info() 表示调用父类的info()方法。这样,就在父类的基础上,补充了视频时长输出的功能。
提示:在书写时应注意,被重写的方法前面的一行 @Override ,也是有用的,不要随便删掉。
11.3 构造方法的继承
在Java语言里,构造方法是自动继承的。这意味着,如果B继承于A,则A的构造方法会被自动调用。
例如,先定义一个类 Parent,
public class Parent
{
int a;
public Parent()
{
a = 10;
System.out.println("父类Parent构造...");
}
}
在定义一个Child继承于Parent,
public class Child extends Parent
{
public Child()
{
System.out.println("子类Child构造...");
}
}
显然,在子类中并没有看到有调用父类的构造方法。但是,运行以下代码试一下,
public static void main(String[] args)
{
Child ch = new Child();
System.out.println("程序退出");
}
在这里,创建了一个Child对象,自然地会调用Child的构造方法。但在控制台的输出显示中,
在这个显示中,可以确定是先调用了父类的构造方法,再调用了子类的构造方法。这充分说明,父类的构造方法默认会被调用 。
11.3.1 显式调用父类构造方法
可以在子类中显示调用父类的构方法。在父类有多个构造方法的时候,就特别的有用。例如,先给父类添加多个构造方法,
public class Parent
{
int a;
public Parent()
{
a = 10;
System.out.println("父类Parent构造...");
}
public Parent(int a)
{
this.a = a;
System.out.println("父类Parent构造222...");
}
}
此时父类有2个构造方法,那么在子类里就可以显式指定调用哪一个,例如,
public class Child extends Parent
{
public Child()
{
super(12);
System.out.println("子类Child构造...");
}
}
使用 super 关键字可以显式指定调用父类的构造方法。例如,super()表示调用父类的无参构造方法,而super(12) 表示调用另一个带参的构造方法。其匹配规则在第七章(方法的重载)那一章节已经讲过。
11.4 单根继承
在Java语言里,一个类只能有一个父类。例如,下面的写法是错误的,
public class A extends B, C // 错误的写法!
{
}
A不能同时有两个父类B,C,这是语法禁止的。
如果A继承于B, B继承于 C, C继承于D,也就是说B是父亲,C是祖父,D是曾祖父,可以就形成一根继承链条:
A -> B -> C -> D
其中,箭头表示继承关系。链条的最顶端,是顶级父类D。
11.4.1 Object类
在Java语言里,如果一个类没有显式地指定父类,则默认继承于Object类。例如,
public class Student
{
public String id;
public String name;
public boolean sex;
}
这个Student类没有指定父类,则默认父类是Object类。相当于写成,
public class Student extends Object
{
public String id;
public String name;
public boolean sex;
}
通常情况下,extends Object 是省略不写的。
在Java里,所有类的顶级父类都是Object。 或者说,所有的类都是Object类的子类或孙子类。
以前面一节所用的例子进行说明,
public class Parent
{
}
public class Child extends Parent
{
}
由于Parent的父类是Object,所以最终的继承链为:
此图可以在Eclipse中,右键选中一个类,然后点菜单Quick Type Hierarchy显示。图中可以看到,Child的父类是Parent,而Parent的父类为Object。
11.4.2 重写toString方法
在Object中有一个方法toString(),用于将对象转成字符串显示。这是我们经常需要重写的一个方法。
例如,有一个类Student,
public class Student
{
public String id;
public String name;
public boolean sex;
}
然后在main()里调用它,
public static void main(String[] args)
{
Student s = new Student();
s.id = "20180001";
s.name = "邵发";
s.sex = true;
System.out.println("学生信息:" + s);
}
然后Eclipse运行程序,在控制台里输出如下,
实际上在前面的章节已经强调过,Java里的对象是默认不能打印显示的,否则就会出现 my.Student@6d06d69c 类似的字样。(类名+对象地址)。
此时我们应在Student类重写一下toString()方法,以便能以正常的字符串显示,示例如下,
public class Student extends Object
{
public String id;
public String name;
public boolean sex;
@Override
public String toString()
{
String result = id + " / " + name + " / " ;
if(sex)
result += "男";
else
result += "女";
return result;
}
}
再运行程序,则输出为,
其实,
System.out.println("学生信息:" + s);
就相当于
System.out.println("学生信息:" + s.toString());
所以最终显示的是s.toString() 方法返回的字符串。
提示:toString()方法是经常要重写的方法,一定要理解掌握。
11.5 多态
多态(polymorphism)是一个软件设计上的一个术语。 Java支持多态设计,具体体现在以下语法现象:
v 重载 Overload: 方法允许重名
v 重写 Override: 允许子类重写父类的方法
v 泛型(模板): 在高级语法篇中讲解,例如ArrayList,HasMap
方法重写,就是一种多态的设计。比如说,父类MyFile的info()方法,与子类MyVideoFile的info()方法,两者方法名相同,但是子类重新把方法重写了一遍。同一个方法,两种不同的行为功能,这就是多态的设计理态。(注:“多态”这个术语翻译的有点晦涩,不必纠结字面意思)。
11.5.1 父子类型之间的转换
假设有一个类Pie表示饼干,另一个类ApplePie表示苹果味的饼干,它们具有继承关系,
public class ApplePie extends Pie
{
}
子类转成父类顺利成章的,例如,
ApplePie p1 = new ApplePie();
Pie p2 = (Pie) p1; // 类型转换: ApplePie -> Pie
其中,p1是一个ApplePie的对象,由于“苹果味饼干是一种饼干”,所以在逻辑上可以很容易接受 Pie p2 = (Pie) p1,这是很自然的转换、顺理成章的转换。
通过情况下,将子类类型转成父类类型,直接隐式转换就可以,
Pie p2 = p1; // 隐式转换即可,没有风险
更简洁的,可以写成:
Pie p2 = new ApplePie();
这是一种常见的写法,右侧为一个ApplePie对象,被转成Pie类型的引用。
比如,有一个类Baby,需要传入Pie对象,
public class Baby
{
// 宝宝要吃饼干
public void eat ( Pie p)
{
}
}
这个eat()方法表示:宝宝要吃饼干,需传入Pie对象。那么,现在有一块ApplePie,传给它是不是也可以呢?当然可以。
Pie p = new ApplePie();
Baby bb = new Baby();
bb.eat( p );
虽然eat() 方法要求传入Pie类型的对象,但传入ApplePie类型也是没有问题的,因为ApplePie就是一种Pie。
11.5.2 方法的多态调用
考虑以下代码,
MyFile file = new MyVideoFile();
file.info();
那么,file.info () 具体执行的是MyFile.info() 还是 MyVideoFile.info()呢?
在做这种判断时,有一个很简单的原则:看对象的真正类型。在这里,file真正指向的是一个MyVideoFile对象,所以真正执行的是MyVideoFile里的info()方法。
也就是说,虽然字面上file对象是MyFile引用类型,但它指向的对象的具体类型是MyVideoFile。我们要看目标对象的具体类型。
提示:这一语法在理解上需要一定时间,但初期我们应强行记住这种写法 Parent p = new Child() ,这是Java里面很常见的写法。