第七章 面向对象的编程(OOP)
ADT的具体实现技术OOP
OOP的基本概念
【对象】
-
对象是现实世界存在的,有两个特征——(静态的)状态、(动态的)行为。(例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。)
-
对象是类的一个实例,有状态和行为。
-
概念:一个对象是一堆状态和行为的集合。
- 状态是包含在对象中的数据,在Java中,它们是对象的fields。
- 行为是对象支持的操作,在Java中,它们称为methods。
【类】
-
类是一个模板,它描述一类对象的行为和状态。(现实世界上并不存在,是对象的抽象)
-
每个对象都有一个类
-
类定义了属性类型(type)和行为实现(implementation)
-
简单地说,类的方法是它的应用程序编程接口(API)。
-
由类生成对象的过程是实例化的过程(也可以说是创建对象的过程)
-
属于类的:类成员变量(class variable)又叫静态变量;类方法(class method)又叫静态方法(加上
static
)- 要调用类成员变量和类方法,通过
类名.类方法
和对象.类方法
。(如System.out.println()
、Math.sin()
)。类的使用不需要创建对象。
- 要调用类成员变量和类方法,通过
-
属于对象(实例)的:实例变量(instance variable)和实例方法(instance method)是不用
static
形容的实例和方法;- 实例方法通过
对象.实例方法
访问。
- 实例方法通过
-
总结:
- 类变量和类方法与类相关联,并且每个类都会出现一次。 使用它们不需要创建对象。
- 实例方法和变量会在每个类的实例中出现一次。
【接口】
-
Java 中的 interface(接口)是一种表示抽象数据类型的好方法。接口中是一连串的方法标识,但是没有方法体(定义)。接口通常以interface来声明。
-
如果想要写一个类来实现接口,我们必须给类加上 implements 关键字,并且在类内部提供接口中方法的定义。所以接口+实现类也是 Java 中定义抽象数据类型的一种方法。用于设计和表达ADT的语言机制
-
Interface和Class:定义和实现ADT。可以把接口看成特殊的类。
-
接口中只有方法的定义,没有实现;接口中不含属性
-
接口之间可以继承与扩展
-
一个类可以实现多个接口(从而具备了多个接口中的方法)(Java不支持多继承)
-
一个接口可以有多种实现类(如
List
有两种实现方法),但一个类只能有一个父类。 -
接口:确定ADT规约;类:实现ADT;也可以不需要接口直接使用类作为ADT,既有ADT定义也有ADT实现。(更倾向于使用前者,防止依赖于具体实现细节)
-
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
-
接口中不能有构造方法Constructor(由于构造方法的方法名必须和类名相同,但接口可以有不同实现类)
-
通过实现类创建接口对象时,需要声明通过的实现类名(如
ArrayList()
)。为了进一步进行封装,Java8以后支持接口包含静态方法(含方法体),相当于把构造方法封装在接口里,例如:pubilc interface Mystring{ ... public static Mystring valueOf(boolean b){ return new FastMystring(true); // } }
Mystring s = Mystring.valueof(true); ...
public interface Set<E>{ //接口 //方法签名 } public class ArraySet implements Set<E>{ //接口的实现类 //接口中的方法在实现类中必须有方法体 }
【在接口中使用default方法】
-
接口中的每个方法在所有(实现)类中都要实现,缺点是会导致部分方法的重复实现。
-
通过default方法,在接口中统一实现某些功能,无需在各个类中重复实现它。(它允许我们向接口添加新方法,这些方法在实现中自动可用。因此,不需要修改实现类)
-
default 方法的典型使用方式:以增量式的为接口增加额外的功能而不破坏已实现的类
public interface Example { default int method1(int a) {…} static int method2(int b) {…} public int method3(); } public class C implements Example { //若重写method1则会覆盖,否则调用method1 @Override public int method3() {…} //需要重写 public static void main(String[] args) { Example.method2(2); //直接通过类名调用 C c = new C(); c.method1(1); c.method3(); } }
【封装和信息隐藏】
-
区分模块设计好坏的一个最重要的因素是:它的内部数据和其他实现细节是否很好地隐藏起来了。
-
封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。
-
设计良好的代码隐藏了所有的实现细节
- 干净地将API与实施分开
- 模块只能通过API进行通信
- 对彼此的内在运作不了解
-
信息封装的好处
- 将构成系统的类分开,减少耦合。允许隔离地开发、测试、优化、使用、理解和修改它们。
- 加快系统开发速度——不同类并发开发
- 减轻了维护的负担——可以更快地理解和调试类,而不用担心损害其他模块。
- 启用有效的性能调整
- 增加软件复用
-
用接口进行信息隐藏
- 使用接口类型声明变量
- 客户端仅使用接口中定义的方法
- 客户端代码无法直接访问属性
-
客户端仍然可以访问其他的非接口成员
-
成员的可见性修饰符
private
只能通过声明类访问protected
可从声明类的子类访问(以及包内)public
可从任何地方访问default
(什么也不加)在包里进行访问(包可见性)
作用域 当前类 同一包内(子孙类) 子孙类(不同包) 其他包 private √ √ √ √ protected √ √ √ × default √ √ × × private √ × × ×
继承和重写
【继承】
-
继承概念:继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
class A extends B
-
继承是为了代码重用。
-
超类(父类)特性(public、protected)在子类中隐式可用
-
子类会默认继承父类所有的属性和方法(private的呢???)
-
UML图中。继承用直线加三角形(箭头)表示
-
严格继承:子类只能添加新方法,无法重写超类中的方法。
- 如果想实现严格继承,要在方法前加上
final
,这样这个方法就不能被重写了。(在Java中,没有特殊关键字的情况下,方法默认是可重写的) - 如果想让一个类不能被继承,那么就在类前加上
final
- 如果想实现严格继承,要在方法前加上
【final总结】
- 修饰字段(属性):对不可变类型,值不能被修改;对可变类型,引用不能被改变。
- 修饰方法:该方法不能被重写
- 修饰类:该类不能被继承(扩展)
【覆盖/重写(Overriding)】
-
重写概念:重写是子类对父类的允许访问的方法的实现过程进行重新编写,但两个方法要具有完全相同的签名(signature),即方法名、返回值、形参都不能改变。亦即外壳不变,核心重写!
-
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
-
实际执行时调用哪种方法,在运行时决定
-
父类中的某个方法实现体为空,意味着其所有子类型都需要这个功能, 但各有差异,没有共性,在每个子类中均需要重写。
父类型中的被重写方法体不为空:意味着对其大多数子类型来说,该方法是可以被直接复用的。
-
当子类包含覆盖超类方法的方法时,它还可以使用关键字
super
调用超类方法。(this
表示是当前类的)。(即:重写之后,利用super.方法名()
复用了父类型中函数的功能,并对其进行了扩展) -
在子类构造方法里调用父类的构造方法,
super()
必须写在子类构造方法的第一行(见PPT例题) -
建议:重写的时候不要改变原方法的本意
-
重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。
-
子类只能添加新方法,无法重写超类中的方法。
-
重写的tips:
- 使用
@Override
,让编译器检查重写的这个方法和被覆盖的方法签名完全一样。 - 可见性可以保持不变或增加,但不能减少(如原来是
protected
,重写以后就不能是默认default
)。(因为子类正常情况下是可以替换父类的)
- 使用
【抽象类】
- 除了普通类、接口以外,还有抽象类。普通类是最具体的(有方法的实现),接口是最抽象的(没有方法的实现)。
- 抽象方法:只有定义没有实现
- 抽象类:包含至少一个抽象方法的类称为抽象类。(即抽象类至少有一个没有具体实现的方法)
- 抽象类不能实例化(不能用new 生成对象)。(和接口一样都不能,只能通过一个继承它的子类来使用)
- 继承某个抽象类的子类在实例化时,所有父类中的抽象方法必须已经实现。(和接口一样)
- UML图中三者写法相似(都是三段)
- 在抽象类中:
abstract
关键字要写在类的声明前和未实现的方法声明前。 - 所有子类型完全相同的操作, 放在父类型中实现,子类型中无需重写。 有些子类型有而其他子类型无的操作,不要在父类型中定义 和实现,而应在特定子类型中 实现。如果某些操作是所有子类型都共有, 但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写
多态、子类型、重载
【多态】
- 多态是同一行为具有多种不同表现形式或形态的能力
- 三种类型的多态
- 特殊多态 (Ad hoc polymorphism):一个方法可以有多个同名的实现(方法重载)。
- 参数多态 (Parametric polymorphism):一个类型名字可以代表多个类型(泛型(编程))。
- 子类型多态、包含多态:一个变量名字可以代表多个类的实例(子类型)。
【重载】
- 重载(Overloading) :多个方法具有同样的名字,但有不同的参数列表,返回值类型相同与否都可以。
- 每个重载的方法(或构造函数)都必须有一个独一无二的参数类型列表。
- 价值:方便client调用,client可用不同的参数列表,调用同样的函数。
- 重载是静态多态,根据参数列表进行最佳匹配。在编译阶段时(进行静态类型检查)决定要具体执行哪个方法 (static type checking) 。与之相反,重写方法则是在运行阶段进行动态检查。
- 重载规则
- 被重载的方法必须有不同的参数列表(参数个数或类型不一样,参数名不一样不能作为判断重载是否合法的依据);
- 被重载的方法可以有相同/不同的返回类型;(无法以返回值类型作为重载函数的区分标准。)
- 被重载的方法可以有相同/不同的访问修饰符;(相同/不同的public/private/protected)
- 被重载的方法可以声明新的或更广的检查异常;
- 方法能够在同一个类中重载,也可以在一个子类中重载。
- 要调用的方法的哪个重写版本是在运行时根据对象类型决定的,但是要调用的方法的哪个重载版本是根据编译时传递的参数的引用类型决定的。(见PPT例题)
- 父类不能调用子类的方法
【重写与重载的区别】
区别点 | 重载方法 | 重写方法 |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改(无要求) | (早期)一定不能修改 (现在)子类中方法的返回值必须是父类方法返回值的子类型(协变) |
异常 | 可以修改 | 可以减少或删除,一定不能抛出新的或者更宽泛的异常 |
访问 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
调用情况 | 引用类型决定选择哪个重载版本(基于声明的参数类型)。在编译时发生。实际调用的方法仍然是在运行时发生的虚拟方法调用,但是编译器始终知道要调用的方法的签名。因此,在运行时,参数匹配将已经确定,只是不是方法所在的实际类(编译时看该类里有没有需要的方法) | 对象类型(换句话说,堆上实际实例的类型)决定选择哪种方法在运行时发生。 |
-
调用方法 / 参数看的是本身的类型而不是指向的对象,如
Animal animalRefToHorse = new Horse()
看的是Animal; -
对于重载与重写,编译时决定是否能够调用,运行时决定调用哪种方法。
-
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
-
方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
-
方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
-
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
-