文章目录
前言
最近在阅读一些 Android 开源框架的源码的时候,由于对 Java 的继承和接口方面的内容掌握的不是很牢固,可以说阅读得是苦不堪言,总会产生一些莫名其妙的疑惑点。所以决定对于这部分的知识进行一个整理。继承的知识会分成两篇文章进行介绍,第一篇会比较偏重基础,第二篇会比较偏重一些注意事项和一些知识点的补充。
继承是什么
按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式并在其中添加新代码,这种方法就叫做继承。继承会使子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法。也就是说,子类和父类是“相似的”。下面举一个继承的例子:
如上图所示,动物继承生物类;老虎又继承动物类。从这个例子中可以明显看出:越往上的类是越抽象,越往下的类越具体。而在我们在设计的时候,父类也往往是比较抽象的类。接下来我们来看看如何使用继承。
继承关键字
extends
要想让一个类继承另一个类,就要使用到关键字extends,举一个最简单的例子:
publicclassTestextendsObject{}
表示的就是Test
类继承了Object
类,我们可以说,Test
是Object
的子类,也可以说,Object
是Test
的超类或父类。Test
会继承Object
的域和方法,也就是说 Test 的实例对象可以调用getClass
、equals
等方法,这些方法都是在Object
中定义的。
而事实上,我们每创建出一个类,都会隐式地继承根类Object
。这也就解释了为什么我们创建的新类实例化它的对象之后我们就能调用equals
、getClass
等方法。知道了这点,我们也就能知道下面的代码和上面的例子是等价的了:
// 即使没有声明,Test 也隐式地继承了 Object 类publicclassTest{}
接下来让我们来看看另一个比较关键的关键字,super
。
super
Java 用super
来表示超类的意思,超类也就是我们的父类。为了能解释super
的作用,我们以图形类为例:
// 图形作为父类classShape{Shape(){
System.out.println("Shape Constructor 1");}}classCircleextendsShape{Circle(){super();
System.out.println("Circle Constructor");}publicstaticvoidmain(String[] args){
Circle c=newCircle();}}
运行程序,会打印出如下信息:
Shape Constructor 1
Circle Constructor
从打印信息我们可以看到,super
调用了父类Shape
的构造方法,事实上也正是如此,接下来我们看看 super 应该如何调用父类的构造方法以及它的注意事项。
super 调用父类构造方法
上面举的例子是调用构造方法最简单的一种方式,接下来我们往Shape
添加几个有参构造方法:
classShape{Shape(){
System.out.println("Shape Constructor 1");}Shape(int i){
System.out.println("Shape Constructor 2");}Shape(int i, String s){
System.out.println("Shape Constructor 3");}}classCircleextendsShape{Circle(){super();}Circle(int i){super(i);}Circle(int i, String s){super(i, s);}publicstaticvoidmain(String[] args){
Circle c1=newCircle();
Circle c2=newCircle(1);
Circle c3=newCircle(1,"str");}}
运行程序,会打印出如下信息:
Shape Constructor 1
Shape Constructor 2
Shape Constructor 3
可以看到,在这个例子中,子类Circle
在使用 super 时往括号内添加了参数,打印信息说明这Circle
的3个构造方法分别调用了Shape
的3个构造方法。而 Java 是如何知道我们要调用的是哪个构造方法呢?答案就是super
括号内的参数类型和个数。例如super()
调用的是Shape
第一个构造方法;super(i, str)
调用的是第三个构造方法。
而子类构造方法有一点值得注意,先举例子:
classCircleextendsShape{Circle(){// super();}publicstaticvoidmain(String[] args){
Circle c1=newCircle();}}
当我们把super()
注释掉之后,你可能会觉得运行这个程序不会打印任何信息,但事实上它仍然会打印出Shape Constructor 1
。这是因为即使在子类的构造方法中没有使用super
来调用父类的构造方法,Java 仍然会在子类的构造方法中隐式地添加super()
。这么做的原因是为了构造出完整的对象,所以编译器会强制子类去调用父类的构造方法,具体原因我们会留在第二篇介绍,这里先不做深究。
当然super
绝不仅仅能调用父类的构造方法,它还能调用父类的域和方法,我们继续往下看。
super 调用父类的域和方法
我们接着往Shape
中添加域和方法,并在Circle
的draw
方法中调用Shape
的域和方法,如下所示:
classShape{int size=1;finalstatic String description="This is a shape";// ...省略构造方法...voiddraw(){
System.out.println("Shape.draw()");}}classCircleextendsShape{// ...省略构造方法...voiddraw(){// 调用 Shape 的 draw()super.draw();// 调用 Shape 的成员
System.out.println("size: "+super.size+"\nDescription: "+super.description);}publicstaticvoidmain(String[] args){
Circle c=newCircle();
c.draw();// 调用 Circle 的 draw() 方法}}
运行程序,打印出如下信息:
Shape.draw()
size: 1
Description: This is a shape
从运行结果可以看到,我们通过super
成功调用了父类Shape
的draw
方法和域成员size
以及description
。它们的调用格式均为super.xxx
,其中xxx
为方法名或域成员。
总结一下 super 的用法
1.super
表示超类,也就是父类。例如我们例子中的Shape
。
2.super
可以调用父类的构造方法,格式为super()
。Java 会根据super()
括号内的参数个数和参数类型调用相应的父类构造方法。
3.super
可以调用父类的域和方法,格式为super.xxx
,其中xxx
为域成员或者方法名。
4.即使子类构造方法中未使用super
调用父类的构造方法,Java 仍然会在子类的构造方法中隐式地添加super()
。
权限修饰符
默认情况
在我们前面的例子中,我们的构造方法、域和方法都未添加任何权限修饰符,在这种情况下,它们为包访问权限。也就是说,包外的类无法继承或者使用super
来调用我们的构造方法、域和方法。接下来我们来了解一下private
、protected
和public
在继承中会起到什么作用。
private
我们都知道声明为private
的成员是意思是私有的,不希望暴露给外界看的。所以在继承中,我们也无法用super
调用被private
修饰的成员。例子如下所示:
classShape{privateint size=1;finalstatic String description="This is a shape";privateShape(){
System.out.println("Shape Constructor 1");}Shape(int size){this.size= size;
System.out.println("Shape Constructor 2");}privatevoiddraw(){
System.out.println("Shape.draw()");}}classCircleextendsShape{Circle(int size){super(size);// 必须显示地调用父类的构造方法}voiddraw(){// 无法调用 Shape 的 draw()//! super.draw();// 无法调用 Shape 的域成员//! super.size
String s=super.description;// 可以调用 description}publicstaticvoidmain(String[] args){
Circle c=newCircle();
c.draw();// 调用 Circle 的 draw() 方法}}
可以看到,由于Shape
的成员变量size
和方法draw
都被声明为private
,所以在子类Circle
中也就无法再使用super
来调用它们了。但是我要讲的远不止于此,注意,Shape
的无参构造方法也被声明为private
了,此时在子类构造方法中也无法用super
来调用父类的构造方法。
但是我们前面说过,编译器会强制子类去调用父类的构造方法,而隐式的调用调用的是父类的无参构造方法,但它又被声明为private
,所以这时候我们就必须在子类的构造方法中显示地使用super
来调用父类的有参构造方法。而父类中只存在有参构造方法时也会导致这个情况。
所以对于子类构造方法来说,总结一句话就是:当super
无法调用父类的无参构造方法时,子类构造方法必须显式地使用super
调用父类的有参构造方法。
protected & public
介绍完了比较重要的private
,接下来的protected
和public
权限修饰符就比较容易了。把它们放在一起讲是有原因的,因为在继承关系中,它们所展示出来的特性是一模一样的,即无论子类是谁,无论子类在哪里,都可以访问(即调用或重写)由public
或protected
声明的父类成员。
重写父类方法
子类继承了父类,就获得了父类的特性和方法,但是父类的方法并不一定能满足我们的要求或者父类为抽象类的时候(子类为普通类),这时候就需要我们来重写父类的方法。接下来我们就来介绍如何重写父类方法以及重写父类方法中存在的陷阱。
final 关键字
我们都知道,final
关键字表示“这是无法改变的”的意思,显而易见,由final
声明的方法是无法被重写的。所以一旦父类的方法被声明为final
,子类就无法重写该方法。事实上,声明为 private 的方法也会被隐式地声明为final
,所以声明为private
的方法也同样无法被重写。例子如下所示:
publicclassShape{// ...// 声明为 final 的方法无法被重写finalvoiddraw(){
System.out.println("Shape.draw()");}// 声明为 private 的方法无法被重写privatevoidprivateMethod(){
System.out.println("Shape.privateMethod()");}}classCircleextendsShape{// ...// 错误:无法重写声明为 final 的方法//@Override//final void draw() {}// 错误:无法重写声明为 private 的方法//@Override//private void privateMethod() {}}
@Override
前面的例子中,我们看到子类在重写父类方法时,前面添加了一个@Override
,这是一个注解,在我们的方法前添加,如果该方法不是重写方法,那么编译器就会报错,这有助于我们正确地重写父类的方法。重写方法时不加这个注解也是可以的,不过仍然推荐添加上去,因为这能让我们避免很多意想不到的坑,后面的陷阱就可以通过它避免。
示例
接下来做一个示例展示如何重写父类方法,代码如下:
publicclassShape{voiddraw(){
System.out.println("Shape.draw()");}}classCircleextendsShape{// 重写父类的 draw 方法@Overridevoiddraw(){
System.out.println("Circle.draw()");}publicstaticvoidmain(String[] args){
Circle c=newCircle();
c.draw();}}
这段代码是不是有点熟悉呢?其实在之前的例子中我们一直有使用到重写方法,只是我们没有提及,我们来重新看看它。
父类Shape
有一个draw
方法,打印信息Shape.draw()
,而我们在子类中重写了父类的draw
方法,打印的信息是Circle.draw()
。当我们运行程序时,打印出来的信息我想你也已经猜到了,就是Circle.draw()
。重写父类方法就是这么的简单。
这里要特别提醒的是,注意到我们Shape
的draw
方法是无添加权限修饰符的,所以当包外的类继承Shape
时,draw
方法无法被包外的子类重写。
一个小陷阱
接下来我们修改下Shape
,子类也做下相应的修改,代码如下:
publicclassShape{privatevoidmethod(){
System.out.println("Shape.method()");}}classCircleextendsShape{privatevoidmethod(){
System.out.println("Circle.method()");}publicvoiduseMethod(){method();}publicstaticvoidmain(String[] args){
Circle c=newCircle();
c.useMethod();}}
注意看,Shape
的method
方法是被声明为private
的,但是我们的子类Circle
似乎试图去覆盖这个私有方法,而且你会发现上述代码是可以通过编译运行的。但是前面我们提到过,声明为private
的方法是无法被子类重写的,那么这又是为什么呢?
答案其实很简单,method
并不是重写方法,它其实是Circle
的新方法,只不过与Shape
的method
方法同名罢了,当我们往Circle
的method
方法前加上@Override
注解时,编译器就会报错,这个报错已经说明了一切,所以我才会推荐(其实是强烈建议)在重写方法前加上@Override
注解。
修改权限修饰符
我们的重写方法是可以对权限修饰符进行修改的,当然,它也必须遵循一定的规则:只能从小范围往大范围改,不能从大范围往小范围改。例子如下所示:
publicclassShape{// 无权限修饰符的方法voiddraw(){
System.out.println("Shape.draw()");}// 权限修饰符为 protected 的方法protectedvoidprotectedMethod(){}// 权限修饰符为 public 的方法publicvoidpublicMethod(){}}classCircleextendsShape{// 添加权限修饰符 protected@Overrideprotectedvoiddraw(){super.draw();}// 将权限修饰符修改为 public@OverridepublicvoidprotectedMethod(){super.protectedMethod();}// 错误,只能从小范围往大范围修改//@Override//protected void publicMethod() {// super.publicMethod();//}}
从上面的例子中我们可以看到,Shape
的draw
方法没有任何权限修饰符,它可以在重写方法中声明为protected
或public
的方法,因为默认情况下的范围只能是包内访问(小),而public
和protected
均能提供包内包外的访问(大),所以这个修改是合法的。而同理protected
方法也可以被声明为public
的方法,但是public
的方法不能被声明为protected
或默认情况或private
的方法。
重写抽象类的方法
含有抽象方法的类就是抽象类,抽象关键字是abstract
。抽象方法是没有方法体的方法,它只有一个方法声明。抽象类的具体介绍会在下一章,这里先简单提及一下重写抽象类方法的注意事项。
当普通类继承抽象类时,必须重写抽象类的所有抽象方法来为父类的方法体提供定义,这样子我们才能够创建子类的实例。例子如下所示:
publicabstractclassShape{// 抽象方法 drawabstractvoiddraw();}classCircleextendsShape{// 必须重写父类的抽象方法@Overrideprotectedvoiddraw(){
System.out.println("Circle.draw()");}}
可以看到,我们的Circle
是一个普通类,当它继承自抽象类Shape
时(注意关键字abstract
),必须重写抽象方法draw
,否则就会报错。而当抽象类继承抽象类时,可以不重写父类的抽象方法。例子如下所示:
abstractclassCircleextendsShape{abstractvoiddrawCircle();// Circle 自己的抽象方法}
我们将Circle
也修改成了一个抽象类,此时即使我们不重写draw
方法,也不会有任何问题。
总结
文章最后总结一下本篇所涉及到的知识:
- 继承会使子类继承父类的特性和方法,即父类有的子类也有。
- 继承的关键字是
extends
,当子类继承父类时,子类可以通过super
调用父类的构造方法、域和方法。 - 当子类构造方法没有调用父类构造方法时,会隐式地调用
super()
,当父类的无参构造方法不可调用时,子类构造方法必须显示地调用父类的构造方法。 - 当我们的成员或方法没有添加任何权限修饰符时,默认为包访问权限。
- 声明为
private
的成员无法被子类访问;声明为public
或protected
的父类成员,无论子类在何处,都可以访问父类成员。 - 父类的
private
方法无论如何都无法被重写,如果子类中出现了这个方法,它属于子类的新方法,只不过和父类的方法恰巧同名罢了,使用@Override
可以避免这种陷阱。 - 父类中声明为
final
的方法是无法被重写的,事实上声明为private
的成员也会被隐式地声明为final
。 - 在重写方法时,添加注解
@Override
永远是个好习惯。 - 重写父类方法时,重写方法的权限修饰符可以由小范围往大范围修改。
- 抽象类被继承时,如果子类为普通类,那么子类必须为父类的抽象类方法提供定义,即重写这些抽象类方法;如果子类也为抽象类,那么可以选择不重写这些父类的抽象方法。