3.4 面向对象的编程
一. 接口
Java
中的 interface
(接口)是一种表示抽象数据类型的好方法。接口中是一连串的方法标识,但是没有方法体(定义)。如果想要写一个类来实现接口,我们必须给类加上 implements
关键字,并且在类内部提供接口中方法的定义。所以接口+实现类也是 Java
中定义抽象数据类型的一种方法。
我们偏向于使用接口来定义变量。
Set<Criminal> senate = new HashSet<>();
接口中无构造函数。可以设计静态方法构造。静态方法可以存在于接口中。
使用接口的优点:
- 接口只为使用者提供“契约”。实例化的变量不能放在接口中(具体实现被分离在另外的类中)。
- 允许了一种抽象类型能够有多种实现/表示,即一个接口可以有多个实现类(而一个类也可以同时实现多个接口)。
然而, Java
的静态检查会发现没有实现接口的错误,但不会检查是否遵循了接口中的文档注释。
二. 继承与重写
严格继承:子类只能添加新方法,无法重写超类中的方法。
//Superclass
public class Car {
public final void drive() {...}
public final void brake() {...}
public final void accelerate() {...}
}
//Subclass
public class LuxuryCar extends Car {
public void playMusic() {...}
public void ejectCD() {...}
public void resumeMusic() {...}
public void pauseMusic() {...}
}
@Override
的使用:通知编译器这个方法必须和其父类中的某个方法的标识完全一样(覆盖)。
但是由于实现接口时编译器会自动检查我们的实现方法是否遵循了接口中的方法标识,这里的 @Override
更多是一种文档注释,它告诉读者这里的方法是为了实现某个接口,读者应该去阅读这个接口中的规格说明。
同时,如果你没有对实现类(子类型)的规格说明进行强化,这里就不需要再写一遍规格说明了。(DRY
原则)
重写的时候,不要改变原方法的本意。
为了让接口不必直接知道抽象层次最底层的名称,例如下面的 FastMyString
:
MyString s = new FastMyString(true);
System.out.println("The first character is: " + s.charAt(0));
我们可以为接口定义静态方法,即静态的工厂方法来实现创建:
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);
}
// ...
重写之后,可利用 super()
复用了父类型中函数的功能(构造方法中必须放在第一句),并对其进行扩展:
class Thought {
public void message() {
System.out.println("Thought.");
}
}
public class Advice extends Thought {
@Override // @Override annotation in Java 5 is optional but
public void message() {
super.message(); // Invoke parent's version of method.
System.out.println("Advice.");
}
}
Thought parking = new Thought();
parking.message(); // Prints "Thought."
Thought dates = new Advice();
dates.message(); // Prints "Advice \n Thought."
三. 泛型(参数型多态)
例如 Set<E>
,Set
是一个泛型类型。
这种类型的规格说明中用一个占位符(以后会被作为参数输入)表示具体类型,而不是分开为不同类型例如 Set<String>
, Set<Integer>
, 进行说明。
对于泛型接口的实现:
- 一个非泛型的实现(用一个特定的类型替代
E
,不适用于任意类型的元素)
public interface Set<E> {
...
}
public class CharSet1 implements Set<Character> {
...
}
- 一个泛型实现(保留类型占位符)
public interface Set<E> {
...
}
public class HashSet<E> implements Set<E> {
...
}
泛型的通配符 ?
,运行时泛型会进行擦除。
四. 枚举
枚举:当值域很小且有限时,将所有的值定义为被命名的常量。这样的类型往往会被用来组成更复杂的类型(DateTime
或者Latitude
),或者作为一个改某个方法的行为的参数使用(例如drawline
)。
public enum Month { JANUARY, FEBRUARY, MARCH, ..., DECEMBER };
这实质上定义了一个被命名的值的集合,由于这些值实际上是public static final
,所以我们将这个集合中的每个值的每个字母都大写。
枚举显式地列出了一个集合中的所有元素,并且JAVA
为每个元素都分配了数字作为代表它们的值。
对于枚举类型来说,==
和 equals()
效果一致。这是因为实际上只有一个对象来表示枚举类型的每个取值,且用户不可能创建更多的对象,它没有构造者方法。
Java
支持在 switch
中使用 enum
类型。Java
对枚举类型有更多的静态检查。
一个enum
声明中可以包含所有能在class
声明中常用字段和方法。所以你可以为这个ADT
定义额外的操作,并且还定义你自己的表示(成员变量)。
所有的enum
类型也都有一些内置的操作,这些操作在Enum
中定义:
ordinal()
是某个值在枚举类型中的索引值。compareTo()
基于两个值的索引值来比较两个值。name()
返回字符串形式表示的当前枚举类型值。toString()
和name()
是一样的。
五. 多态与重载
多态:让一个对象或一个类在不同的时间产生不同的变化:
- 特殊多态(重载):使用功能重载的许多语言都支持特殊多态。不同方法共用一个方法名。
- 参数化多态:泛型编程。
- 子类型
subtyping
多态、包含多态:当名称表示由某个公共父类关联的许多不同类的实例时。如:
Set<Criminal> senate = new HashSet<>();
1. 特殊多态(重载)
重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型。
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;
}
}
静态多态:
- 根据参数列表进行最佳匹配
- 静态类型检查
- 在编译阶段时决定要具体执行哪个方法
与之相反,重写方法是在运行时进行动态检查。
函数重载中的规则:重载的函数必须根据特性或数据类型有所不同。
- 不同的参数列表
- 相同 / 不同的返回值类型
- 相同 / 不同的
public
/private
/protected
- 相同 / 不同的异常
- 可以在同一个类内或子类中重载
- 方法名相同
Override
与 Overload
的比较
Overloading | Overriding | |
---|---|---|
参数列表 | 必须改变 | 禁止改变 |
返回类型 | 可以改变 | 禁止改变 |
异常 | 可以改变 | 可以减少或消除,禁止抛出新的或更广泛的 checked 异常 |
规约 | 可以改变 | 不能有更多的限制(可以少一些限制) |
调用 | 引用类型决定选择哪个重载版本(基于声明的参数类型)。在编译时发生。实际调用的方法仍然是在运行时发生的虚拟方法调用,但是编译器始终知道要调用的方法的签名。因此,在运行时,参数匹配将已经确定,只是不是方法所在的实际类 | 对象类型(换句话说,堆上实例的类型)决定了哪个方法被选中。发生在运行时 |
调用方法 / 参数看的是本身的类型而不是指向的对象,如下程序第三条对 dostuff()
的调用,调用的是第一个 dostuff()
方法:
class Animal {
public void eat() {}
}
class Horse extends Animal {
public void eat(String food) {}
}
public class UseAnimals {
//第一个
public void doStuff(Animal a) {
System.out.println("Animal");
}
public void doStuff(Horse h) {
System.out.println("Horse");
}
}
public class TestUseAnimals {
public static void main (String [] args) {
UseAnimals ua = new UseAnimals();
Animal animalobj = new Animal();
Horse horseobj = new Horse();
Animal animalRefToHorse = new Horse();
ua.doStuff(animalobj);
ua.doStuff(horseobj);
ua.doStuff(animalRefToHorse);//第三句
}
}
对于重载与重写,编译时决定是否能够调用,运行时决定调用哪种方法。
interface Animal {
void vocalize();
}
class Cow implements Animal {
public void vocalize() { moo(); }
public void moo() {System.out.println("Moo!"); }
}
在调用
Animal b = new Cow();
b.moo();
会发生编译错误。
2. 子类型多态
例如 ArrayList
和 LinkedList
是 List
的子类型。
B
是 A
的子类型意味着每一个 B
都是 A
,即每一个 B
都满足了 A
的规格说明。这也意味着 B
的规格说明至少强于 A
的规格说明。——任何一个 B
都是 A
。
然而编译器只会检查是否实现接口中规定的所有函数、标识是否对得上,但不会检查是否通过其他形式弱化了规格说明。
子类型多态:不同类型的对象可以统一的处理而无需区分。
instanceof
:获取当前正在运行对象的实际类型:
public void doSomething(Account acct) }
long adj = 0;
if (acct instanceof CheckingAccount) {
checkingAcct = (CheckingAccount) acct;
adj = checkingAcct.getFee();
} else if (acct instanceof SavingsAccount) {
savingsAcct = (SavingsAccount) acct;
adj = savingsAcct.getInterest();
}
...
}
少用这种方法。
不要出现向下转换的情况。
六. Java
中重要的方法
equals()
两个对象相等时返回true
。如果需要值语义,则@Override
。hashCode()
用于哈希映射的哈希代码。如果需要值语义,则@Override
。toString()
一个可打印的字符串表示。总是@Override
,除非你知道toString()
不会被调用。- 静态方法:类的所有对象所共享
- 实例方法:单独所有
七. 抽象类
类中至少有一个方法是抽象方法。