Java软件设计基础 5. 继承与多态
类的封装 类的特性 Java程序设计的基本单位是类,类是用来创建具体对象的模板,有以下三个重要特性: 封装 继承 多态 Java语言中,对象就是对一组变量和相关方法的封装(Encapsulation) ,通过对对象的封装,实现模块化和信息隐藏。通过对类的成员辅以一 定的访问限制,实现类中成员的信息隐藏。 继承 继承(inheritance)允许创建分等级层次的类,在Java中,所有的类都 是直接或间接的继承java.lang.Object得到的。 多态 多态(Polymorphism)是指同名的不同方法在类程序中共存,系统根据 传递参数个数或类型的不同来决定调用哪一个方法。
类的封装 封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据不受外部干扰且不易被误用。 利用抽象数据类型将数据和基于数据的操作结合在一起,数据被保护在抽象数据类型的内部,系统的其他部分只有通过包裹在数据之外被授权的操作,才能够与这个抽象数据类型进行交互。 类是Java封装的基本单元。 一个类定义了将被一个对象集共享的结构和行为; 每一个给定的对象都包含这个类定义的数据和操作; 对象也被看成是类的实例(instance of a class); 类是逻辑结构,而对象是真正存在的物理实体。 封装可以提高程序中数据的完整性和安全性;开发过程的复杂性;减少出错可能;提高类或模块的可重用性。
访问权限 类中的每个方法或成员变量的访问权限都可以通过修饰符(public、private、protected、default)被标记。这种访问权限控制实现了一定范围内的信息隐藏。 private 被修饰的成员变量和方法称为私有变量和私有方法。 类中限定为private的成员变量和方法只能被这个类本身的方法访问,不能在类外通过名字来访问。 x has private access in A y( ) has private access in A
对于私有成员变量和方法,只有在本类中创建该类的对象时,这个对象才能访问自己的私有变量和类中的私有方法。 同一个类的不同对象可以访问对方的私有变量或调用对方的私有方法,这是因为访问保护是控制在类的级别上,而不是对象的级别上。 在类内部创建对象,可以访问自己的私有变量和私有方法。
protected 构造方法也可以限定为private,如果一个类的构造方法被声明为私有,则其他类不能生成该类的一个对象。 编译报错
public friendly 修饰的成员变量和方法被称为公共变量和公共方法,可被所有的类访问。 这是缺省情况,限定为友好的成员可以被这个类本身和同一个包中的其他类访问,但是对于包外的所有类就不能访问。
5.2 类的继承 继承机制 继承是面向对象编程技术的一块基石。 继承是一个对象获得另一个对象的属性的过程。 继承能够创建一个通用类,并定义一系列相关项目的一般特性。该类可以被更具体的类继承。 每个具体的类都增加一些自己特有的东西。 继承是一个对象获得另一个对象的属性的过程。 由继承得到的类为子类(subclass或childclass),也被称为派生类(derived class)、扩展类(extended class)。 被继承的类为超类(superclass),也被称为基类(base class)或者父类(parent class),包括所有直接或间接被继承的类; 类可以从另一个类的子类的子类的子类……继承而来,最终都是继承自最顶级的类Object。 定义在Object中的方法可被所有子类使用。
Object Number ImaginaryNumber Java平台类层次结构:所有类都是Object类的后裔
继承支持按层次分类的概念。 单继承 简单的说,类的继承性就是新的子类可以从另一个父类派生出来,并自动拥有父类的全部属性和方法。 子类继承父类的成员变量与方法,就如同在子类中直接声明的一样,可以被子类中自己声明的任何实例方法所调用。 子类继承父类的状态和行为,同时也可以修改继承自父类的状态和行为,并添加新的状态和行为。通俗的说法:子类更具有特殊性。 单继承 使得代码更加可靠,不会出现因多个父类有相同的方法或属性所带来的麻烦。 继承用来为is-a关系建模。不要为了重用方法而盲目的派生一个类。例如,从Person类派生Tree类毫无意义。子类和父类之间必须存在is-a关系。
按层次分类 继承父类的属性与方法 添加新的特性(包括新的属性与方法)
创建子类 格式 说明 子类能够继承父类中public和protected成员变量和方法; 如果子类和父类在同一个包内,子类能继承父类中用默认修饰符修饰的成员变量和方法; 一般情况下,子类不能继承父类中的private成员变量和方法; 这里要特别说明的是,因为嵌套类可以访问其外部类的所有私有成员 ,所以,若子类是继承自某公共或者受保护嵌套类,则可以间接访问( 关于嵌套类,我们将在下节课中讲解)。 若缺省extends子句,则该类为java.lang.Object的子类。 [修饰符] class 子类名 extends 父类名{类体}
c has private access in SuperClass
如果子类声明了一个与父类中的成员同名的成员,则子类不继承父类中的同名成员; 以上程序将打印“5”,表明子类Sub中并未继承父类的a。
构造方法 与属性和方法不同,父类的构造方法并不传给子类,它们只能从子类的构造方法中通过关键字super调用。 构造方法链 在任何情况下,构造一个类的实例时,将会沿着继承链调用所有父类的构造方法,父类的构造方法在子类的构造方法之前调用。 调用父类的构造方法必须使用关键字super,并且这个调用必须是构造方法的第一条语句。在子类中使用父类构造方法的名字会引起语法错误。
关键字super Java中通过super来实现对父类成员的访问,super用来引用父类中的方法和变量,主要是用于继承被隐藏(hiding)的成员变量或者被覆盖的方法。 使用情况 访问父类被隐藏的成员变量,其引用格式为: 调用父类被覆盖的方法,其引用格式为: 调用父类的构造方法,其引用格式为: super.成员变量名 super.方法名([参数列表]) super([参数列表])
例程:自行车类 自行车类作为父类 三个属性:踏板步调、速度、档位 构造方法 提供四个方法: 设置踏板步调; 设置档位; 刹车减速; 提速;
自行车下面还有许多子类,如山地车、公路自行车等。 以山地车为例 除了拥有父类的三个属性,还有座高这一特有的属性 新增的方法:设置座高
构造方法也可以利用super关键字,改为:
this与super的总结 this用来引用当前对象,super用来引用当前父类对象。用this和super区别分别出现在父类、子类中的同名成员变量和类中方法使用的同名局部变量。 子类隐藏了父类的成员变量 子类的方法遮蔽了子类的成员变量
this的使用 可以出现在实例方法和构造方法中,但不可以出现在类方法中。因为类方法可以直接通过类名直接调用,这时可能还没有任何对象诞生。 代表创建的对象exa,这时number还未赋值; number赋值完毕;
this可以出现在类的实例方法中,代表使用该方法的当前对象。类的实例方法可以通过this调用该类的其他方法,或者成员变量。 在某个对象调用f方法的过程中又调用了方法g。由于这种逻辑关系非常明确,所以当一个方法调用另一个方法时可以省略this,即本条语句等价于: g();
super的使用 用来访问父类被隐藏的成员变量,或调用父类被覆盖的方法和构造函数。
例程:成员变量的隐藏和方法的覆盖 父类的成员变量 父类的构造函数 父类的方法
子类的成员变量隐藏了父类的成员变量 子类的方法覆盖了父类的方法 子类的方法中调用了父类的方法
分析: 根据构造方法链的原理,子类在实例化时,首先调用父类构造函数, 实例化父类。之后才是子类自身实例化。 将程序片段略作修改: 这就是为什么上例中会首先打印“in SuperClass:x=3”的原因。 将程序片段略作修改:
那么,父类中为什么会不存在无参数的构造方法呢? 当运行主程序的“SubClass sub=new SubClass();”语句时,上述程 序会报错,原因在于子类实例化之前要实例化父类,调用父类中的无参 数构造方法,而父类中不存在无参数构造方法。 那么,父类中为什么会不存在无参数的构造方法呢? 当构造一个类的对象时,编译器首先判断类是否有构造方法,如果没有,则 自动加上一个默认构造方法;而一旦发现有构造方法后,则不再自动加上构造 方法。 此时再用无参数的构造方法去创建对象,就会提示出错,因为已经没有系统 提供的默认构造方法了。 如果一个类要生成其他子类,最好提供一个无参数的构造方法以避免编程错误。
在上述程序中,除了在父类中另外再定义一个无参数构造方法外,还 可以显式的调用父类的构造函数,使用super语句。但是这一语句必须是 子类构造函数的第一句。 改写如下:
总结 继承的最大好处就是类的可重用性。 当创建自己的类时,应尽可能将它作为某一个类的子类,在自己的子类中加入一些特殊的内容,而不必重新定义这个类所需的全部属性和行为。 初始化过程总是由高级到低级层次;资源回收过程应从低级到高级层次进行。
5.3 多态机制 多态 方法重载 又被称为“一个方法名字,多个行为结果”。 通过方法覆盖(Override)和方法重载(Overload)来实现多态。 方法重载 一个类中可以有多个具有相同名字的方法,由传递给它们的不同个数和类型的参数来决定使用哪种方法。 通过重载可以定义同类的操作方法(行为),而不需要取很多个类似又容易混淆的名字。 方法的重载是功能多态性的体现。 功能多态性是指可以向功能传递不同的消息,以便让对象根据相应的消息来产生一定的行为。
重载方法需要满足以下条件: 方法名相同; 方法的参数签名(即参数类型、个数、顺序)不相同; 方法的返回类型可以不相同,方法的修饰符也可以不相同; 不能根据方法返回类型的不同来区分重载的方法。
例程:希望根据接收到的不同数据作出不同的反应 根据不同的参数类型、个数、顺序进行判断,决定到底执行哪个方法。
方法覆盖 如果子类中实例方法的参数签名和返回类型与父类中的实例方法都一样,就称为子类中的方法覆盖了父类的方法。 通过方法覆盖,子类可以重新实现父类的某些实例方法,使其具有自己的特征。 覆盖方法也可以返回被覆盖方法的返回类型的子类型,称为协变返回类型(convariant return type)。 在方法覆盖中,子类需要保持与父类完全一致的方法头声明,否则就不是方法覆盖。有以下情况: 参数个数、类型、顺序与父类完全相同,而返回类型类型不同:
area() in B cannot override area() in A; attempting to use incompatible return type
参数个数或参数类型与父类不尽相同,这时子类出现两个方法具有相同的名字,但保证了参数的不同,即子类出现了重载的方法: 合法覆盖父类的方法后,可以合法的定义另外的重载方法。
子类方法缩小了父类方法的访问权限: area() in B cannot override area() in A; attempting to assign weaker access privileges was public
方法的隐藏 终止覆盖 如果子类定义的类方法的签名和父类中方法的签名相同,那么子类中的方法就隐藏了超类中的方法。 父类的静态方法不可以被子类隐藏为非静态,父类中的非静态方法不可以被子类覆盖为静态。 终止覆盖 为了防止利用属于系统重要信息的类来创建子类和进行方法覆盖,替换原有的类攻击系统,防止父类被覆盖,引入了仲只覆盖的理念,即final修饰符外延性的使用。 用final修饰类来避免产生子类,用final修饰符修饰父类中的方法就可以避免子类来覆盖此方法。 父类的私有方法是隐性final的,不能被子类覆盖。
Java中的每个类都源于java.lang.Object类。如果一个类在定义时没有指定继承,它的父类默认是Object。 5.4 toString方法 Java中的每个类都源于java.lang.Object类。如果一个类在定义时没有指定继承,它的父类默认是Object。 toString()方法 调用对象的toString()方法返回一个代表该对象的字符串。 默认情况下,返回一个由该对象所属的类名、@和该对象十六进制的散列码组成的字符串。
通常应该覆盖toString()方法,使它返回一个代表该对象的易懂的字符串。 也可以传递对象来调用: System.out.println(object) System.out.print(object) 这等价于调用: System.out.println(object.toString()) System.out.print(object.toString())
5.5 抽象类 引言 说明 在继承的层次结构中,随着各新子类的出现,类变得越来越专门和具体。 类的设计应该保证父类包含子类的共同特征,有时,将一个父类设计的非常抽象,以至于它没有具体的实例,这样的类称为抽象类(abstract class)。 说明 抽象类和常规类一样具有数据和方法,但是不能用new操作符来创建它的实例; 抽象方法只有方法头而没有实现,它的实现由子类提供。 子类可以是抽象的,即使它的父类是具体的。
非抽象类不能包含抽象方法,如果一个抽象父类的子类不能实现所有的抽象方法,它必须声明为抽象的。 抽象类不能用new操作符实例化,但仍然可以定义它的构造方法,可在子类的构造方法中调用。 包含抽象方法的类必须是抽象的。但是允许声明没有抽象方法的抽象类。
例 有以下形状类:
当涉及计算面积及周长时,其方法的实现取决于几何对象的具体类型。 当然,可也以将所有的形状归为一类,以方法的重载来计算形状的面积及周长,但这样使得类的特征不够明显。 可将getArea和getPerimeter方法定义为抽象方法,这些方法将在子类中实现。
子类继承抽象父类,增加自己特有的属性,并实现抽象父类的抽象方法。
具体实例的创建 创建了一个新的圆形与新的矩形,并把它们赋值给变量s1和s2,这两个变量的类型是Shape。 当使用s1.getArea()时,由于s1是一个圆形,因此会自动调用Circle类中实现的getArea方法。 调用哪个方法由JVM在运行时根据对象的类型动态的决定。
类型转换(cast) 分析 类型转换允许在一种类型的位置上使用另一种类型的对象,但是只能在继承和实现允许转换的对象之间进行。 从以上语句可看出myBike属于MountainBike类型。 MountainBike是从Bicycle和Object派生而来,因此MountainBike是Bicycle,也是Object,可以在任何能够调用Bicycle和Object对象的位置使用它。 相反的情况并不一定成立:Bicycle可能是MountainBike,但并不一定是,Object可能是Bicycle或者MountainBike,但并不一定是。 类型转换允许在一种类型的位置上使用另一种类型的对象,但是只能在继承和实现允许转换的对象之间进行。 类型转换有两种:
隐式类型转换(implicit cast),如: 语句中的obj既是Object也是MountainBike。 显式类型转换(explicit cast) 以上语句会出现编译时错误,因为编译器不知道obj是MountainBike类, 此时需要通过显式类型转换,将MountainBike赋值给obj: 这种类型转换插入一个运行时的类型检查,如果obj不是MountainBike, 就会抛出异常。
5.6 接口 接口的特点 对象通过暴露的方法定义它们和外部世界的交互。方法形成对象和外部世界的接口(interface)。 接口通常是一组具有空方法体的相关方法。 通过实现接口,类可以更加正式的说明它承诺提供的行为。接口构成了类和外部世界之间的契约,并且该契约在编译时由编译器强制实施。 如果类声明实现一个接口,那么这个接口定义的所有方法都必须出现在其源代码中,否则就不能成功编译。 接口不是类层次结构的一部分,但它们与类结合在一起工作。Java通过接口使得处于不同层次、甚至互不相关的类可以具有相同的行为(即实现相同的接口)。
接口的定义 格式: 说明 Java中接口是类似于类的一种引用类型,只能包含常量、方法体和嵌套类型,其中没有方法体; 接口不能被实例化,只能被类实现,或者被其他接口扩展。 说明 关键字 interface表明其后紧跟着的是接口名; extends表明继承自那些父接口;
接口的特点 访问权限控制符 接口体 接口可以继承自多个父接口; public表明任意类和接口均可使用这个接口; 缺省修饰符表明只有与该接口定义在同一个包中的类和接口才可以使用这 个接口。 接口体 常量定义:在接口中定义的常量被缺省默认为public、final、static,而不 需特意写明以上修饰符,也不能被其他修饰符修饰。 方法体:接口中只进行方法的声明,而不负责方法的实现,因此没有方法 体。定义在接口中的方法被默认为public和abstract,同样不需特意修饰,也 不能被其他修饰符修饰。 接口的特点 接口可以继承自多个父接口; 如果在子接口中定义了和父接口同名的常量,则父接口中的常量被隐藏; 如果在子接口中定义了和父接口同名的方法,则父接口中的方法被重载;
接口的实现 实现格式 说明 接口允许没有父接口; 类修饰符只能是public或缺省;
例程 定义接口Shape2D,指明类需要实现的方法:计算二维图形的面积和周长。
抽象类和接口都可以用于模拟共同特征,一般来说strong is-a relationship应该用类来继承模拟;weak is-a relationship是类属关系,指对象拥有某种属性,可用接口来模拟。例如,职员和人的关系应该用类继承模拟;字符串都可以用来比较,则可以使String类实现Caparable接口。 可以使用接口避开单重继承的限制。
包(package)是组织一组相关类和接口的名称空间。 5.7 包 包(package)是组织一组相关类和接口的名称空间。 包的分类: API Java平台提供的数量庞大的类库(包的集合),可以用于应用程序的开发 ,它的包代表和通用编程相关的最常见的任务。 API使得程序员可以把精力集中于特定的应用程序设计,而不必在基础 设施上浪费时间。 自定义包 Java编程语言编写的软件可能由数百或甚至数千个独立的类构成,把相 关的类和接口存放在包中以组织这些内容。 包的作用 如同文件夹的概念一样,不同的包中的类可以同名,这样可以避免命名的冲突; 使得功能相关的类易于查找和使用,同一包中的类和接口通常是功能相关的;
定义 包是一组相关类和接口的集合,即类和接口的容器。 它提供了访问权限(控制类之间的访问)和命名的管理机制(划分类名空间)。 可提供一种访问权限的控制机制,一些访问权限以包为访问范围。 定义 包是一组相关类和接口的集合,即类和接口的容器。 它提供了访问权限(控制类之间的访问)和命名的管理机制(划分类名空间)。 包层次的根目录是由环境变量CLASSPATH来确定的。 定义格式 说明: 在定义类和接口的源文件的第一行使用package语句,就指明了该文件 中定义的类和接口属于第一条语句定义的包; 包可以带路径,形成与Java开发系统文件结构相同的层次关系。 包嵌套的层次可以用来保证包名的唯一性。 package 包名1[.包名2[.包名3…]];
包的引用 将类和接口组织成包的目的是为了能够更有效的使用包中的已经具备一定功能的类。 不同程序文件内的类也可同属于一个包,只要在这些程序文件前加上 同一个包的声明; 如果缺省package语句,则指定为无名包。 包的引用 将类和接口组织成包的目的是为了能够更有效的使用包中的已经具备一定功能的类。 依次把Father类、Mother类、Son类装入包Family
import 包名1[.包名2[.包名3…]].类名|*; 格式 装载使用已编译好的包的方式: 在要引用的类名前带上包名: 在文件开头使用import引入包中的类: 在文件前使用import引入包中所有的类: import 包名1[.包名2[.包名3…]].类名|*;
说明 使用*时表示要从包中引入所有的类,也只能是该包中的类; Java编译器为所有程序自动引入包java.lang,所以不必显式引入; 需要注意的是:包实际上并不如同最初看上去那样具有层次结构,某 个包名字中的“.”并不表示它们之间的包含关系,只是为了使它们之间的 联系更明显。 以上语句只能导入java.awt包中的所有类,而不能导入java.awt.font、 java.awt.color或者任何java.awt.XXXX包,如果计划使用以上的包,则必 须另外导入: