JavaSE 拾遗(5)——JavaSE 面向对象程序设计语言基础(5)...继承和多态


上一篇博客《JavaSE 拾遗(4)——JavaSE 面向对象程序设计语言基础(4)》说了封装,这次接着说继承和多态。

在人们认识事物时,通常是先有子类,再有父类,再有更多的其他子类,这么一个过程,这个奠定了人们描述事物的体系结构。所以继承不是简单的加法关系,更重要的是语义上的抽象层次的不同,所以在给有继承关系的类命名的时候一定需要体现出语义上的同源关系和层次关系。不应该单纯不为复用而抽象,而是为概念而抽象,正如李氏替换原则“在使用定义的类的时候,子类型必须能够替换它们基类型”。extends 主要表述了 is a 的关系。编程语言中的继承,正好实现了对世界中事物描述的时候,分层,分粒度的描述的方法。如果说封装是对你要描述的现实中的东西进行拆分组合,那么继承可以看做是对面向对象表述方法自身的拆分组合。比如,一个小的封闭空间,里面有 男人小明、女人小芳、一只哈士奇狗、还有一座房子,描述这个整体的系统,我们可以说系统是 动物 和 房子 组成,还可以说是 人、狗、房子 组成,还可以说是男人、女人、狗、房子组成,这就是 分层 分粒度 描述事物的方法。所以面向对象程序设计就有两个问题,一个是怎么学习使用别人建立好的系统,二是怎么正确构造自己的系统。这样描述系统,就产生了两种描述系统的方法:一是纵向,二是横向。一般先是拆分事物,通过 组合 聚合 关联 的关系,描述事物,再是把拆分开的部分分类,比较异同,通过 继承 的方式来描述事物。现实中对事物的描述需要结合着两种方法。拆分事物,我们可以看做是封装来实现的,后面一种方法则对应于继承,其实封装也体现了拆分组合,继承也体现了拆分组合。


继承

在 java 中和继承相关的内容有

  • 继承 和 extends 关键字
  • 父子类中变量的特点
  • 父子类中函数的特点
  • 父子类中构造函数的特点
  • final
  • abstract
  • interface

继承 和 extends 关键字

继承,就是子类得到父类所有成员,通过 extends 关键字表述父类子类的关系,java只支持单继承,多实现,因为多继承会带来安全隐患。

多继承为什么如此重要,因为越是底层的事物,越是具有多个方面的特性,每个方面是一种看待事物的角度,从每一个角度看事物,都可以有一个纵向的关系。多个纵向的关系的描述就可以用多继承来实现。

在 java 里面我们怎么选择这些多种纵向关系中的一种来作为继承呢?如果说方法的不安全性是 java 不支持多继承的一方面,那么更重要的一方面就是 java 把继承这种机制做了主次分类,主就是 extends ,次就是 implements,意思是说类必须有且只有一个主的分类,用 extends。我们在定义类的时候,一个最重要的原则就是单一职责原则,那么我们表达主纵向关系的那个角度一定要选择类的单一职责所表示的角度,这样才能体现类的主要功能。和 c++ 相比,这或许就是 java 的优越性,java 把这种类的主要功能和扩展功能用 extends 和 implements 分开了。接口是对 扩展功能的抽象,而父类是对主要功能的抽象。

如何使用一个继承体系中的功能?要想使用体系,先查阅体系父类的描述,因为父类中是该体系的共性类容,通过了解共性内容就可以了解体系的基本功能,整个体系的了解过程有点象二叉树查找的过程,在从根类往子类走,了解完一个分支,再学习其他分支。


父子类中变量的特点
  • 子类可以继承父类的成员变量
  • 子类还可以覆盖父类的成员变量
hotspot jvm 对象的内存结构如图:

关于对象内存结构更详细的内容可以看 这里
this.xxx 为什么可以表示本类对象成员?
在给子类对象中的成员变量分配堆空间时,也会给父类成员变量分配空间,就像上图一样。在通过对象调用成员函数的时候,jvm 会把这 this 压入 java 栈 的当前函数栈帧的局部变量表第0个单元,所以类中的函数通过这个引用可以访问到该对象的实例的所有字段;通过该对象同样可以访问到方法区中的函数,是因为对象的头部有个单元保存该类的 Class 实例的引用,JVM 取得方法区引用后,就可以解析类中涉及到的所有的 符号引用,比如方法名、字段名、类名等等。结合函数内部 "this.成员变量名"  的表示方法,成员函数就能准确的操作堆区中成员变量的数据,this.成员方法名,也可以检索到方法区中方法的符号引用,并且在压入栈帧的时候,把 this 保存在局部变量表,这就是为什么类中非静态成员的方法都要使用 this.xxx 的格式。
super.xxx 为什么可以表示父类对象成员?
在子类中使用 super.字段 ,编译器会直接把这个解析为父类字段的符号引用,因为子类对象中也存在该父类字段,所以会直接访问到父类字段。
/**
需求:演示 super、函数多态 的原理

思路:

步骤:
*/
public class Test extends Father{
	int i = 10;

    public static void main(String[] args) {
		// 测试 super.字段和 super.方法
		new Test().println();
		// 测试方法的多态
		Test t = new Test();
		Father f = new Test();
		t.invokeMethod();
		f.invokeMethod();
    }

	void println(){
		// 测试 super.字段
		System.out.println(this.i + "   " + super.i);
		// 测试 super.方法
		this.invokeMethod();
		super.invokeMethod();
		this.privateFunction();
		super.privateFunction();
	}

	void invokeMethod(){
		System.out.println("子类 invokeMethod++++" + i);
	}
}

class Father
{
	int i = 9;

	void invokeMethod(){
		System.out.println("父类 invokeMethod   " + i);
	}

	public void privateFunction(){
		invokeMethod();
		System.out.println("父类 privateFunction   " + i);
	}
}
运行结果是 :

javap 反汇编 Test.class 结果为:
Compiled from "Test.java"
public class Test extends Father {
  int i;

  public Test();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method Father."<init>":()V
       4: aload_0       
       5: bipush        10
       7: putfield      #2                  // Field i:I
      10: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class Test
       3: dup           
       4: invokespecial #4                  // Method "<init>":()V
       7: invokevirtual #5                  // Method println:()V
      10: new           #3                  // class Test
      13: dup           
      14: invokespecial #4                  // Method "<init>":()V
      17: astore_1      
      18: new           #3                  // class Test
      21: dup           
      22: invokespecial #4                  // Method "<init>":()V
      25: astore_2      
      26: aload_1       
      27: invokevirtual #6                  // Method invokeMethod:()V
      30: aload_2       
      31: invokevirtual #7                  // Method Father.invokeMethod:()V
      34: return        

  void println();
    Code:
       0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: new           #9                  // class java/lang/StringBuilder
       6: dup           
       7: invokespecial #10                 // Method java/lang/StringBuilder."<init>":()V
      10: aload_0       
      11: getfield      #2                  // Field i:I
      14: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      17: ldc           #12                 // String    
      19: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: aload_0       
      23: getfield      #14                 // Field Father.i:I
      26: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      29: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      32: invokevirtual #16                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      35: aload_0       
      36: invokevirtual #6                  // Method invokeMethod:()V
      39: aload_0       
      40: invokespecial #7                  // Method Father.invokeMethod:()V
      43: aload_0       
      44: invokevirtual #17                 // Method privateFunction:()V
      47: aload_0       
      48: invokespecial #18                 // Method Father.privateFunction:()V
      51: return        

  void invokeMethod();
    Code:
       0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: new           #9                  // class java/lang/StringBuilder
       6: dup           
       7: invokespecial #10                 // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #19                 // String 子类 invokeMethod++++
      12: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: aload_0       
      16: getfield      #2                  // Field i:I
      19: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      22: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: invokevirtual #16                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      28: return        

  public void privateFunction();
    Code:
       0: aload_0       
       1: invokespecial #18                 // Method Father.privateFunction:()V
       4: return        
}
如上面 println 函数的11 23 36 40 索引位指令所示,super 在编译器时期转换为符号索引。
如上面 main 函数的 27 31 索引位指令所示,编译器在编译时期只是按照引用类型变量的类型转换为符号引用,但是传递的 this 却是子类对象的 this, jvm 在执行改函数的符号引用解析的时候,是结合目前栈帧中的 this + 方法区中的class字节码来解析,如果函数符号的引用是 this ,符号引用表示是本类的符号引用,那么 jvm 先是查找子类中有没有该函数,再是查找父类中有没有该函数,这种动态解析的方法也叫做动态绑定,是实现函数多态的核心技术。

1.编译器对类中的函数中出现的函数和字段的解析有两种情况,一种是源代码通过某个对象的引用来使用函数和字段,这时编译器根据该引用的类型来解析该字段,jvm 执行的时候的解析需要结合对象的实际类型来解析;一种是源代码中通过的 this 或者 super 来函数和字段,那么编译器根据 this super 对应的类型解析,因为这是显示声明区分父类和子类成员。

2.JVM 解析函数符号引用的时候,也是根据符号和对象的引用来解析,如果函数符号是:
invokevirtual #6                  // Method invokeMethod:()V
传递的 this 对象,那么先根据 this 对象中的 Class 引用,解析函数,如果该 Class 中有这个函数符号,那么就直接使用该 Class 中的函数符号,如果没有,再解析该类的父类中是否有子类可以访问的该函数;如果是函数符号是:
 invokespecial #7                  // Method Father.invokeMethod:()V
传递的是 this 对象,那么就直接根据 this 对象中的 Class 引用,找到该 Class 的父类 Class ,查看是否有该函数存在,如果没有,再查看 父类的父类的 Class,是否有子类可以访问的该函数符号存在。
所以在,在子类中使用 super 可以无视子类符号引用的隐藏作用,直接匹配父类的 符号引用。但是对于没有使用 super 的函数,无论是父类中的还是子类中的符号引用,都要按照 this 所指的 Class 开始查询。

3.JVM 解析字段符号引用的时候,根据编译时期的结果,直接去对应的区域中取值,不会发生像函数一样的多态性,如果是在子类对象的字段区查找,只有在子类中没有这个字段的符号引用的时候,才会查找父类是否有该字段。

super 主要的作用就是,用在子类中,就是显示的指定不是覆盖。所以会直接调用父类的成员,包括调用父类成员函数的时候,如果成员函数中有访问覆盖的成员变量,也是访问父类的成员变量。
父类函数访问的范围只限于父类成员,
根据父类成员函数的修饰符不同,this 传送的值也不一样,private 修饰的父类函数,在子类对象调用的时候传子类对象 super 引用给成员函数中的 this,其他修饰符的传子类对象的 this 引用给成员函数的 this。
所以结果是子类对象在执行父类函数的时候,如果函数的修饰符是 private 那么,该函数的访问范围只能在父类对象范围内,既只能访问父类的成员函数,只能使用父类对象的成员变量,如果不是 private 那么函数的访问范围就是整个父子类范围,此时,如果子类有成员和父类重名,那么就算子类成员覆盖父类成员。成员变量覆盖的现象很少使用。
子类对象中包含父类对象的成员,所以可以向上转型,也就有了对象的多态性,成员变量的多态性。

父子类中函数的特点
  • 子类可以继承父类的成员函数
  • 子类还可以覆盖(隐藏)父类的成员函数
什么是函数覆盖,在具有父子类关系的两个类中,父类和子类各有一个函数定义完全相同的函数,这就是函数覆盖。函数的三要素 函数名、返回值、参数列表 必须一样,才能形成子类函数覆盖父类函数。覆盖的时候,子类函数访问权限必须大于等于父类函数访问权限,静态只能覆盖静态。
当子类继承了父类,沿袭了父类的功能,对于某功能,子类虽然具备该功能,该功能在父子类中,概念上意义相同,但是功能的内容却不相同,这时没有必要定义新的功能,而是使用函数覆盖。函数的覆盖,形成了函数的多态。

父子类中构造函数的特点
构造函数不存在覆盖。在子类对象进行初始化的时候,父类构造函数也会运行。
因为子类构造函数默认第一行有一条隐式的 super 语句,super()。
父类中的数据,子类整个范围都可以直接使用,包含子类构造函数,所以子类对象在创建时,首先调用父类构造函数进行父类成员变量的初始化。
父类没有空参数构造函数的时候,子类必须手动使用 super 语句,调用父类构造函数。

final关键字
final 关键字可以修饰 类 变量 成员函数
修饰类,表示类不能被继承,这就是系统结构的最底层了
修饰函数,表示函数不能被复写,这样保证了类的封装性,因为继承能破坏封装性。
修饰变量,修饰变量,表示这个变量只能被赋值一次,相当于常量使用。
final 修饰是严谨的一种表现,也是类结构体系的标志
final主要还是用来作为类结构体系的标志
不要提前优化,所以,final 表示严谨的话,可以不用先加
内部类定义在类中局部位置上时,只能访问该局部被 final 修饰的局部变量

