详解Java的继承机制和继承的内部处理

一、继承机制

       子类会自动继承父类的成员供自己使用,但有时候该成员可能不符合子类的要求。一直简单的解决办法是不使用它,另外取名定义新的奕量和方法。 但有时取名是一件麻烦事,而且子类的使用者有可能在无意中使用了设计者不愿意提供的成员(因为子类没有修改它的访问权限)。一种彻底的解决办法是为成员取一个同样的名字,并重新定义它的值或行为,将父类的同名变量和方法遮盖掉。
       子类的成员变量和父类的同名,称为父类的成员变量(属性)被隐藏;如果是成员方法同名,称为父类的成员方法被覆盖。

1.属性的隐藏

只要子类中的成员变量与父类的同名,就可以将父类的变量隐藏起来,一般情况下使用的就是子类的同名变量,不过有些细节还是要注意,比如访问权限修饰符,常量修饰符,静态修饰符、数据类型说明符等,Java允许这些修饰符不同。下面讲解几种情况

       1.修饰符完全相同的情况:
       这个无需多解释,各类修饰符完全相同的隐藏就是最基础的。

       2.访问权限不相同的情况:
       Java中规定,子类用于隐藏的变量可以和父类的访问权限不同,如果访问权限改变,则以子类的权限为准。

       3.数据类型不相同的情况:
       Java允许子类的变量和父类变量的类型完全不同,以修改后的数据类型为准。

       4.常量修饰符不同的情况:
       Java允许父类的变量被子类的常量隐藏,也可以是父类的常量被子类的变量隐藏。

       5.静态变量修饰符不同的情况:
       Java允许用实例成员变量来隐藏静态成员变量,也允许以静态成员变量来隐藏实例成员变量

综上这些概括起来就是:子类变量可以修改继承下来的父类变量的任何属性,使用子类对象时,以修改之后的属性为准。

2.方法的覆盖

子类中,如果继承下来的方法不能满足自己的需要,可以将其重写一遍,这称为
覆盖”(也叫重写)。覆盖必须满足以下两个条件。
  1.方法名称必须相同。
  2.方法的参数必须完全相同,包括参数的个数、类型和顺序
如果只满足第一条,不满足第二条,那么就不是覆盖,而是重载。由于方法不仅有各种权限修饰符,而且还有返回类型修饰符,所以它的覆盖比成员变量的隐藏规则要复杂一些。**原则上,如果覆盖成功,那么使用子类对象时,方法的所有属性都以覆盖后的为准。**下面分类来介绍覆盖时的一些要求。

       1.修饰符完全相同的覆盖:
       最基本的,也是最简单的。

       2.访问权限不相同的情况:
       子类方法的访问权限可以与父类的不相同,但只允许权限更宽松,而不允许更严格,它遵循的是“公开的不再是秘密”这一原则,没有任何办法能够改变这一原则,这一点和C++有很大的区别。

       3.返回值数据类型不相同的情况:(这条是可以区别重写和重载的)
  如果返回的是基本类型,那么在覆盖时不允许出现返回值数据类型不相同的情况。也就是说,覆盖与被覆盖的方法的返回值数据类型必须完全相同。
  如果返间类型是复合类,时问类型必须相容。也就是说,或者完全相同,或者子类的返回类型与父类的返回类型存在继承关系。

       4.final修饰符不同的情况:
  若方法前面用final修饰,表示该方法是一个最终方法,它的子类不能覆盖该方法。反之,一个非最终方法可以在子类中指定final修饰符,将其变成最终方法。

       5.静态修饰符不同的情况:
  Java规定,静态方法不允许被实例方法覆盖,同样,实例方法也不允许用静态方法覆盖。也就是说,不允许出现父类方法和子类方法覆盖时的static修饰符发生变化。

3.构造方法没有继承机制

从形式上看,构造方法比普通方法要简单:它没有返回值,没有 static、final等x修饰符,而且一般不会用 private修饰。按照某些教材的说法,构造方法也如普通成员方法一样可以被继承,只是有一些特殊性。但是Sun公司在《Java语言规范》中明确规定构造方法不是成员方法,所以它不遵循成员方法的继承规则,而且它根本不会被继承。

       1.无参数构造方法的自动调用机制
       按照Sun公司的解释,系统是自动为子类添加了一个不带参数的构造方法,而子类这个构造方法又会自动调用父类无参数的构造方法。这种调用就是通过super关键字来实现的。
在这里插入图片描述
这里是自动调用父类的无参构造方法,即使我们不加super,系统也会默认加上

       2.带参数的构造方法不会被自动调用
       系统只提供不带参数的构造方法,没有提供带参数的构造方法,自然无法调用带参构造方法,只能显式的使用super进行调用
在这里插入图片描述
这里我注释掉就会报错

下面这个就可以综合体现这两点,第一,这个是会报错的,原因是无参数构造方法的自动调用机制,会给红色框那里加上super()方法,调用父类的无参构造,但是父类没有无参构造,所以报错。第二,那个蓝色框那里体现了带参数的构造方法不会被自动调用这个原理,只能显式的使用super调用
在这里插入图片描述
最后还要明确一点,由于构造方法不会被继承,也就不存在覆盖的问题。Java还规定,子类中无论哪个构造方法在执行时,都会先执行父类中无参数的构造方法,除非显示地调用了其他的构造方法。(就像上面那个例子的蓝色框里面实现的一样)



二、继承的内部处理

       对于父类,当它被子类继承后,并非复制了一份成员方法和成员属性到子类的空间中,它仍然只在父类空间中存在一份,子类通过继承链(本质上是指针) 来访问父类中的方法。如果程序中通过“子类对象名.成员方法名” 的方式使用成员,编译器会首先到子类中查找是否存在此成员,如果没有,顺着继承链到其父类空间中查找,依次往上推,如果找到Ob-ject类(该类为所有类的公共祖先)还未发现此成员,则编译器报错。

       由于父类的成员没有被复制到子类空间中,所以系统在生成子类对象时,会自动先生成一个父类的隐藏对象,父类如果还有父类,则以此类推。这就是为什么我们调用子类构造方法会自动添加 super()方法(即父类的构造方法),正是用于实现父类对象的构造。如果有多个父类对象,则所有对象共享一份成员方法,每个对象有各自的存储空间保存成员属性。
  
  在自动生成父类对象的过程中,必须保证父类的 class 文件可以访问到。 如果编译成功后,将父类的class 文件删除,运行时系统将会报错。

(这里可以小小的插一段我自己的理解,关于继承的话,有时候很多调用父类的方法以及使用变量其实都是对父类的那个隐藏对象在进行操作,本质上是子类对象通过继承链访问或者修改父类对应的那个隐藏对象,很多人就迷惑在这个地方,子类对象跟父类对象是不能混淆的(重写父类方法或者变量的话就是自己所拥有的了),只不过我们可以拿这个隐藏父类对象的一切,这就是继承)

下图为所示系统内部对继承的处理方法。
在这里插入图片描述

       从图中可以看出,即便成员变量b已经被覆盖,但只要调用方法f(),则仍然可以访问到被覆盖的父类变量b。(一般这个就是通过super实现)

详细了解继承的内部处理,下面我们来举几个例子
1.隐藏对象的讲解

public class Main {
	public static void main(String[] args) {
		B b = new B();
        b.view();
	}
}


class A {
    public int m = 1;
    public void view() {
        System.out.println(this.m);
    }
}
 
 
class B extends A {
	public int m = 2;
}

如果不是很清楚Java是如何实现继承的话,这个是很容易做错的,惯性思维会认为输出结果是2,但是我们可以很清楚的判断,B调用的是父类方法view。 方法中的this指代了当前的父类A创建的隐藏对象,所以调用的是该对象的m,输出结果应该是1。

运行结果:
在这里插入图片描述


2.同样的代码,我们改一下,我们重写一下父类的方法

public class Main {
	public static void main(String[] args) {
		B b = new B();
        b.view();
	}
}


class A {
    public int m = 1;
    public void view() {
        System.out.println(this.m);
    }
}
 
 
class B extends A {
	public int m = 2;
	public void view() {
        System.out.println(this.m);
    }
}

运行结果:
在这里插入图片描述
这次结果为2,原因就是我们重写了父类方法,子类已经拥有了,就不会去父类查找了,所以调用的是自己的方法,this指代的是当前B类创建的对象b,所以结果为2。


3.最后我们验证一下如果子类不存在成员变量之类的话,会不会去查找父类是否存在

public class Main {
	public static void main(String[] args) {
		B b = new B();
        b.view();
	}
}


class A {
    public int m = 1;
    public void view() {
        System.out.println(this.m);
    }
}
 
 
class B extends A {
	//public int m = 2;
	public void view() {
        System.out.println(this.m);
    }
}

运行结果:
在这里插入图片描述
很明显,输出结果为1,验证了我们的猜想。


当然我们上面所有的只是测验了一下成员变量,而对于成员方法而言,又是不是这样的呢?答案可能是出乎你的意料(具体详情可以了解一下Java静态绑定和动态绑定机制,相信这些你一定会更加吃透)

推荐一篇阅读:https://blog.csdn.net/weixin_43465312/article/details/101542326

发布了149 篇原创文章 · 获赞 84 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/weixin_43465312/article/details/101224003
今日推荐