Java 基础之继承(一)

前言

最近在阅读一些 Android 开源框架的源码的时候,由于对 Java 的继承接口方面的内容掌握的不是很牢固,可以说阅读得是苦不堪言,总会产生一些莫名其妙的疑惑点。所以决定对于这部分的知识进行一个整理。继承的知识会分成两篇文章进行介绍,第一篇会比较偏重基础,第二篇会比较偏重一些注意事项和一些知识点的补充。

继承是什么

按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式并在其中添加新代码,这种方法就叫做继承。继承会使子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法。也就是说,子类和父类是“相似的”。下面举一个继承的例子:
继承的例子
如上图所示,动物继承生物类;老虎又继承动物类。从这个例子中可以明显看出:越往上的类是越抽象,越往下的类越具体。而在我们在设计的时候,父类也往往是比较抽象的类。接下来我们来看看如何使用继承。

继承关键字

extends

要想让一个类继承另一个类,就要使用到关键字 extends,举一个最简单的例子:

public class Test extends Object{
}

表示的就是 Test 类继承了 Object 类,我们可以说,TestObject 的子类,也可以说,ObjectTest 的超类或父类。Test 会继承 Object域和方法,也就是说 Test 的实例对象可以调用 getClassequals 等方法,这些方法都是在 Object 中定义的。

而事实上,我们每创建出一个类,都会隐式地继承根类 Object。这也就解释了为什么我们创建的新类实例化它的对象之后我们就能调用 equalsgetClass 等方法。知道了这点,我们也就能知道下面的代码和上面的例子是等价的了:

// 即使没有声明,Test 也隐式地继承了 Object 类
public class Test {
}

接下来让我们来看看另一个比较关键的关键字,super

super

Javasuper 来表示超类的意思,超类也就是我们的父类。为了能解释 super 的作用,我们以图形类为例:

// 图形作为父类
class Shape {
	Shape(){
		System.out.println("Shape Constructor 1");
	}
}

class Circle extends Shape{
	Circle(){
		super();
		System.out.println("Circle Constructor");
	}
	
	public static void main(String[] args) {
		Circle c = new Circle();
	}
}

运行程序,会打印出如下信息:

Shape Constructor 1
Circle Constructor

从打印信息我们可以看到,super 调用了父类 Shape 的构造方法,事实上也正是如此,接下来我们看看 super 应该如何调用父类的构造方法以及它的注意事项。

super 调用父类构造方法

上面举的例子是调用构造方法最简单的一种方式,接下来我们往 Shape 添加几个有参构造方法:

class Shape {
	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");
	}
}


class Circle extends Shape{
	Circle(){
		super();
	}
	
	Circle(int i){
		super(i);
	}
	
	Circle(int i, String s){
		super(i, s);
	}
	
	public static void main(String[] args) {
		Circle c1 = new Circle();
		Circle c2 = new Circle(1);
		Circle c3 = new Circle(1, "str");
	}
}

运行程序,会打印出如下信息:

Shape Constructor 1
Shape Constructor 2
Shape Constructor 3

可以看到,在这个例子中,子类 Circle 在使用 super 时往括号内添加了参数,打印信息说明这 Circle 的3个构造方法分别调用了 Shape 的3个构造方法。而 Java 是如何知道我们要调用的是哪个构造方法呢?答案就是 super 括号内的参数类型和个数。例如 super() 调用的是 Shape 第一个构造方法;super(i, str) 调用的是第三个构造方法。

而子类构造方法有一点值得注意,先举例子:

class Circle extends Shape{
	Circle(){
		// super();
	}
	
	public static void main(String[] args) {
		Circle c1 = new Circle();
	}
}

