Java 基础之类的继承

前言

最近在阅读一些 Android 开源框架的源码的时候,由于对 Java 的继承接口方面的内容掌握的不是很牢固,可以说阅读得是苦不堪言,总会产生一些莫名其妙的疑惑点。所以决定对于这部分的知识进行一个系统的整理。这一篇介绍的是类的继承

概念

下面一段话是《Thinking in Java》中关于继承的描述:

继承 是所有 OOP 语言和 Java 语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从 Java 的标准根基类 Object 进行继承。

这段话有个重要的信息点:
当我们创建一个类时,实质上会隐式地继承Object类,例如:

public class Test {
}

等同于下面的代码:

public class Test extends Object{
}

这也就解释了为什么我们每创建一个类的时候,类中都会有 getClass()equals()hashCode()toString() 等方法,因为所有的类都显示或者隐式地继承了Object类,所以Object类中的这些方法会被子类所继承

继承的语法

继承的关键字是 extends,举一个最简单的例子:

public class Parent{
}

public class Child extends Parent{
}

表示的就是 Child 类继承了 Parent 类,我们可以说,ChildParent子类,也可以说,ParentChild超类父类。当然这里我们只是写了2个空类,下面我们来开始为它们添加构造方法。例子如下:

/**
 * 父类
 */
public class Parent {

	// 不带参数的构造方法1
	public Parent() {
		System.out.println("父类构造方法(无参)");
	}
	
	// 带参数的构造方法2
	public Parent(int val) {
		System.out.println("父类构造方法(有参) "+val);
	}
	
	// 带参数的构造方法2
	public Parent(int val, String str) {
		System.out.println("父类构造方法(有参) "+val+str);
	}
}

在父类中,我们定义了1个不带参和2个带参的构造方法,通过打印信息来判断继承的工作方式。接下来看看我们的子类:

/**
 * 子类
 */
public class Child extends Parent{

	// 子类无参构造方法1
	public Child() {
		super();
	}
	
	// 子类有参构造方法2
	public Child(int val) {
		super(val);
	}
	
	// 子类有参构造方法3
	public Child(int val, String str) {
		super(val, str);
	}
	
	public static void main(String[] args) {
		Child c1 = new Child();    
		Child c2 = new Child(1);
		Child c3 = new Child(1, "str");
	}
}

main 方法中,我们分别用子类的3个构造方法实例化了3个对象(c1、c2、c3),运行结果如下:

父类构造方法(无参)
父类构造方法(有参) 1
父类构造方法(有参) 1 str

从运行结果可以看到,3 个对象分别打印出了父类 Parent 的三个构造方法中的输出信息,子类的构造方法是如何获得这些信息的呢?仔细看看子类的3个构造方法,里面都有个关键字 super,而且 super 括号内还可以添加参数。事实上,子类就是通过 super 来调用父类的构造方法的。接下来我们来看看 super 关键字有什么用处。

super 关键字

super 调用父类构造方法

Javasuper 来表示超类的意思,在这个例子中,超类就是 Parentsuper()根据括号内的参数个数和参数类型来决定调用父类的哪个调用方法。例如 super() 调用的就是第1个构造方法,super(val, str) 调用的就是第3个构造方法。
super 在构造方法的调用上有两个点值得注意:
1. 下面两个构造方法实现的效果是一样的:

public Child() {
}

public Child() {
	super();
}

在实例化 Child 后,它们都会打印出信息:父类构造方法(无参)。原因是在子类构造方法中,即使没有调用 super(),Java 也会在子类的构造方法中隐式地调用 super()

2. 如果父类不存在无参构造方法或者无参构造方法的修饰符为 private,子类构造方法必须显示地调用 super()

将上面的 Parent 类修改一下:

public class Parent {
	
	// 注意:此时修饰符为 private
	private Parent() {
		System.out.println("父类 private 的无参构造方法");
	}
	
	// 带1个参数的构造方法
	public Parent(int val) {
		System.out.println("父类构造方法(1) "+val);
	}
	
	// 带2个参数的构造方法
	public Parent(int val, String str) {
		System.out.println("父类构造方法(2) " + val + " " + str);
	}
}

此时 Parent 的无参构造方法修饰符为 private,但子类无法调用修饰符为 private 的父类构造方法(原因后面会讲)。所以 Child 的构造方法必须显示地调用 super()。例子如下所示:

public class Child extends Parent{

