目录
潜在类型机制
支持潜在类型机制的语言
Python的潜在类型机制
C++的潜在类型机制
Java中的直接潜在类型机制
潜在类型机制的替代方案
反射
将方法应用于序列中的每个元素
Java 8的潜在类型机制(间接实现)
潜在类型机制的使用例(Suppliers)
总结
本笔记参考自: 《On Java 中文版》
潜在类型机制
通过泛型,我们应该可以向“将代码写得更通用一点”这一理念更进一步。特别是在编写简单的Java泛型时,泛型可以在不了解具体类型的情况下执行方法。然而,随着对Java泛型的了解逐渐深入,类型擦除无疑使泛型的作用打了折扣,并限制了“泛型”这一概念。
一些语言会提供潜在类型机制(又称结构化类型机制),它还有一个更有意思的名称:鸭子类型机制。比方说,“如果某件事物走路像鸭子,说话也像鸭子,那么就可以把它看做鸭子”。
与泛型不同,潜在类型机制只对方法本身有所要求,而不需要实现特别的类或接口。
潜在类型机制可以超越类的层次结构,调用不属于某个公共接口的方法。
支持潜在类型机制的语言
有许多语言支持潜在类型机制,例如Python、C++和Go等。
Python的潜在类型机制
先看看Python中的潜在类型机制:
【例子:Python中的潜在类型机制】
class Dog:
def speak(self):
print("汪!")
def sit(self):
print("坐着")
def reproduce(self):
pass
class Robot:
def speak(self):
print("锵!")
def sit(self):
print("咔")
def oilChange(self):
pass
def perform(anything):
anything.speak()
anything.sit()
a = Dog()
b = Robot()
perform(a)
perform(b)
程序执行的结果是:
在perform(anything)中并不包含任何关于anything的类型信息,anything只是一个标识符。在后台,anything相当于一个被隐藏的接口,它包含了perform()要求的操作。但我们无需显式地表明它,因为它是潜在的。
而如果向perform()传入不支持操作的对象,就会报错:
----------
C++的潜在类型机制
同样可以用C++来实现上面的例子:
【例子:C++中的潜在类型机制】
#include<iostream>
using namespace std;
class Dog {
public:
void speak() {
cout << "汪!" << endl;
}
void sit() {
cout << "坐着" << endl;
}
};
class Robot {
public:
void speak() {
cout << "锵!" << endl;
}
void sit() {
cout << "咔" << endl;
}
void oilChange() {}
};
template<class T> void perform(T anything) {
anything.speak();
anything.sit();
}
int main() {
Dog d;
Robot r;
perform(d);
perform(r);
}
程序执行的结果相同:
若试图传入错误的类型,编译器也会报错。不同与Python,C++会在运行时抛出错误(尽管C++的错误信息出了名的冗长)。但这两门语言都保证了类型不会错用。
----------
也可以使用Go实现这个例子:
【例子:Go中的潜在类型机制】
package main
import "fmt"
type Dog struct{}
func (this Dog) speak() {
fmt.Printf("汪!\n")
}
func (this Dog) sit(){
fmt.Printf("坐着\n")
}
func (this Dog) reproduce(){
}
type Robot struct{}
func (this Robot) speak() {
fmt.Printf("锵!\n")
}
func (this Robot) sit(){
fmt.Printf("咔\n")
}
func (this Robot) oilChange(){
}
func perform(speaker interface {speak(); sit()}){
speaker.speak()
speaker.sit()
}
func main(){
perform(Dog{})
perform(Robot{})
}
程序会得到相同的结果:
Java中的直接潜在类型机制
Java的泛型加入得较晚,因此没有支持潜在类型机制。因此,若要在Java中实现上面所述的效果,通常会需要使用接口(并且使用边界):
【例子:通过接口模拟潜在类型机制】
import reflection.pets.Dog;
class PerformingDog extends Dog implements Performs {
@Override
public void speak() {
System.out.println("汪!");
}
@Override
public void sit() {
System.out.println("坐下");
}
public void reproduce() {
}
}
class Robot implements Performs {
@Override
public void speak() {
System.out.println("锵!");
}
@Override
public void sit() {
System.out.println("咔");
}
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());
}
}
程序执行的结果是:
然而,仔细考虑这种做法会发现,Communicate.perform()并不需要使用到泛型,它可以直接使用Performs接口:
class Communicate {
public static void perform(Performs performer) {
performer.speak();
performer.sit();
}
}
说到底,无论是PerformingDog还是Robot都已经强制实现了Performs接口。
潜在类型机制的替代方案
尽管Java并没有(直接)支持潜在类型机制,但我们依旧可以想办法创建出真正意义上的泛型代码,实现方法的跨层次应用。
反射
一个方案是使用反射:
【例子:使用反射创建泛型代码】
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
// 这个类没有实现Perform接口:
class Mime {
public void walkAgainstTheWind() {
}
public void sit() {
System.out.println("假装坐着");
}
public void pushInvisibleWalls() {
}
@Override
public String toString() {
return "哑剧";
}
}
class SmartDog {
public void speak() {
System.out.println("汪!");
}
public void sit() {
System.out.println("坐下");
}
public void reproduce() {
}
}
class CommunicateReflectively {
public static void perform(Object speaker) {
Class<?> spkr = speaker.getClass();
try {
try {
Method speak = spkr.getMethod("speak");
speak.invoke(speaker);
} catch (NoSuchMethodException e) {
System.out.println(speaker + "没法说话");
}
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
} catch (NoSuchMethodException e) {
System.out.println(speaker + "无法坐下");
}
} catch (SecurityException |
IllegalAccessException |
IllegalArgumentException |
InvocationTargetException e) {
throw new RuntimeException(speaker.toString(), e);
}
}
}
public class LatentReflection {
public static void main(String[] args) {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Robot());
CommunicateReflectively.perform(new Mime());
}
}
程序执行的结果是:
在这里,SmartDog、Robot和Mime之间没有任何的直接联系,我们直接通过反射动态调用speak()和sit()。
将方法应用于序列中的每个元素
还可以进一步地开发反射。假设我们需要为一个序列中的每个元素应用方法,只使用接口仍是不够的,因此我们可以这样做:
【例子:将一个方法应用于序列】
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Apply {
public static <T, S extends Iterable<T>>
void apply(S seq, Method f, Object... args) {
try {
for (T t : seq)
f.invoke(t, args);
} catch (IllegalAccessException |
IllegalArgumentException |
InvocationTargetException e) {
throw new RuntimeException(e); // 可能会因为不正确的使用该方法导致异常
}
}
}
apply()方法可以接受任意数量的序列元素,并将方法f()应用于所有元素。这种做法有一个好处,f.invoke()可以接受任意长度的序列,因此可以认为apply()也可以做到这点。
除此之外,apply()方法使用了for-in语法。 这表示着S可以是任何实现了Iterable接口的类。
接下来就对apply()方法进行测试:
【例子:apply()方法的使用例】
首先创建一个简单的继承结构,这个结构包含一个父类Shape和一个子类Square。
===父类Shape:
public class Shape {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + " " + id;
}
public void rotate() {
System.out.println(this + " rotate");
}
public void resize(int newSize) {
System.out.println(this + " resize " + newSize);
}
}
==子类Square:
public class Square extends Shape {}
接下来的就是测试类ApplyTest。
===ApplyTest:
import onjava.Suppliers;
import java.util.ArrayList;
import java.util.List;
public class ApplyTest {
public static void main(String[] args)
throws Exception {
List<Shape> shapes =
Suppliers.create(ArrayList::new, Shape::new, 3);
Apply.apply(shapes,
Shape.class.getMethod("rotate"));
Apply.apply(shapes,
Shape.class.getMethod("resize", int.class), 7);
System.out.println();
List<Square> squares =
Suppliers.create(ArrayList::new, Square::new, 3);
Apply.apply(squares,
Shape.class.getMethod("rotate"));
Apply.apply(squares,
Shape.class.getMethod("resize", int.class), 7);
System.out.println();
Apply.apply(new FilledList<>(Shape::new, 3),
Shape.class.getMethod("rotate"));
Apply.apply(new FilledList<>(Square::new, 3),
Shape.class.getMethod("rotate"));
}
}
程序执行的结果是:
首先解释Suppliers.create(),看如下代码:
这行代码等价于,将Shape类的构造器作为生成器,生成3个对象,并将结果放入一个ArrayList对象中。
尽管反射可以使代码看起来很优雅,但要注意,反射的运行速度通常会慢于非反射的实现。究其原因,反射在运行时处理了太多东西。尽管我们不应该只因为这个理由放弃使用反射,但这无疑是我们必须考虑的一点。
现在考虑Java 8加入的函数式方式,下面的例子通过它重写了ApplyTest:
【例子:使用函数式方式重写ApplyTest】
import java.util.stream.Stream;
public class ApplyFunctional {
public static void main(String[] args) {
Stream.of(Stream.generate(Shape::new).limit(2),
Stream.generate(Square::new).limit(2))
.flatMap(c -> c) // 将所有元素扁平化,合成一条流
.peek(Shape::rotate)
.forEach(s -> s.resize(7));
System.out.println();
new FilledList<>(Shape::new, 2)
.forEach(Shape::rotate);
new FilledList<>(Square::new, 2)
.forEach(Shape::rotate);
}
}
程序执行的结果是:
这种重写方式已经抛弃了Apply.apply()了。
这么做的好处是,它更加简洁、可读性高并且不会抛出异常。因此现在可以这么说,我们只需要在某些只能使用反射来解决的场景中使用反射就好了。
Java 8的潜在类型机制(间接实现)
Java 8带来的未绑定方法引用可以在某种程度上实现潜在类型机制。
【例子:使用方法引用实现潜在类型机制】
import reflection.pets.Dog;
import java.util.function.Consumer;
class PerformingDogA extends Dog {
public void speak() {
System.out.println("汪!");
}
public void sit() {
System.out.println("坐下");
}
public void reproduce() {
}
}
class RobotA {
public void speak() {
System.out.println("锵!");
}
public void sit() {
System.out.println("咔");
}
public void oilChange() {
}
}
class CommunicateA {
public static <P> void perform(
P performer, Consumer<P> action1, Consumer<P> action2) {
action1.accept(performer);
action2.accept(performer);
}
}
public class DogsAndRobotMethodReferences {
public static void main(String[] args) {
CommunicateA.perform(new PerformingDogA(),
PerformingDogA::speak, PerformingDogA::sit);
CommunicateA.perform(new RobotA(),
RobotA::speak, RobotA::sit);
CommunicateA.perform(new Mime(),
Mime::walkAgainstTheWind, Mime::pushInvisibleWalls);
}
}
程序执行结果是:
因为CommunicateA.perform()没有对P做出限制,因此它可以是任何类型。perform()只要求提供可供Consumer<P>使用的方法。因此我们可以向其中传入任何与其签名一致的方法。
然而,和真正的潜在类型机制相比,这种做法需要我们显式地提供perform()会用到的方法引用。
但这种方式也有更好的一点,它其实不会对传入其中的方法名做出要求。在这个意义上,这种方法要更加通用。
潜在类型机制的使用例(Suppliers)
现在可以通过潜在类型机制创建Suppliers(这个类在之前的笔记中也曾出现)。这个类的方法都用于填充集合:
【例子:潜在类型机制的使用例】
import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class Suppliers {
// 根据factory创建一个新的集合,并且填充它:
public static <T, C extends Collection<T>> C
create(Supplier<C> factory, Supplier<T> gen, int n) {
return Stream.generate(gen)
.limit(n)
.collect(factory, C::add, C::addAll);
}
// 填充已存在的集合coll:
public static <T, C extends Collection<T>>
C fill(C coll, Supplier<T> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(coll::add);
return coll;
}
// 使用到未绑定方法引用adder,可以生成更加通用的方法:
public static <H, A> H fill(
H holder, BiConsumer<H, A> adder,
Supplier<A> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(a -> adder.accept(holder, a));
return holder;
}
}
在第一个fill()方法中,我们返回了coll(即传入的容器的类型信息),这样就能保证不会丢失类型信息了。
第二个fill()方法使用了未绑定的方法引用adder。通过adder.accept(),我们可以将操作a应用于对象holder。
接下来可以尝试使用Suppliers了。
【例子:使用Suppliers】
import onjava.Suppliers;
import java.util.ArrayList;
import java.util.List;
class Customer {
private static long counter = 1;
private final long id = counter++;
@Override
public String toString() {
return "Customer " + id;
}
}
class Teller {
private static long counter = 1;
private final long id = counter++;
@Override
public String toString() {
return "Teller " + id;
}
}
class Bank {
private List<BankTeller> tellers =
new ArrayList<>();
public void put(BankTeller bt) {
tellers.add(bt);
}
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " 服务于 " + c);
}
public static void main(String[] args) {
// 使用create():
RandomList<Teller> tellers =
Suppliers.create(
RandomList::new, Teller::new, 4);
// 演示第一个fill():
List<Customer> customers = Suppliers.fill(
new ArrayList<>(), Customer::new, 12);
customers.forEach(c ->
serve(tellers.select(), c));
// 演示潜在类型机制:
Bank bank = Suppliers.fill(
new Bank(), Bank::put, BankTeller::new, 3);
// 或使用第二个fill():
List<Customer> customers2 = Suppliers.fill(
new ArrayList<>(), List::add, Customer::new, 12);
}
}
程序执行的结果是:
注意第二个fill()的两个使用,我们不仅可以将有所关联的Bank类型,还可以将其用于List。
总结
尽管Java中加入的泛型存在着一些问题,但在此之前我们需要注意一点:无论一门语言多么强大,都有可能被用来编写一个无比糟糕的程序。泛型常常被用于集合之类的场景,但除此之外,泛型还能做到什么?
泛型的概念应该就像它的名字一样,追求的是一种更加“泛型(泛化)”的代码,使一种代码能够适应更多的场景。