本章概要
- 动态类型安全
- 泛型异常
- 混型
- C++ 中的混型
- 与接口混合
- 使用装饰器模式
- 与动态代理混合
动态类型安全
因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的 java.util.Collections 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 checkedCollection()
、checkedList()
、 checkedMap()
、 checkedSet()
、checkedSortedMap()
和 checkedSortedSet()
。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
受检查的集合在你试图插入类型不正确的对象时抛出 ClassCastException ,这与泛型之前的(原生)集合形成了对比,对于后者来说,当你将对象从集合中取出时,才会通知你出现了问题。在后一种情况中,你知道存在问题,但是不知道罪魁祸首在哪里,如果使用受检查的集合,就可以发现谁在试图插入不良对象。
让我们用受检查的集合来看看“将猫插入到狗列表中”这个问题。这里,oldStyleMethod()
表示遗留代码,因为它接受的是原生的 List ,而 @SuppressWarnings(“unchecked”) 注解对于压制所产生的警告是必需的:
CheckedList.java
import java.util.*;
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<>();
oldStyleMethod(dogs1); // Quietly accepts a Cat
List<Dog> dogs2 = Collections.checkedList(new ArrayList<>(), Dog.class);
try {
oldStyleMethod(dogs2); // Throws an exception
} catch (Exception e) {
System.out.println("Expected: " + e);
}
// Derived types work fine:
List<Pet> pets = Collections.checkedList(new ArrayList<>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
}
其它相关类
Cat.java
public class Cat extends Pet {
public Cat(String name) {
super(name);
}
public Cat() {
super();
}
}
Dog.java
public class Dog extends Pet {
public Dog(String name) {
super(name);
}
public Dog() {
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));
}
}
Pet.java
public class Pet extends Individual {
public Pet(String name) {
super(name);
}
public Pet() {
super();
}
}
运行这个程序时,你会发现插入一个 Cat 对于 dogs1 来说没有任何问题,而 dogs2 立即会在这个错误类型的插入操作上抛出一个异常。还可以看到,将导出类型的对象放置到将要检查基类型的受检查容器中是没有问题的。
泛型异常
由于擦除的原因,catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 Throwable(这将进一步阻止你去定义不能捕获的泛型异常)。
但是,类型参数可能会在一个方法的 throws 子句中用到。这使得你可以编写随检查型异常类型变化的泛型代码:
import java.util.*;
interface Processor<T, E extends Exception> {
void process(List<T> resultCollector) throws E;
}
class ProcessRunner<T, E extends Exception> extends ArrayList<Processor<T, E>> {
List<T> processAll() throws E {
List<T> resultCollector = new ArrayList<>();
for (Processor<T, E> processor : this) {
processor.process(resultCollector);
}
return resultCollector;
}
}
class Failure1 extends Exception {
}
class Processor1
implements Processor<String, Failure1> {
static int count = 3;
@Override
public void process(List<String> resultCollector)
throws Failure1 {
if (count-- > 1) {
resultCollector.add("Hep!");
} else {
resultCollector.add("Ho!");
}
if (count < 0) {
throw new Failure1();
}
}
}
class Failure2 extends Exception {
}
class Processor2
implements Processor<Integer, Failure2> {
static int count = 2;
@Override
public void process(List<Integer> resultCollector)
throws Failure2 {
if (count-- == 0) {
resultCollector.add(47);
} else {
resultCollector.add(11);
}
if (count < 0) {
throw new Failure2();
}
}
}
public class ThrowGenericException {
public static void main(String[] args) {
ProcessRunner<String, Failure1> runner = new ProcessRunner<>();
for (int i = 0; i < 3; i++) {
runner.add(new Processor1());
}
try {
System.out.println(runner.processAll());
} catch (Failure1 e) {
System.out.println(e);
}
ProcessRunner<Integer, Failure2> runner2 =
new ProcessRunner<>();
for (int i = 0; i < 3; i++) {
runner2.add(new Processor2());
}
try {
System.out.println(runner2.processAll());
} catch (Failure2 e) {
System.out.println(e);
}
}
}
Processor 执行 process()
方法,并且可能会抛出具有类型 E 的异常。process()
的结果存储在 List<T>resultCollector
中(这被称为_收集参数_)。ProcessRunner 有一个 processAll()
方法,它会在所持有的每个 Process 对象执行,并返回 resultCollector 。
如果不能参数化所抛出的异常,那么由于检查型异常的缘故,将不能编写出这种泛化的代码。
混型
术语_混型_随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。
混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点_面向切面编程_ (AOP) 的味道,而切面经常被建议用来解决混型问题。
C++ 中的混型
在 C++ 中,使用多重继承的最大理由,就是为了使用混型。但是,对于混型来说,更有趣、更优雅的方式是使用参数化类型,因为混型就是继承自其类型参数的类。在 C++ 中,可以很容易地创建混型,因为 C++ 能够记住其模版参数的类型。
下面是一个 C++ 示例,它有两个混型类型:一个使得你可以在每个对象中混入拥有一个时间戳这样的属性,而另一个可以混入一个序列号。
// generics/Mixins.cpp
#include <string>
#include <ctime>
#include <iostream>
using namespace std;
template<class T> class TimeStamped : public T {
long timeStamp;
public:
TimeStamped() { timeStamp = time(0); }
long getStamp() { return timeStamp; }
};
template<class T> class SerialNumbered : public T {
long serialNumber;
static long counter;
public:
SerialNumbered() { serialNumber = counter++; }
long getSerialNumber() { return serialNumber; }
};
// Define and initialize the static storage:
template<class T> long SerialNumbered<T>::counter = 1;
class Basic {
string value;
public:
void set(string val) { value = val; }
string get() { return value; }
};
int main() {
TimeStamped<SerialNumbered<Basic>> mixin1, mixin2;
mixin1.set("test string 1");
mixin2.set("test string 2");
cout << mixin1.get() << " " << mixin1.getStamp() <<
" " << mixin1.getSerialNumber() << endl;
cout << mixin2.get() << " " << mixin2.getStamp() <<
" " << mixin2.getSerialNumber() << endl;
}
/* Output:
test string 1 1452987605 1
test string 2 1452987605 2
*/
在 main()
中, mixin1 和 mixin2 所产生的类型拥有所混入类型的所有方法。可以将混型看作是一种功能,它可以将现有类映射到新的子类上。注意,使用这种技术来创建一个混型是多么的轻而易举。基本上,只需要声明“这就是我想要的”,紧跟着它就发生了:
TimeStamped<SerialNumbered<Basic>> mixin1,mixin2;
遗憾的是,Java 泛型不允许这样。擦除会忘记基类类型,因此
泛型类不能直接继承自一个泛型参数
这突显了许多我在 Java 语言设计决策(以及与这些功能一起发布)中遇到的一大问题:处理一件事很有希望,但是当您实际尝试做一些有趣的事情时,您会发现自己做不到。
与接口混合
一种更常见的推荐解决方案是使用接口来产生混型效果,就像下面这样:
import java.util.*;
interface TimeStamped {
long getStamp();
}
class TimeStampedImp implements TimeStamped {
private final long timeStamp;
TimeStampedImp() {
timeStamp = new Date().getTime();
}
@Override
public long getStamp() {
return timeStamp;
}
}
interface SerialNumbered {
long getSerialNumber();
}
class SerialNumberedImp implements SerialNumbered {
private static long counter = 1;
private final long serialNumber = counter++;
@Override
public long getSerialNumber() {
return serialNumber;
}
}
interface Basic {
void set(String val);
String get();
}
class BasicImp implements Basic {
private String value;
@Override
public void set(String val) {
value = val;
}
@Override
public String get() {
return value;
}
}
class Mixin extends BasicImp
implements TimeStamped, SerialNumbered {
private TimeStamped timeStamp = new TimeStampedImp();
private SerialNumbered serialNumber = new SerialNumberedImp();
@Override
public long getStamp() {
return timeStamp.getStamp();
}
@Override
public long getSerialNumber() {
return serialNumber.getSerialNumber();
}
}
public class Mixins {
public static void main(String[] args) {
Mixin mixin1 = new Mixin(), mixin2 = new Mixin();
mixin1.set("test string 1");
mixin2.set("test string 2");
System.out.println(mixin1.get() + " " +
mixin1.getStamp() + " " + mixin1.getSerialNumber());
System.out.println(mixin2.get() + " " +
mixin2.getStamp() + " " + mixin2.getSerialNumber());
}
}
Mixin 类基本上是在使用_委托_,因此每个混入类型都要求在 Mixin 中有一个相应的域,而你必须在 Mixin 中编写所有必需的方法,将方法调用转发给恰当的对象。这个示例使用了非常简单的类,但是当使用更复杂的混型时,代码数量会急速增加。
使用装饰器模式
当你观察混型的使用方式时,就会发现混型概念好像与_装饰器_设计模式关系很近。装饰器经常用于满足各种可能的组合,而直接子类化会产生过多的类,因此是不实际的。
装饰器模式使用分层对象来动态透明地向单个对象中添加责任。装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。某些事物是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。这使得对装饰器的使用是透明的——无论对象是否被装饰,你都拥有一个可以向对象发送的公共消息集。装饰类也可以添加新方法,但是正如你所见,这将是受限的。
装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。因此可以将基于参数化类型的混型当作是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。
前面的示例可以被改写为使用装饰器:
import java.util.*;
class Basic {
private String value;
public void set(String val) {
value = val;
}
public String get() {
return value;
}
}
class Decorator extends Basic {
protected Basic basic;
Decorator(Basic basic) {
this.basic = basic;
}
@Override
public void set(String val) {
basic.set(val);
}
@Override
public String get() {
return basic.get();
}
}
class TimeStamped extends Decorator {
private final long timeStamp;
TimeStamped(Basic basic) {
super(basic);
timeStamp = new Date().getTime();
}
public long getStamp() {
return timeStamp;
}
}
class SerialNumbered extends Decorator {
private static long counter = 1;
private final long serialNumber = counter++;
SerialNumbered(Basic basic) {
super(basic);
}
public long getSerialNumber() {
return serialNumber;
}
}
public class Decoration {
public static void main(String[] args) {
TimeStamped t = new TimeStamped(new Basic());
TimeStamped t2 = new TimeStamped(new SerialNumbered(new Basic()));
//- t2.getSerialNumber(); // Not available
SerialNumbered s = new SerialNumbered(new Basic());
SerialNumbered s2 = new SerialNumbered(new TimeStamped(new Basic()));
//- s2.getStamp(); // Not available
}
}
产生自泛型的类包含所有感兴趣的方法,但是由使用装饰器所产生的对象类型是最后被装饰的类型。也就是说,尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。
因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰中的一层(最后一层),而混型方法显然会更自然一些。因此,装饰器只是对由混型提出的问题的一种局限的解决方案。
与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模型的机制(查看 类型信息 一章中关于 Java 的动态代理如何工作的解释)。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。
由于动态代理的限制,每个被混入的类都必须是某个接口的实现:
DynamicProxyMixin.java
import java.lang.reflect.*;
import java.util.*;
import static com.example.test.Tuple.tuple;
class MixinProxy implements InvocationHandler {
Map<String, Object> delegatesByMethod;
@SuppressWarnings("unchecked")
MixinProxy(Tuple2<Object, Class<?>>... pairs) {
delegatesByMethod = new HashMap<>();
for (Tuple2<Object, Class<?>> pair : pairs) {
for (Method method : pair.a2.getMethods()) {
String methodName = method.getName();
// The first interface in the map
// implements the method.
if (!delegatesByMethod.containsKey(methodName)) {
delegatesByMethod.put(methodName, pair.a1);
}
}
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Object delegate = delegatesByMethod.get(methodName);
return method.invoke(delegate, args);
}
@SuppressWarnings("unchecked")
public static Object newInstance(Tuple2... pairs) {
Class[] interfaces = new Class[pairs.length];
for (int i = 0; i < pairs.length; i++) {
interfaces[i] = (Class) pairs[i].a2;
}
ClassLoader cl = pairs[0].a1.getClass().getClassLoader();
return Proxy.newProxyInstance(cl, interfaces, new MixinProxy(pairs));
}
}
public class DynamicProxyMixin {
public static void main(String[] args) {
Object mixin = MixinProxy.newInstance(
tuple(new BasicImp(), Basic.class),
tuple(new TimeStampedImp(), TimeStamped.class),
tuple(new SerialNumberedImp(), SerialNumbered.class));
Basic b = (Basic) mixin;
TimeStamped t = (TimeStamped) mixin;
SerialNumbered s = (SerialNumbered) mixin;
b.set("Hello");
System.out.println(b.get());
System.out.println(t.getStamp());
System.out.println(s.getSerialNumber());
}
}
Tuple.java
public class Tuple {
public static <A, B> Tuple2<A, B> tuple(A a, B b) {
return new Tuple2<>(a, b);
}
public static <A, B, C> Tuple3<A, B, C>
tuple(A a, B b, C c) {
return new Tuple3<>(a, b, c);
}
public static <A, B, C, D> Tuple4<A, B, C, D>
tuple(A a, B b, C c, D d) {
return new Tuple4<>(a, b, c, d);
}
public static <A, B, C, D, E>
Tuple5<A, B, C, D, E> tuple(A a, B b, C c, D d, E e) {
return new Tuple5<>(a, b, c, d, e);
}
}
Tuple2.java
public class Tuple2<A, B> {
public final A a1;
public final B a2;
public Tuple2(A a, B b) {
a1 = a;
a2 = b;
}
public String rep() {
return a1 + ", " + a2;
}
@Override
public String toString() {
return "(" + rep() + ")";
}
}
Tuple3.java
public class Tuple3<A, B, C> extends Tuple2<A, B> {
public final C a3;
public Tuple3(A a, B b, C c) {
super(a, b);
a3 = c;
}
@Override public String rep() {
return super.rep() + ", " + a3;
}
}
Tuple4.java
public class Tuple4<A, B, C, D> extends Tuple3<A, B, C> {
public final D a4;
public Tuple4(A a, B b, C c, D d) {
super(a, b, c);
a4 = d;
}
@Override
public String rep() {
return super.rep() + ", " + a4;
}
}
Tuple5.java
public class Tuple5<A, B, C, D, E> extends Tuple4<A, B, C, D> {
public final E a5;
public Tuple5(A a, B b, C c, D d, E e) {
super(a, b, c, d);
a5 = e;
}
@Override
public String rep() {
return super.rep() + ", " + a5;
}
}
因为只有动态类型而不是静态类型才包含所有的混入类型,因此这仍旧不如 C++ 的方式好,因为可以在具有这些类型的对象上调用方法之前,你被强制要求必须先将这些对象向下转型到恰当的类型。但是,它明显地更接近于真正的混型。
为了让 Java 支持混型,人们已经做了大量的工作朝着这个目标努力,包括创建了至少一种附加语言( Jam 语言),它是专门用来支持混型的。