	// 子类无参构造方法
	public Child() {
		// super()  // 错误,因为父类不存在无参构造方法
		super(1);   // 一定要调用 super,否则会报错
	}
	
	// 子类有1个参树的构造方法
	public Child(int val) {
		super(val);
	}
	
	// 子类有2个参树的构造方法
	public Child(int val, String str) {
		super(val, str);
	}

	public static void main(String[] args) {
		Child c1 = new Child();
		Child c2 = new Child(2);
		Child c2 = new Child(1, "str");
	}
}

当父类不存在无参构造方法时,情况也是一样的。此时如果子类的构造方法中没有调用 super,编译时是无法通过的。而且,子类此时也不允许存在未定义构造方法的情况。

super 调用父类方法和成员

接下里我们往Parent里面添加3个权限修饰符分别为 publicprotectedprivate 的3个方法以及3个成员,代码如下所示:

public class Parent {
	public static final int CONSTANT_VALUE = 1; // 修饰符为 public
	protected int val;  // 修饰符为 protected
	private int privateValue;    // 修饰符为 private
	
	// ...省略构造方法...
	
	// 未添加任何权限修饰符的方法
	void method() {
		System.out.println("父类无任何修饰符的方法");
	}

	// 修饰符为 public 的方法
	public void publicMethod() {
		System.out.println("父类 publicMethod");
	}
	
	// 修饰符为 protected 的方法
	protected void protectedMethod() {
		System.out.println("父类 protectedMethod");
		
	}
	
	// 修饰符为 private 的方法
	private void privateMethod() {
		System.out.println("父类 privateMethod");
	}

}

接下来是 Child 的代码:

public class Child extends Parent{

	// ...省略构造方法...
	
	// 子类的方法
	public void newMethod(){
		super.method();        // 注意:调用无添加权限修饰符的方法
		super.publicMethod();  // 调用修饰符为 public 的方法
		super.protectedMethod(); // 调用修饰符为 protected 的方法
		// super.privateMethod();  // 不可调用修饰符为 private 的方法
		super.val = 2;  // 可以调用修饰符为 public 的成员变量
		int i = super.CONSTANT_VALUE;  // 可以调用修饰符为 protected 的成员变量
		// super.privateValue = 3;   // 修饰符为private,不可调用
	}
	
}

在子类方法 newMethod 中可以看到 super 的另一个作用,即调用父类的方法和成员调用父类方法时,使用 super.XXX(),其中 XXX() 表示父类的方法名;而调用父类成员时使用 super.x,其中 x 表示父类的成员。使用 super 调用父类的方法或者成员时,有以下 3 点需要注意:

  1. 权限修饰符为 private 的方法和成员不可被调用。上述例子中的 privateMethodprivateValue 就无法被调用。而权限修饰符为 publicprotected 的方法和成员均可被调用。特别强调 protected,在继承中 protectedpublic 可以看作是相等的,不区分包内或包外。这个规则对于构造方法的调用也同样适用。
  2. 父类方法和成员的类型无论是静态、动态、常量还是变量都可被调用。简单一句话,父类任何方法和成员都可被调用private 的除外)。
  3. 看到我们父类 Parentmethod 方法,它没有添加任何的权限修饰符,根据 Java 的规则,它的调用只能发生在包内继承,一个包外的类继承 Parent 时,无法调用该方法。这个规则同样适用于成员和构造方法。

总结一下 super 的用法

  1. super 表示超类,也就是父类。例如我们例子中的 Parent
  2. super() 可以调用父类的构造方法,在子类中,如果父类含有无参构造方法,则在子类的构造方法中即使不使用 super(),系统也会隐式地为子类构造方法添加 super()
  3. 父类不含无参构造方法父类无参构造方法修饰符为 private 时,子类的构造方法一定要显示地使用 super(params)
  4. super() 会根据括号内的参数个数和参数类型选择调用父类的哪个构造方法。
  5. super 可以调用权限修饰符为 publicprotected 的父类方法和成员,private 的方法和成员不可调用!调用方式为 super.XXX()super.x

重写方法

子类是可以重写父类除 private 之外的方法。重写方法前面一般都会加上一个注解符 @Override,表示这是子类的重写方法,在我们重写父类方法时推荐加上此注解符。

可以不重写的情况

在父类为实例类的时候,我们可以选择不重写父类的方法,但是我们仍然可以对它进行调用,例子如下所示:

public class Parent {
	public void publicMethod() {
		System.out.println("父类的方法");
	}
}


