面向对象编程(Object-Oriented Programming)——将ADT的接口和其实现分离开来,在Java之中通过Java的接口来实现。并且通过接口来定义ADT,然后在用具体的类去实现ADT。
一. 面向对象的标准
一个面向对象编程的语言应当有着一个有关类的中心理论。这样的编程语言应当使其类和特征具有断言(assertion)(规约,前提,后置和不变量)和异常处理,并且能依靠工具来针对这些断言产生文档,还能在运行时可选地进行监视。
· 这些帮助开发一个可靠的软件
· 这些提供了一个系统的文档
· 这是对于面向对象的软件进行测试和debug的主要工具
静态类型(Static typing):一个定义良好的类型系统应该通过执行一些类型声明和兼容性规则,确保它所接受的系统的运行时类型安全。
泛型(Genericity):“适应变化”并且是“复用性设计”
继承(Inheritance):应该可以定义类之间的继承关系,以此来控制由此产生的潜在的复杂性
多态(Polymorphism):在基于继承的类型系统的控制下,应该可以将实体(表示软件文本中的运行时对象的名称)与各种可能类型的运行时对象相关联。
动态分派/绑定(Dynamic dispatch/binding):在实体上调用一个特性总是应该引起与相关联的运行时对象类型相对应的特性,这在不同的调用执行过程中不一定相同。
(这英语看的我头疼)
二. 基本的理论:对象(object)、类(class)、属性(attribute)、方法(method)
对象:现实世界中的任何事物都有两种特征——状态(state)和行为(behavior)。如何区分现实世界的状态和行为会帮助我们更好的理解OOP
· 狗有很多种状态(名字、颜色、品种、饥饿程度)和行为(吠、摇尾巴等)
因此我们在运用OOP时需要知道有哪些状态和行为。
一个对象就是不同的状态和行为的集合体(Bundle),在编程之中。
· 状态——对象内部的数据,在java之中表现为域(fields)
· 行为——对象所支持的操作,在java之中称之为方法(method)
类:
每一个对象都有一个类
· 类定义了方法和域
· 方法和域统称为成员
类不仅仅定义了类型(type),也定义了实现方式(implementation)
· 类型 ≈ 对象的使用地点
· 实现方式 ≈ 对象能做些什么
宽泛地讲,一个类的方法即它的应用程序编程接口(API,Application Programming Interface)
类成员变量(Class variable):是一个与类相关联的变量而不是与类的实例相关联。我们也可以将类中的方法与类相关联——称之为类方法(Class method)。(Static)
· 为了引用类成员变量和类方法,我们可以通过类的名字和所引用的东西之间通过'.'来调用
e.g. String.valueOf()
那些不是类的成员变量和方法的称之为实例方法(instance method)和实例变量(instance variable)。
· 为了引用实例方法和变量,必须要通过类的实例来调用
总结:类成员变量和类方法是与类相关联的,并且只在类之中出现一次,并且不需要生成相应对象;而实例变量和方法在每个类的实例之中都会出现一次。
三. 接口(Interface)
Java的接口是一种设计和实现ADT的实用的语言表达机制,它的具体实现是通过一个类来实现。
· Interface和Class:定义和实现ADt
· 接口之间可以继承
· 一个类可以实现多个接口;同样的,一个接口可以有多个实现
e.g.
接口与类:
接口:确定ADT的规约 类:实现ADT
注意:类确实可以定义ADT,但是更加倾向于接口
· 公共的成员变量可能会导致一些错误
· 接口之中的变量和参数的使用是建立在一个实现能满足的情况下
· 接口会支持实现方式的改变
· 接口能使实现的细节具有独立性
接口的优点:
接口确定和用户之间的规约
· 接口是所有用户程序员都需要理解的一个ADT
· 用户并不能直接根据ADT的表示来创建一个依赖关系,因为实例变量并不能放在接口之中
· 在完全不同的类之中,实现方式能被很好地区分和保持良好
多种ADT的实现方式能在一个项目之中同时存在,因为由不同的类去实现同一个接口
· 多种实现方式是因为ADT有不同的表现和行为 e.g. HashSet, TreeSet
四. 封装(Encapsulation)和信息隐藏(Information hiding)
一个区分设计好坏的模块的重要的标准就是它隐藏了多少内部的信息和其它模块实现的具体的信息
一个有着良好设计的代码应当隐藏其所有实现的具体细节:
· 将API与实现分离
· 模块之间的交互仅通过API实现
· 彼此的内部工作是相互独立的
信息隐藏的好处:
· 将组成系统的类进行解耦
- 允许每个类能被单独地进行开发、测试、优化、使用、理解和修改
· 加速系统的开发
- 可以同时开发多个类(并行开发)
· 能使性能提高
- 可以单独优化某个类
· 增加软件复用性
- 一个耦合度低的类通常可以在多种场合下被使用
通过接口来实现信息隐藏
· 使用接口来声明变量
· 客户端仅使用接口中定义的方法
· 客户端代码无法直接访问属性
五. 继承(Inheritance)和重写(Overriding)
(1)重写
可重写的方法(Rewriteable Method):一个能被重新实现的方法(java之中并没有特定关键字)
严格继承(Strict Inheritance):子类只能添加新方法,无法重写超类(superclass)之中的方法。在Java之中,一个方法如果不能被重写,那么它要有final来修饰。
final关键字:
final 域:防止一个域在初始化之后再被重新赋值
final 方法:防止重写
final 类:防止被继承
方法的重写:是一种语言的特征,它允许子类能针对在父类之中出现过的方法用另外一种特定方式去重新实现。
· 重写的函数:完全一样函数名、返回值、参数
· 实际执行时调用哪个方法,在运行时决定。如果是父类的对象调用了方法,那么就使用父类之中的那个版本;如果是子类的对象调用了那个方法,就使用子类之中的版本。
当一个子类重写了父类之中的方法时,它同样可以通过super来调用父类之中的方法
e.g.
(2)抽象类(Abstract Class)
抽象方法(Abstract Method):一个方法只有一个声明,但是没有具体的实现方式;由关键字abstract定义
抽象类(Abstract Class):一个至少含有一个抽象方法的类
接口:一个只含有抽象方法的抽象类
具体类 -> 抽象类 -> 接口
六. 多态(Polymorphism),子类(subtyping),重载(overloading)
(1)三种类型的多态
特殊多态(Ad hoc Polymorphism):一个函数有着异构的,在小范围将特定类型混合的实现方式。特殊多态通常通过功能重载(function overloading)来实现
参数化多态(Parametric Polymorphism):当代码并不涉及到任何类型,因此可以被使用为任何类型。在OOP的实现之中,通常表示为泛型(generic)。
子类型多态(Subtyping,也被称为包含多态(inclusion Polymorphism)):许多不同的类有着一个共同的父类。
(2)特殊多态
即一个函数有多种表示方式
e.g.
(3)重载
重载方法让程序员在同一个类之中复用同样的一个方法, 但是他们的参数会有所不同(有时候返回值也会不同)。重载会让客户端的调用更加方便,因为客户单可以使用不同的参数列表来调用同样的函数
功能性重载(Function Overloading):同一个函数名有着多种不同的实现方式。
静态多态(static polymorphism):重载是一种静态多态。
· 调用函数会根据参数列表进行最佳匹配
· 会执行静态类型检查
· 在编译阶段时就会决定要执行哪个方法
注意:重写(Override)是在运行时才进行动态检查(Dynamic checking)。
重载的规则:
· 不同的参数列表
· 相同/不同的返回值
· 相同/不同的访问修饰符(private, protected, public)
· 相同/不同的异常
· 可以在同一个类之内进行重载,也可以在子类之中进行重载。
e.g.
重写所使用的方法的版本是在运行时依据对象类型确定的;但是重载所使用的方法的版本是根据在编译时所传递的参数的引用类型所确定的
重写 vs 重载
(3)参数多态和泛型编程
参数多态是指函数在一系列类型上表现是一致的,而这些类型又表现为相同的结构
泛型编程是一种编程风格,数据类型和方法都是由一种“之后定义的类型(type to-be-specified-later)”来编写,只有当实例化的时候才会将真正需要到的类型传入。泛型编程的中心是围绕着一个“从具体之中抽象”的思想,将泛型与高效的算法相结合,从而产生出各种各样不同类型的软件。
Java之中的泛型:
· 类型变量是一种无条件的标识符,它们由泛型类、泛型接口、泛型方法、泛型构造器来声明
类型变量是通过<>来声明的
e.g.
· 泛型类:其定义中包含了类型变量(泛型接口、泛型方法同理)
泛型接口
- 非泛型的实现类
e.g.
- 泛型的实现类
e.g.
Java泛型的一些细节:
· 可以有多个类型参数 e.g.
· 通配符(Wildcards),只有在使用泛型的时候才出现,不能在定义中出现
e.g.
· 泛型类型的信息会被擦去(仅限在编译时),因此不能使用instanceOf()来检查泛型
· 不能创建泛型数组
e.g.
(4)子类型多态
类型是一系列值的集合:
· Java之中的List类型就是这样定义的
· 如果我们观察List所有可能的值,我们会发现没有一个值是List的对象,因为List是一个接口,我们不能创建一个接口的实例;但是每一个值都是List众多实现方式之一的对象。
因此我们可以定义子类型——子类型是父类型的一个简单子集
e.g. ArrayList和LinkedList是List的子类型
Java之中Collection的API
子类型的好处:代码的复用,模块化的灵活性
子类型的另一种解释:
“B是A的子类型”意味着“每一个B都是A”;如果用规约来描述,就是“每一个B都满足A的规约”
· 如果B的规约至少是和A的规约一样强的话,那么B就是A的子类型
· 当我们用一个类去实现一个接口时,Java的编译器会自动强调一个要求:每个接口中的方法都在B中出现了,并且具有兼容性的函数头。
· 子类型的规约不能弱化父类型的规约
子类型多态:不同类型的对象可以统一处理而无需区分,从而隔离了“变化”。
类型转换:有时候我们会使用强制类型转换,但是对于子类型之间的转换要注意避免抛出异常。
在父类型与子类型之间的向下转型(downcast)是不被允许的
七. 动态分配(Dynamic dispatch)
动态分配是一个对于一个多态在运行时来选择何种操作去执行的一个过程;静态分配是在编译阶段就确定要具体执行哪个操作。
方法的动态分派过程:
1.(编译阶段)决定将要进入哪个类
2.(编译阶段)决定将要执行哪个函数名
- 找到所有匹配、可调用的方法
3.(运行阶段)确定接收器的动态类别
4.(运行阶段)从动态类之中定位到要调用的方法
- 在第2步所找到的函数之中寻找对应方法
- 没找到就去父类之中寻找
动态分配不等同于推迟绑定(late binding/ Dynamic binding)
- 绑定:将调用的名字与实际的方法的名字联系起来(可能有很多个);分派:具体执行哪个方法(early binding -> static dispatch)
- 动态分派:编译阶段可能绑定到多态操作,运行阶段决定执行哪个(重写和重载也是如此)
- 推迟绑定:编译阶段不知道类型,一定是动态分派(重写是推迟绑定,重载是提早绑定)
八. Java的Object类之中的一些重要方法
equals() - 如果两个对象“相同”就为真
hashCode() - 哈希表所用的哈希码
toString() - 输出表示
toString()——一般要重写,因为原本的输出太垃圾了,除非不需要输出
e.g.
equals & hashCode ——如果是要确定两者的值在语义上是否相等,就要重写,反之不用
e.g.
资料来源 CMU15-214 哈工大软件构造课程