当我们把 super() 注释掉之后,你可能会觉得运行这个程序不会打印任何信息,但事实上它仍然会打印出 Shape Constructor 1。这是因为即使在子类的构造方法中没有使用 super 来调用父类的构造方法,Java 仍然会在子类的构造方法中隐式地添加 super()。这么做的原因是为了构造出完整的对象,所以编译器会强制子类去调用父类的构造方法,具体原因我们会留在第二篇介绍,这里先不做深究。
当然 super 绝不仅仅能调用父类的构造方法,它还能调用父类的域和方法,我们继续往下看。

super 调用父类的域和方法

我们接着往 Shape 中添加域和方法,并在 Circledraw 方法中调用 Shape 的域和方法,如下所示:

class Shape {
	int size = 1;
	final static String description = "This is a shape";
	
	// ...省略构造方法...
	
	void draw() {
		System.out.println("Shape.draw()");
	}
}


class Circle extends Shape{
	// ...省略构造方法...
	
	void draw() {
		// 调用 Shape 的 draw()
		super.draw();  
		// 调用 Shape 的成员
		System.out.println("size: " + super.size + "\nDescription: " + super.description);
	}
	
	public static void main(String[] args) {
		Circle c = new Circle();
		c.draw();  // 调用 Circle 的 draw() 方法
	}
}

运行程序,打印出如下信息:

Shape.draw()
size: 1
Description: This is a shape

从运行结果可以看到,我们通过 super 成功调用了父类 Shapedraw 方法和域成员 size 以及 description。它们的调用格式均为 super.xxx,其中 xxx 为方法名或域成员。

总结一下 super 的用法

1.super 表示超类,也就是父类。例如我们例子中的 Shape
2.super 可以调用父类的构造方法,格式为 super()。Java 会根据 super() 括号内的参数个数和参数类型调用相应的父类构造方法。
3.super 可以调用父类的域和方法,格式为 super.xxx,其中 xxx 为域成员或者方法名。
4.即使子类构造方法中未使用 super调用父类的构造方法,Java 仍然会在子类的构造方法中隐式地添加 super()

权限修饰符

默认情况

在我们前面的例子中,我们的构造方法、域和方法都未添加任何权限修饰符,在这种情况下,它们为包访问权限。也就是说,包外的类无法继承或者使用 super 来调用我们的构造方法、域和方法。接下来我们来了解一下 privateprotectedpublic 在继承中会起到什么作用。

private

我们都知道声明为 private 的成员是意思是私有的,不希望暴露给外界看的。所以在继承中,我们也无法用 super 调用被 private 修饰的成员。例子如下所示:

class Shape {
	private int size = 1;
	final static String description = "This is a shape";
	
	private Shape(){
		System.out.println("Shape Constructor 1");
	}
	
	Shape(int size){
		this.size = size;
		System.out.println("Shape Constructor 2");
	}
	
	private void draw() {
		System.out.println("Shape.draw()");
	}
}


class Circle extends Shape{
	Circle(int size){
		super(size);  // 必须显示地调用父类的构造方法
	}

	void draw() {
		// 无法调用 Shape 的 draw()
		//! super.draw();  
		// 无法调用 Shape 的域成员
		//! super.size
		String s = super.description;  // 可以调用 description 
	}
	
	public static void main(String[] args) {
		Circle c = new Circle();
		c.draw();  // 调用 Circle 的 draw() 方法
	}
}

可以看到,由于 Shape 的成员变量 size 和方法 draw 都被声明为 private,所以在子类 Circle 中也就无法再使用 super 来调用它们了。但是我要讲的远不止于此,注意,Shape 的无参构造方法也被声明为 private 了,此时在子类构造方法中也无法用 super 来调用父类的构造方法。

但是我们前面说过,编译器会强制子类去调用父类的构造方法,而隐式的调用调用的是父类的无参构造方法,但它又被声明为 private,所以这时候我们就必须在子类的构造方法中显示地使用 super 来调用父类的有参构造方法。而父类中只存在有参构造方法时也会导致这个情况

所以对于子类构造方法来说,总结一句话就是:super 无法调用父类的无参构造方法时,子类构造方法必须显式地使用 super 调用父类的有参构造方法