public class Child extends Parent{
	public static void main(String[] args) {
		Child child = new Child();
		child.publicMethod();  // 即使不重写仍然可以调用
	}
}

运行结果:

父类的方法

可以看到,child 调用了父类的 publicMethod 方法,为什么会这样子呢?因为继承实际上就是继承了父类的属性和方法,简单来说就是,父类有的,子类也有
有时候父类的方法并不一定能满足我们的要求,这时候我们可以重写父类的方法,例如子类调用 publicMethod 我不想打印 父类的方法 而是想打印 子类的方法 ,我们可以对子类做如下修改:

public class Child extends Parent{
	
	// 重写了父类的 publicMethod 方法
	@Override
	public void publicdMethod() {
		System.out.println("子类的方法");
	}
	
	public static void main(String[] args) {
		Child c = new Child();
		c.publicdMethod();
	}
}

此时运行结果就会打印出 子类的方法
在重写方法时我们还可以对重写方法的权限修饰符进行修改,如下所示:

public class Child extends Parent{
	// 重写修饰符为 protected 的父类方法
	@Override
	public void protectedMethod() {
		super.protectedMethod();
	}

	public static void main(String[] args) {
		Child c = new Child();
		c.protectedMethod();
	}
}

我们在这里重写了父类修饰符为 protected 的方法 protectedMethod。在这里我们可以把重写方法的修饰符修改为 public,这是合法的。但是要注意修饰符只能从小范围往大范围改,不能从大范围往小范围改。例如 protected 可以修改为 public,但是 public 不可以修改为 protected

总结一下:

  1. 子类重写父类的方法时推荐在重写方法前面加上注解符 @Override
  2. 子类只能重写父类修饰符为 publicprotected 的方法。
  3. 子类重写父类方法并不是必须的(父类为抽象类除外),可以在我们认为需要重写的时候才进行重写,子类仍然可以调用父类的方法。
  4. 子类重写父类方法时方法的权限修饰符可以进行修改,但是只能从小范围向大范围修改

必须重写的情况

当父类为抽象类的时候,父类的抽象方法必须重写。那么为什么父类要使用抽象类?
在实际应用中,往往会将父类作为抽象类被继承。有时候我们的父类无法进行具体描述,例如光说一只鸟并不知道这只鸟是长什么样的,它有可能是鸽子那么小,也有可能是老鹰那么大。所以我们就用抽象类作为父类进行方法的定义就好。例子如下所示:

public abstract class Bird {
	// 定义抽象方法
	abstract void size();
}


public class Dove extends Bird{
	@Override
	void size() { // 重写抽象方法
		doveSize();
	}
	
	public void doveSize(){	
		System.out.println("像鸽子那么小");
	}

}

这里我们将鸟类定义为抽象类,抽象的关键字是 abstract。因为不同鸟类有不同的大小,所以我们只在抽象类中定义一个抽象方法 size。我们知道,抽象类是无法被实例化的,抽象方法也是没有方法体的,除非它被重写,否则抽象方法没有意义。所以继承的子类必须重写父类的抽象方法使得该方法具有意义,才能在子类实例化后被调用。接下来我们讲解一下对象类型的转换这一部分内容,这也是很重要的一块内容。

对象类型的转换

对象类型的转换是 Java 中常见的一种操作,包括2种形式,向上转型向下转型,接下来分别介绍它们的使用。
我们继续使用上面的鸟类的例子,为它多添加一个子类麻雀:

// 父类:鸟类
public abstract class Bird {
	void abstract size();
}

// 子类:鸽子
public class Dove extends Bird{
	@Override
	void size() {
		System.out.println("像鸽子那么大");
	}
	
	public void sendMessage(){
		System.out.println("鸽子会送信");
	}
}

// 子类:麻雀
public class Sparrow extends Bird{
	@Override
	void size() {
		System.out.println("像麻雀那么小");
	}
	
	public void speak(){
		System.out.println("麻雀会叽叽叫");
	}
}

向上转型

在我们的这个例子中,我们的继承关系图如下所示:
继承关系图
向上转型的意思就是,将子类对象看做是父类对象是就叫做向上转型,即将一个具体的类转换为一个比较抽象的类。对于我们的例子来说也就是具体的鸟类(鸽子、麻雀)转换为鸟类这个父类,对比继承关系图是向上走的一个操作,示例代码如下:

