在本章的开头介绍过这样的思想,即要编写能够尽可能广泛地应用的代码。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。然后,我们就可以编写出无需修改就可以应用于更多情况的代码,即更加“泛化”的代码。
Java 泛型看起来是向这一方向迈进了一步。当你在编写或使用只是持有对象的泛型时,这些代码将可以工作于任何类型(除了基本类型,尽管正如你所见到的,自动装箱机制可以克服这一点)。或者,换个角度讲,“持有器”泛型能够声明:“我不关心你是什么类型”。如果代码不关心它将要作用的类型,那么这种代码就可以真正地应用于任何地方,并因此而相当泛化。
还是正如你所见到的,当要在泛型类型上执行操作(即调用 Object 方法之外的方法)时,就会产生问题。擦除强制要求指定可能会用到的泛型类型的边界,以安全地调用代码中的泛型对象上的具体方法。这是对“泛化”概念的一种明显的限制,因为必须限制你的泛型类型,使它们继承自特定的类,或者实现特定的接口。在某些情况下,你最终可能会使用普通类或普通接口,因为限定边界的泛型可能会和指定类或接口没有任何区别。
某些编程语言提供的一种解决方案称为_潜在类型机制_或_结构化类型机制_,而更古怪的术语称为_鸭子类型机制_,即“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以将它当作鸭子对待。”鸭子类型机制变成了一种相当流行的术语,可能是因为它不像其他的术语那样承载着历史的包袱。
泛型代码典型地只能在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码)。正由于此,潜在类型机制使得你可以横跨类继承结构,调用不属于某个公共接口的方法。因此,实际上一段代码可以声明:“我不关心你是什么类型,只要你可以 speak()
和 sit()
即可。”由于不要求具体类型,因此代码就可以更加泛化。
潜在类型机制是一种代码组织和复用机制。有了它,编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方。
支持潜在类型机制的语言包括 Python(可以从 www.Python.org 免费下载)、C++、Ruby、SmallTalk 和 Go。Python 是动态类型语言(几乎所有的类型检查都发生在运行时),而 C++ 和 Go 是静态类型语言(类型检查发生在编译期),因此潜在类型机制不要求静态或动态类型检查。
如果我们将上面的描述用 Python 来表示,如下所示:
# generics/DogsAndRobots.py
class Dog:
def speak(self):
print("Arf!")
def sit(self):
print("Sitting")
def reproduce(self):
pass
class Robot:
def speak(self):
print("Click!")
def sit(self):
print("Clank!")
def oilChange(self):
pass
def perform(anything):
anything.speak()
anything.sit()
a = Dog()
b = Robot()
perform(a)
perform(b)
output = """
Arf!
Sitting
Click!
Clank!
"""
Python 使用缩进来确定作用域(因此不需要任何花括号),而冒号将表示新的作用域的开始。“#” 表示注释到行尾,就像Java中的 “ // ”。类的方法需要显式地指定 this 引用的等价物作为第一个参数,按惯例成为 self 。构造器调用不要求任何类型的“ new ”关键字,并且 Python 允许普通(非成员)函数,就像 perform()
所表明的那样。
注意,在 perform(anything)
中,没有任何针对 anything 的类型,anything 只是一个标识符,它必须能够执行 perform()
期望它执行的操作,因此这里隐含着一个接口。但是你从来都不必显式地写出这个接口——它是潜在的。perform()
不关心其参数的类型,因此我可以向它传递任何对象,只要该对象支持 speak()
和 sit()
方法。如果传递给 perform()
的对象不支持这些操作,那么将会得到运行时异常。
输出规定使用三重引号创建带有内嵌换行符的字符串。
我们可以用 C++ 产生相同的效果:
// generics/DogsAndRobots.cpp
#include
using namespace std;
class Dog {
public:
void speak() { cout << "Arf!" << endl; }
void sit() { cout << "Sitting" << endl; }
void reproduce() {}
};
class Robot {
public:
void speak() { cout << "Click!" << endl; }
void sit() { cout << "Clank!" << endl; }
void oilChange() {}
};
template<class T> void perform(T anything) {
anything.speak();
anything.sit();
}
int main() {
Dog d;
Robot r;
perform(d);
perform(r);
}
/* Output:
Arf!
Sitting
Click!
Clank!
*/
在 Python 和 C++ 中,Dog 和 Robot 没有任何共同的东西,只是碰巧有两个方法具有相同的签名。从类型的观点看,它们是完全不同的类型。但是,perform()
不关心其参数的具体类型,并且潜在类型机制允许它接受这两种类型的对象。
C++ 确保了它实际上可以发送的那些消息,如果试图传递错误类型,编译器就会给你一个错误消息(这些错误消息从历史上看是相当可怕和冗长的,是 C++ 的模版名声欠佳的主要原因)。尽管它们是在不同时期实现这一点的,C++ 在编译期,而 Python 在运行时,但是这两种语言都可以确保类型不会被误用,因此被认为是强类型的。潜在类型机制没有损害强类型机制。
这里用 Go 语言编写相同的程序:
// generics/dogsandrobots.go
package main
import "fmt"
type Dog struct {}
func (this Dog) speak() { fmt.Printf("Arf!\n")}
func (this Dog) sit() { fmt.Printf("Sitting\n")}
func (this Dog) reproduce() {}
type Robot struct {}
func (this Robot) speak() { fmt.Printf("Click!\n") }
func (this Robot) sit() { fmt.Printf("Clank!\n") }
func (this Robot) oilChange() {}
func perform(speaker interface { speak(); sit() }) {
speaker.speak();
speaker.sit();
}
func main() {
perform(Dog{})
perform(Robot{})
}
/* Output:
Arf!
Sitting
Click!
Clank!
*/
Go 没有 class 关键字,但是可以使用上述形式创建等效的基本类:它通常不定义为类,而是定义为 struct ,在其中定义数据字段(此处不存在)。
对于每种方法,都以 func 关键字开头,然后(为了将该方法附加到您的类上)放在括号中,该括号包含对象引用,该对象引用可以是任何标识符,但是我在这里使用 this 来提醒您,就像在 C ++ 或 Java 中的 this 一样。 然后,在Go中像这样定义其余的函数。
Go也没有继承关系,因此这种“面向对象的目标”形式是相对原始的,并且可能是我无法花更多的时间来学习该语言的主要原因。 但是,Go 的组成很简单。
perform()
函数使用潜在类型:参数的确切类型并不重要,只要它包含了 speak()
和 sit()
方法即可。 该接口在此处匿名定义,内联,如 perform()
的参数列表所示。
main()
证明 perform()
确实对其参数的确切类型不在乎,只要可以在该参数上调用 talk()
和 sit()
即可。 但是,就像 C ++ 模板函数一样,在编译时检查类型。
语法 Dog {} 和 Robot {} 创建匿名的 Dog 和 Robot 结构。
因为泛型是在这场竞赛的后期才添加到 Java 中,因此没有任何机会可以去实现任何类型的潜在类型机制,因此 Java 没有对这种特性的支持。所以,初看起来,Java 的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。(使用擦除来实现 Java 泛型的实现有时称为第二类泛型类型)例如,在 Java 8 之前如果我们试图用 Java 实现上面 dogs-and-robots 的示例,那么就会被强制要求使用一个类或接口,并在边界表达式中指定它:
Performs.java
public interface Performs {
void speak();
void sit();
}
DogsAndRobots.java
class PerformingDog extends Dog implements Performs {
@Override
public void speak() {
System.out.println("Woof!");
}
@Override
public void sit() {
System.out.println("Sitting");
}
public void reproduce() {
}
}
class Robot implements Performs {
@Override
public void speak() {
System.out.println("Click!");
}
@Override
public void sit() {
System.out.println("Clank!");
}
public void oilChange() {
}
}
class Communicate {
public static <T extends Performs>
void perform(T performer) {
performer.speak();
performer.sit();
}
}
public class DogsAndRobots {
public static void main(String[] args) {
Communicate.perform(new PerformingDog());
Communicate.perform(new Robot());
}
}
Dog.java
public class Dog extends Pet {
public Dog(String name) {
super(name);
}
public Dog() {
super();
}
}
Individual.java
public class Pet extends Individual {
public Pet(String name) {
super(name);
}
public Pet() {
super();
}
}
Individual.java
import java.util.*;
public class Individual implements Comparable<Individual> {
private static long counter = 0;
private final long id = counter++;
private String name;
public Individual(String name) {
this.name = name;
}
// 'name' is optional:
public Individual() {
}
@Override
public String toString() {
return getClass().getSimpleName() +
(name == null ? "" : " " + name);
}
public long id() {
return id;
}
@Override
public boolean equals(Object o) {
return o instanceof Individual &&
Objects.equals(id, ((Individual) o).id);
}
@Override
public int hashCode() {
return Objects.hash(name, id);
}
@Override
public int compareTo(Individual arg) {
// Compare by class name first:
String first = getClass().getSimpleName();
String argFirst = arg.getClass().getSimpleName();
int firstCompare = first.compareTo(argFirst);
if (firstCompare != 0) {
return firstCompare;
}
if (name != null && arg.name != null) {
int secondCompare = name.compareTo(arg.name);
if (secondCompare != 0) {
return secondCompare;
}
}
return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
}
}
但是要注意,perform()
不需要使用泛型来工作,它可以被简单地指定为接受一个 Performs 对象:
class CommunicateSimply {
static void perform(Performs performer) {
performer.speak();
performer.sit();
}
}
public class SimpleDogsAndRobots {
public static void main(String[] args) {
CommunicateSimply.perform(new PerformingDog());
CommunicateSimply.perform(new Robot());
}
}
在本例中,泛型不是必需的,因为这些类已经被强制要求实现 Performs 接口。