abstract关键字
abstract可以修饰 函数、类
abstract修饰函数的时候,表示函数是抽象函数,没有函数体的函数,抽象函数用来表述 不明确函数主体的函数,抽象函数在父类中仍然有实实在在的意义,抽象函数表示只进行功能定义抽取,而不进行功能体的抽取,抽象函数的存在可以完善父类的概念,增加程序的易读性。抽象函数只能定义在抽象类中。
abstract修饰类的时候,表示该类是抽象的,可能含有抽象函数,并且不能用来创建对象。
模板方法
/**
需求:演示模板方法模式

思路:	模板方法,又叫方法模板,就是一个方法,虽然我们知道其功能,但是我们
		只能确定其中前面部分和结尾部分内容,按照现在的程序设计理论和方法——
		顺序 分支 循环,我们没法完整的实现这个函数。那么,我们就把这个这个
		方法拆分为两个方法,其中已知的部分写一个方法,未知的部分写一个方法,
		在已知部分的内部适当的位置,调用未知的部分,那么就能完成了我们希望
		的整体的功能。当未知的部分确定以后再用子类函数覆盖父类函数的方法,
		实现未知部分。这种程序的结构,就叫做模板方法模式,已知部分的方法,
		就叫做模板方法。模板方法是一种把已知未、知拆分的方法。

	 函数参数	表达了只有等到程序执行时才能知道的数据
	 模板方法模式	表达了只有等到程序执行时才能知道的行为
	 反射		表达了只有等到程序执行时才能知道的类名

	 模板方法其实完全可以不使用,也能实现想要的功能,就是等已知功能的全
	 部行为的时候,才在子类把 knownMethod 方法完全实现。

	 这也说明了,系统模型的设计 和 oop 的设计的不同。系统模型设计要求自然、
	 易懂,oop 设计的时候,要求易扩展、易复用。我认为不应该牺牲易读性,来
	 实现 易扩展、易复用。应该尽量实现易读性,还实现易扩展性,不要一味强求
	 易复用。过早追求易复用和过早优化是一样的道理,过早优化不是优雅的设计。
	 
	 模板方法模式,可以用来表达现实中功能延迟确定的情况。

	 继承打破了封装性,这里也有表现,按道理,父类的方法只要不想对外提供接口
	 的都应该是 private 属性,但是为了让子类可以使用,需要把限制放宽,否则子
	 类不能覆盖,覆盖也属于在子类中使用父类方法的情况,覆盖就打破了父类封装性

步骤:
*/
abstract class TemplateMethod
{
	public final void knownMethod()
	{
		System.out.println("a");
		unknownMethod();
		System.out.println("b");
	}

	void unknownMethod()
	{
		System.out.println("c");
	}
}

class TemplateMethodChild extends TemplateMethod
{
	void unknownMethod()
	{
		System.out.println("d");;
	}
}

class TemplateMethodMain
{

	public static void main(String[] args) 
	{
		TemplateMethod part = new TemplateMethodChild();	//输出 adb
		part.knownMethod();
		System.out.println("Hello World!");
	}
}
可以说模板方法是以继承的形式把 method 未知部分和已知部分分开,其实还可以用组合的方式把 method 未知的部分和已知的部分分开,把未知的部分单独封装为一个类B,在已知部分的类 A 中定义一个引用指向这个类的对象,在已知部分的方法中调用类B的对象的未知部分功能。就像 Comparator 和 Comparable 接口直接的关系。

多态

多态,就是指某一种事物具有多种表现形态
多态的组成:
成员函数多态性——覆盖时一个函数定义有多种函数体
对象多态性——一个对象可以属于类及其父类
多态的好处是提高程序扩展性,用接口形式表现的多态更加符合语义,所以我们有个设计原则叫依赖接口,不依赖实现,使程序的表达更加简洁。
多态中成员函数的特点是:编译期间参阅引用变量所属的类中是否有调用的方法,运行期间参阅对象所属的类中是否有调用的方法
多态中成员变量和静态函数,编译运行时都参阅引用所属的类,所以只有成员函数能表现出对象的多态性

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值