public class Test {
	public static void main(String[] args) {
		Bird bird1 = new Dove();      // 向上转型
		Bird bird2 = new Sparrow();   // 向上转型
	}
}

main 方法中的对象 bird1bird2 分别通过 DoveSparrow 的构造方法进行实例化,实例化之后的对象 bird1bird2 对象类型均为 Bird。但是要注意二者并不一样,虽然二者的对象类型都是 Bird 但是 bird1Dove 的实例对象bird2 Sparrow 的实例对象

向下转型

向上转型是往抽象的转换,那么向下转型就是往较为具体的类做转换了。这样的转型通常会出现问题,例如我们不能说所有鸟类都是鸽子,车是公交车的一种,这与逻辑不相符。所以说子类对象总是父类的一个实例,但父类对象不一定是子类的实例,修改 Test 代码如下:

public class Test {
	public static void main(String[] args) {
		Bird bird1 = new Dove();      // 向上转型(将鸽子看作鸟类)
		Bird bird2 = new Sparrow();   // 向上转型(将麻雀看作鸟类)
		// Dove dove = bird1     // 直接将父类对象赋予子类将会产生异常
		Dove dove = (Dove)bird1;          // 向下转型(将父类对象赋予子类对象需要强制转换)
		Sparrow sparrow = (Sparrow)bird2; // 向下转型(将父类对象赋予子类对象需要强制转换)
	}
}

可以看到 main 方法中的对象 dovesparrow 需要使用显示类型转换。因为父类对象不一定是子类的实例。就好比一只鸟不一定就是鸽子,它还有可能是麻雀等其它鸟类。当我们在做向下转型时,就需要告知编译器我们需要的是什么,所以必须使用显示类型转换。
但是为什么在向上转型的时候我们却不需要使用显示类型转换呢?因为麻雀/鸽子都是鸟类,编译器已经知道了我们要的是鸟类,所以在向上转型时不需要使用显示类型转换。

instanceof

在对象类型转换的结尾再介绍一个操作符 instanceof。这个操作符的作用非常简单,就是用于判断父类对象是否为子类对象的实例,是会返回 true,否会返回 false。它的用处是用于保证向下转型的安全性,因为当父类对象不是子类对象的实例时会抛出 ClassCastExecption 异常,示例代码如下:

public class Test {
	public static void main(String[] args) {
		Bird bird = new Dove();
		if(bird instanceof Dove){  // 这里会返回true 
			Dove dove = (Dove)bird;
		}
		
		if(bird instanceof Sparrow){ // 这里会返回 false
			Sparrow sparrow= (Sparrow)bird;
		}
	}
}

这些其实都很好理解,bird 向上转型为 Bird,它同时也是子类 Dove 的实例对象,所以 bird instanceof Dove 会返回 true,而 bird instanceof Sparrow 则返回 false,因为它并不是 Sparrow 的实例对象。我们可以通过 instanceof 进行类型判断来保证我们向下转型的安全。
instanceof 也可以对接口类型进行判断,这个会在我们接口的介绍中涉及到,这里不做介绍。

继承的其他注意事项

1. Java 中规定,类不能同时继承多个父类,所以下面的继承是不合法的:

public class A extends B, C{ // 不能同时继承多个类
}

如果我们在实际使用中确实有继承多个类的需求,我们应当考虑使用接口来满足我们的需求。

2. Java 是支持多重继承的,例子如下所示:

// B 继承自 C
public class B extends C{ 
}

// A 继承自 B
public class A extends B{ 
}

总结

文章最后总结一下继承涉及到的知识:

  1. 当 A 继承 B 时,B 为 A 的超类或父类,A 为 B 的子类。
  2. 子类中可以用 super 调用父类非 private 修饰的所有方法和成员。如果子类构造方法没有调用 super(),那么会隐式地调用 super()。如果父类中的无参构造方法不可调用,子类必须显示地调用 super()
  3. 权限修饰符 publicprotected 在继承中效果是一样的,即 protected 不会在继承中区分包内或包外才可调用方法或成员。当方法或成员没有权限修饰符进行修饰时,只有包内继承的类才可以调用。
  4. 父类为抽象类时父类的抽象方法必须重写,当父类为普通类时可以选择不重写直接继承父类的方法。
  5. 对象类型的转换可分为向上转型和向下转型,向下转型时推荐使用 instanceof 进行判断以保证安全性。
  6. Java 不支持同时继承多个类,但是支持多重继承,如果有同时继承多个类的需求应该考虑使用接口。

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/84961649