面向对象编程(OOP)
3-3节学习了ADT理论;本节学习ADT的具体实现技术:OOP。
目录:
- 面向对象的标准
- OOP 的基本概念
- OOP的独特功能
- Java中一些重要的Object方法
- 设计好的class
- OOP的历史
- 总结
面向对象的标准
面向对象的编程方法/语言应该以类的概念作为中心概念。同时还有如下标准:
- 静态类型(Static typing):一个定义良好的类型系统应该通过强制执行一些类型声明和兼容性规则来保证它接受的系统的运行时类型安全。
- 泛型(Genericity):为“ready for change”和“design for/with reuse”:应该可以编写具有代表任意类型的正式泛型参数的类。
- 继承(Inheritance):应该可以将一个类定义为从另一个类继承,以控制潜在的复杂性。
- 多态(Polymorphism):在基于继承的类型系统的控制下,应该可以将实体(表示运行时对象的软件文本中的名称)附加到各种可能类型的运行时对象。
- 动态绑定/分配(Dynamic dispatch/binding):在一个实体上调用一个特性应该总是触发与所连接的运行时对象的类型相对应的特性,这在调用的不同执行过程中不一定是相同的。
OOP 的基本概念
对象(Object)
真实世界的对象有两个特征:它们都有状态和行为。识别真实世界对象的状态和行为是从OOP开始思考的好方法。
例如,狗有状态(名字,颜色,品种,饥饿)和行为(吠叫,取指,摇尾巴)。
由此引出对象的概念:一个对象是一堆状态和行为的集合。状态是包含在对象中的数据,在Java中,它们是对象的fields。行为是对象支持的操作,在Java中,它们称为methods。
类(Class)
每个对象都有一个类,这个类定义了methods和fields。methods和fields都是这个类的成员。类定义了类型(type)和实现(implementation),类型即这个类在哪里可以使用,实现是这个类如何做事情。简单地说,类的方法是它的应用程序编程接口(API)。
例子:复数的类
一个与类相关的变量,而不是类的一个实例,称为类成员变量,相似地,若一个method与类相关,称为类方法。
不是类方法或类变量的方法和变量称为实例方法和实例成员变量。
类变量和类方法与类相关联,并且每个类都会出现一次。 使用它们不需要创建对象。
实例方法和变量会在每个类的实例中出现一次。
下图说明了实例方法和类方法的区别:
接口(Interface)
Java的接口是一种用于设计和表达ADT的有用语言机制,其实现方式是实现该接口的类。
Interface和Class: 定义和实现ADT
接口之间可以继承,一个类可以实现多个接口。一个接口也可以有多种实现。
来看下面一个简单的例子
/** MyString represents an immutable sequence of characters. */
public interface MyString {
// We'll skip this creator operation for now
// /** @param b a boolean value
// * @return string representation of b, either "true" or "false" */
// public static MyString valueOf(boolean b) { ... }
/** @return number of characters in this string */
public int length();
/** @param i character position (requires 0 <= i < string length)
* @return character at position i */
public char charAt(int i);
/** Get the substring between start (inclusive) and end (exclusive).
* @param start starting index
* @param end ending index. Requires 0 <= start <= end <= string length.
* @return string consisting of charAt(start)...charAt(end-1) */
public MyString substring(int start, int end);
}
第一个实现:
public class SimpleMyString implements MyString {
private char[] a;
/** Create a string representation of b, either "true" or "false".
* @param b a boolean value */
public SimpleMyString(boolean b) {
a = b ? new char[] { 't', 'r', 'u', 'e' }
: new char[] { 'f', 'a', 'l', 's', 'e' };
}
// private constructor, used internally by producer operations
private SimpleMyString(char[] a) {
this.a = a;
}
@Override public int length() { return a.length; }
@Override public char charAt(int i) { return a[i]; }
@Override public MyString substring(int start, int end) {
char[] subArray = new char[end - start];
System.arraycopy(this.a, start, subArray, 0, end - start);
return new SimpleMyString(subArray);
}
}
这是第二个实现:
public class FastMyString implements MyString {
private char[] a;
private int start;
private int end;
/** Create a string representation of b, either "true" or "false".
* @param b a boolean value */
public FastMyString(boolean b) {
a = b ? new char[] { 't', 'r', 'u', 'e' }
: new char[] { 'f', 'a', 'l', 's', 'e' };
start = 0;
end = a.length;
}
// private constructor, used internally by producer operations.
private FastMyString(char[] a, int start, int end) {
this.a = a;
this.start = start;
this.end = end;
}
@Override public int length() { return end - start; }
@Override public char charAt(int i) { return a[start + i]; }
@Override public MyString substring(int start, int end) {
return new FastMyString(this.a, this.start + start, this.end + end);
}
}
这就是我们所说的一个接口可以有多个实现。
客户如何使用此ADT?这是一个例子:
MyString s = new FastMyString(true);
System.out.println("The first character is: " + s.charAt(0));
但其中有问题,这么实现接口打破了抽象边界,接口定义中没有包含constructor,也无法保证所有实现类中都包含了同样名字的constructor。 故而,客户端需要知道该接口的某个具体实现类的名字。因为Java中的接口不能包含构造函数,所以它们必须直接调用其中一个具体类的构造函数。该构造函数的规范不会出现在接口的任何地方,所以没有任何静态的保证,即不同的实现甚至会提供相同的构造函数。
幸运的是,(如Java 8中)的接口被允许包含静态方法,所以我们可以实现的创造者操作valueOf
为静态工厂方法的接口MyString
:
public interface MyString {
/** @param b a boolean value
* @return string representation of b, either "true" or "false" */
public static MyString valueOf(boolean b) {
return new FastMyString(true);
}
// ...
现在客户可以使用ADT而不会破坏抽象边界:
MyString s = MyString.valueOf(true);
System.out.println("The first character is: " + s.charAt(0));
Summary of Interface
Safe from bugs
ADT是由其操作定义的,接口就是这样做的。
当客户端使用接口类型时,静态检查确保他们只使用由接口定义的方法。
如果实现类公开其他方法,或者更糟糕的是,具有可见的表示,客户端不会意外地看到或依赖它们。
当我们有一个数据类型的多个实现时,接口提供方法签名的静态检查。Easy to understand
客户和维护人员确切知道在哪里查找ADT的规约。
由于接口不包含实例字段或实例方法的实现,因此更容易将实现的细节保留在规范之外。Ready for change
通过添加实现接口的类,我们可以轻松地添加新类型的实现。
如果我们避免使用静态工厂方法的构造函数,客户端将只能看到该接口。
这意味着我们可以切换客户端正在使用的实现类,而无需更改其代码。
封装和信息隐藏
将精心设计的模块与不好的模块区分开来的唯一最重要的因素是其隐藏内部数据和其他模块的其他实施细节的程度。
设计良好的代码隐藏了所有的实现细节
- 干净地将API与实施分开
- 模块只能通过API进行通信
- 对彼此的内在运作不了解
这就被称为信息隐藏或封装,是软件设计的基本原则。
信息隐藏的好处:
- 将构成系统的类分开
- 加快系统开发速度
- 减轻了维护的负担
- 启用有效的性能调整
- 增加软件复用
信息隐藏接口:
- 使用接口类型声明变量
- 客户端仅使用接口中定义的方法
- 客户端代码无法直接访问属性
成员的可见性修饰符:
private
:只能从声明类内访问protected
:可以从声明类的子类(以及包内)public
:从任何地方访问
继承和重写
重写
可重写方法:允许重新实现的方法。
严格继承:子类只能添加新方法,无法重写超类中的方法。
Example: 重写方法
class Device {
int serialnr;
public final void help() {….}
public void setSerialNr(int n) {
serialnr = n;
}
}
class Valve extends Device {
Position s; public void on() {
…
}
public void setSerialNr(int n) {
serialnr = n + s.serialnr;
}
}
重写的函数:完全同样的signature,实际执行时调用哪个方法,运行时决定。
如果父类的对象用于调用该方法,则会执行父类中的版本;如果使用子类的对象来调用该方法,那么会执行子类中的版本。
当子类包含一个覆盖超类方法的方法时,它也可以使用关键字super
调用超类方法。例子如下
class Thought {
public void message() {
System.out.println(“Thought.");
}
}
public class Advice extends Thought {
@Override //@Override annotation in Java 5 is optional but helpful.
public void message() {
System.out.println(“Advice.");
super.message(); // Invoke parent's version of method.
}
}
Thought parking = new Thought(); parking.message(); // Prints “Thought."
Thought dates = new Advice(); dates.message(); // Prints “Advice."
重写的时候,不要改变原方法的本意。
抽象类
抽象方法:具有signature但没有实现的方法(也称为抽象操作),由关键字abstract
定义。
抽象类:包含至少一个抽象方法的类称为抽象类。
接口:一个只有抽象方法的抽象类。
Concrete class -> Abstract Class -> Interface
多态性,子类型和重载
三种类型的多态
- Ad hoc polymorphism (特殊多态):功能重载
- Parametric polymorphism (参数化多态): 泛型或泛型编程。
- Subtyping (also called subtype polymorphism or inclusion polymorphism 子类型多态、包含多态):当一个名称表示许多不同的类与一些常见的超类相关的实例。
特殊多态:
public class OverloadExample {
public static void main(String args[]) {
System.out.println(add("C","D"));
System.out.println(add("C","D","E"));
System.out.println(add(2,3));
}
public static String add(String c, String d) {
return c.concat(d);
}
public static String add(String c, String d, String e){
return c.concat(d).concat(e);
}
public static int add(int a, int b) {
return a+b;
}
}
重载(Overload):多个方法具有同样的名字,但有不同的参数列表或返回值类型 。
价值:方便client调用,client可用不同的参数列表,调用同样的函数。
例如:doTask()
和 doTask(Object O)
就是重载函数。
重载是静态多态,根据参数列表进行 最佳匹配。在编译阶段时决定要具体执行哪个方法 (static type checking) ,与之相反,overridden methods则是在run-time进行dynamic checking!
以下是合法重载函数的例子:
public void changeSize(int size, String name, float pattern) { }
public void changeSize(int size, String name) { }
public int changeSize(int size, float pattern) { }
public void changeSize(float pattern, String name){ }
public void changeSize(String pattern, float size){ }//the same signature
public boolean changeSize(String name, float pattern) { } //No changes on parameters
重载的方法调用版本是基于对象类型在运行时决定的,但是重载的方法调用版本是基于在编译时传递的参数的引用类型。看下面的小栗子。
重写(Override)和重载(Overload)
参数多态性和泛型编程:
public class Pair<E> {
private final E first, second;
public Pair(E first, E second) {
this.first = first;
this.second = second;
}
public E first() {
return first;
}
public E second() {
return second;
}
}
Client:
Pair<String> p = new Pair<>("Hello", "world");
String result = p.first();
泛型接口:
- 泛型接口,非泛型的实现类
public interface Set<E> {
// ...
/**
* Test for membership.
* @param e an element
* @return true iff this set contains e
*/
public boolean contains(E e);
/**
* Modifies this set by adding e to the set.
* @param e element to add
*/
public void add(E e);
// ...
}
public class CharSet1 implements Set<Character> {
private String s = "";
// ...
@Override
public boolean contains(Character e) {
checkRep();
return s.indexOf(e) != -1;
}
@Override
public void add(Character e) {
if (!contains(e)) s += e;
checkRep();
}
// ...
}
- 泛型接口,泛型 的实现类
public interface Set<E> {
// ...
public class HashSet<E> implements Set<E> {
// ...
一些细节:
- 可以有多个类型参数:例如
Map<E, F>, Map<String, Integer>
- 通配符,只在使用泛型的时候出现,不能在定义中出现,例:
List<?> list = new ArrayList<String>();
- 泛型类型信息被删除
Cannot use instanceof() to check generic type 运行时泛型消失了! - 无法创建通用数组
Pair<String>[] foo = new Pair<String>[42]; // won't compile
子类型多态:
子类型的规约不能弱化超类型的规约。
子类型多态:不同类型的对象可以统一的处理而无需区分,从而隔离了“变化”。
动态绑定
绑定:将调用的名字与实际的方法名字联系起来(可能很多个);
分派:具体执行哪个方法(early binding -> static dispatch)
静态绑定:编译阶段即可确定要执行哪个具体操作。
动态绑定,也称为推迟绑定,编译阶段可能绑定到多态操作,运行阶段决定具体执行哪 个(override和overload均是如此);
推迟绑定:编译阶段不知道类型,一定是动态分派( override是推迟绑定,overload是early binding)。
设计好的类
- 简单
- 本质上是线程安全的
- 可以自由分享
- 不需要防御式拷贝
- 优秀的building blocks
如何编写一个不可变的类
- 不要提供任何mutators
- 确保没有方法可能被覆盖
- 使所有的fields有final修饰
- 使所有的fields有private修饰
- 确保任何可变组件的安全性(避免表示泄露)
- 实现
toString()
,hashCode()
,clone()
,equals()
等。