protected & public

介绍完了比较重要的 private,接下来的 protectedpublic 权限修饰符就比较容易了。把它们放在一起讲是有原因的,因为在继承关系中,它们所展示出来的特性是一模一样的,即无论子类是谁,无论子类在哪里,都可以访问(即调用或重写)由 publicprotected 声明的父类成员。

重写父类方法

子类继承了父类,就获得了父类的特性和方法,但是父类的方法并不一定能满足我们的要求或者父类为抽象类的时候(子类为普通类),这时候就需要我们来重写父类的方法。接下来我们就来介绍如何重写父类方法以及重写父类方法中存在的陷阱。

final 关键字

我们都知道,final 关键字表示“这是无法改变的”的意思,显而易见,由 final 声明的方法是无法被重写的。所以一旦父类的方法被声明为 final,子类就无法重写该方法。事实上,声明为 private 的方法也会被隐式地声明为 final,所以声明为 private 的方法也同样无法被重写。例子如下所示:

public class Shape {

	// ...

	// 声明为 final 的方法无法被重写
	final void draw() {
		System.out.println("Shape.draw()");
	}
	
	// 声明为 private 的方法无法被重写
	private void privateMethod() {
		System.out.println("Shape.privateMethod()");
	}
}


class Circle extends Shape{
	
	// ...
	
	// 错误:无法重写声明为 final 的方法
	//@Override
	//final void draw() {}
	
	// 错误:无法重写声明为 private 的方法
	//@Override
	//private void privateMethod() {}
}

@Override

前面的例子中,我们看到子类在重写父类方法时,前面添加了一个 @Override,这是一个注解,在我们的方法前添加,如果该方法不是重写方法,那么编译器就会报错,这有助于我们正确地重写父类的方法。重写方法时不加这个注解也是可以的,不过仍然推荐添加上去,因为这能让我们避免很多意想不到的坑,后面的陷阱就可以通过它避免。

示例

接下来做一个示例展示如何重写父类方法,代码如下:

public class Shape {
	void draw() {
		System.out.println("Shape.draw()");
	}
}


class Circle extends Shape{
	// 重写父类的 draw 方法
	@Override
	void draw() {
		System.out.println("Circle.draw()");
	}
	
	public static void main(String[] args) {
		Circle c = new Circle();
		c.draw();
	}
}

这段代码是不是有点熟悉呢?其实在之前的例子中我们一直有使用到重写方法,只是我们没有提及,我们来重新看看它。
父类 Shape 有一个 draw 方法,打印信息 Shape.draw(),而我们在子类中重写了父类的 draw 方法,打印的信息是 Circle.draw()。当我们运行程序时,打印出来的信息我想你也已经猜到了,就是 Circle.draw()。重写父类方法就是这么的简单。
这里要特别提醒的是,注意到我们 Shapedraw 方法是无添加权限修饰符的,所以当包外的类继承 Shape 时,draw 方法无法被包外的子类重写。

一个小陷阱

接下来我们修改下 Shape,子类也做下相应的修改,代码如下:

public class Shape {
	private void method() {
		System.out.println("Shape.method()");
	}
}


class Circle extends Shape{
	private void method() {
		System.out.println("Circle.method()");
	}
	
	public void useMethod(){
		method();
	}
	
	public static void main(String[] args) {
		Circle c = new Circle();
		c.useMethod();
	}
}

注意看,Shapemethod 方法是被声明为 private 的,但是我们的子类 Circle 似乎试图去覆盖这个私有方法,而且你会发现上述代码是可以通过编译运行的。但是前面我们提到过,声明为 private 的方法是无法被子类重写的,那么这又是为什么呢?
答案其实很简单,method 并不是重写方法,它其实是 Circle 的新方法,只不过与 Shapemethod 方法同名罢了,当我们往 Circlemethod 方法前加上 @Override 注解时,编译器就会报错,这个报错已经说明了一切,所以我才会推荐(其实是强烈建议)在重写方法前加上 @Override 注解。

修改权限修饰符

我们的重写方法是可以对权限修饰符进行修改的,当然,它也必须遵循一定的规则:只能从小范围往大范围改,不能从大范围往小范围改。例子如下所示:

public class Shape {
	// 无权限修饰符的方法
	void draw() {
		System.out.println("Shape.draw()");
	}
	
	// 权限修饰符为 protected 的方法
	protected void protectedMethod() {
	}
	
	// 权限修饰符为 public 的方法
	public void publicMethod() {
	}
}


class Circle extends Shape{
	// 添加权限修饰符 protected 
	@Override
	protected void draw() {
		super.draw();
	}
	
	// 将权限修饰符修改为 public
	@Override
	public void protectedMethod() {
		super.protectedMethod();
	}

	// 错误,只能从小范围往大范围修改
	//@Override
	//protected void publicMethod() {
	//	super.publicMethod();
	//}

}

从上面的例子中我们可以看到,Shapedraw 方法没有任何权限修饰符,它可以在重写方法中声明为 protectedpublic 的方法,因为默认情况下的范围只能是包内访问(小),而 publicprotected 均能提供包内包外的访问(大),所以这个修改是合法的。而同理 protected 方法也可以被声明为 public 的方法,但是 public 的方法不能被声明为 protected 或默认情况或 private 的方法。

重写抽象类的方法

含有抽象方法的类就是抽象类,抽象关键字是 abstract。抽象方法是没有方法体的方法,它只有一个方法声明。抽象类的具体介绍会在下一章,这里先简单提及一下重写抽象类方法的注意事项。
当普通类继承抽象类时,必须重写抽象类的所有抽象方法来为父类的方法体提供定义,这样子我们才能够创建子类的实例。例子如下所示:

public abstract class Shape {
	// 抽象方法 draw
	abstract void draw(); 
}


class Circle extends Shape{
	// 必须重写父类的抽象方法
	@Override
	protected void draw() {
		System.out.println("Circle.draw()");
	}
}

可以看到,我们的 Circle 是一个普通类,当它继承自抽象类 Shape 时(注意关键字 abstract),必须重写抽象方法 draw,否则就会报错。而当抽象类继承抽象类时,可以不重写父类的抽象方法。例子如下所示:

abstract class Circle extends Shape{	
	abstract void drawCircle();  // Circle 自己的抽象方法
}

我们将 Circle 也修改成了一个抽象类,此时即使我们不重写 draw 方法,也不会有任何问题。

总结

文章最后总结一下本篇所涉及到的知识:

  1. 继承会使子类继承父类的特性和方法,即父类有的子类也有。
  2. 继承的关键字是 extends,当子类继承父类时,子类可以通过 super 调用父类的构造方法、域和方法。
  3. 当子类构造方法没有调用父类构造方法时,会隐式地调用 super(),当父类的无参构造方法不可调用时,子类构造方法必须显示地调用父类的构造方法。
  4. 当我们的成员或方法没有添加任何权限修饰符时,默认为包访问权限。
  5. 声明为 private 的成员无法被子类访问;声明为 publicprotected 的父类成员,无论子类在何处,都可以访问父类成员。
  6. 父类的 private 方法无论如何都无法被重写,如果子类中出现了这个方法,它属于子类的新方法,只不过和父类的方法恰巧同名罢了,使用 @Override可以避免这种陷阱。
  7. 父类中声明为 final 的方法是无法被重写的,事实上声明为 private 的成员也会被隐式地声明为 final
  8. 在重写方法时,添加注解 @Override 永远是个好习惯。
  9. 重写父类方法时,重写方法的权限修饰符可以由小范围往大范围修改。
  10. 抽象类被继承时,如果子类为普通类,那么子类必须为父类的抽象类方法提供定义,即重写这些抽象类方法;如果子类也为抽象类,那么可以选择不重写这些父类的抽象方法。
  • 14
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值