上一篇推文中我们学会了什么是泛型单例工厂,并且了解了它的实现原理:从泛型的使用情况看出你对语言的理解程度(2)。今天我们继续最后一篇关于泛型的学习。
Part1
类型参数和通配符之间存在对偶性,可以使用其中一种方法声明许多方法。例如,下面是静态方法的两种可能声明,用于交换列表中的两个索引项。第一个使用无界类型参数,第二个使用无界通配符:
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
额,可能有些人还不知道对偶性是什么意思,在这说一下:对偶性即导致相同的结果,而表面上不同的理论之间的对应。也就是说上面的两行代码,实现的方式不同但是效果是一样的。
那么这两个方式在作为公共API实现的时候哪个更好呢?答案是用无界通配符的第二种方式。
传入一个列表(任意列表),该方法交换索引元素。不需要担心类型参数。通常,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它; 如果它是有界类型参数,则用有界通配符替换它。
例如,我们swap方法的时候,第一种方式必须使用类型参数限制list,否则会报一个Unchecked assignment的警告:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(); //必须添加类型参数限制
swap(list, 1, 2);
}
而第二种则不需要此种限制,且不会有编译警告:
public static void main(String[] args) {
List list = new ArrayList();
swap(list, 1, 2);
}
很明显,第二种更适合作为公用API来使用。
Part2
集合是泛型的常见应用之一,如 Set<E> 和 Map<K,V>,以及单元素容器,如 ThreadLocal<T> 和 AtomicReference<T>。在所有这些应用中,都是参数化的容器。这将每个容器的类型参数限制为固定数量。通常这正是你想要的。Set 只有一个类型参数,表示其元素类型;Map 有两个,表示键和值的类型;如此等等。
然而,有时你需要更大的灵活性。例如,一个数据库行可以有任意多列,能够以类型安全的方式访问所有这些列是很好的。幸运的是,有一种简单的方法可以达到这种效果。其思想是参数化键而不是容器。然后向容器提供参数化键以插入或检索值。泛型类型系统用于确保值的类型与键一致。
作为这种方法的一个简单示例,考虑一个 Favorites 类,它允许客户端存储和检索任意多种类型的 Favorites 实例。Class 类的对象将扮演参数化键的角色。这样做的原因是 Class 类是泛型的。Class 对象的类型不仅仅是 Class,而是 Class<T>。例如,String.class 的类型为 Class<String>、Integer.class 的类型为 Class<Integer>。在传递编译时和运行时类型信息的方法之间传递类 Class 对象时,它被称为类型标记。
avorites 类的 API 很简单。它看起来就像一个简单的 Map,只不过键是参数化的,而不是整个 Map。客户端在设置和获取 Favorites 实例时显示一个 Class 对象。以下是 API:
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
下面是一个示例程序,它演示了 Favorites 类、存储、检索和打印 Favorites 字符串、整数和 Class 实例:
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,favoriteInteger, favoriteClass.getName());
}
Favorites 的实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。它也是异构的:与普通 Map 不同,所有键都是不同类型的。因此,我们将 Favorites 称为一个类型安全异构容器。
这里发生了一些微妙的事情。每个 Favorites 实例都由一个名为 favorites 的私有 Map<Class<?>, Object> 支持。你可能认为由于通配符类型是无界的,所以无法将任何内容放入此映射中,但事实恰恰相反。需要注意的是,通配符类型是嵌套的:通配符类型不是 Map 的类型,而是键的类型。这意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>,下一个是 Class<Integer>,等等。这就是异构的原理。
总之,以集合的 API 为例的泛型在正常使用时将每个容器的类型参数限制为固定数量。你可以通过将类型参数放置在键上而不是容器上来绕过这个限制。你可以使用 Class 对象作为此类类型安全异构容器的键。以这种方式使用的 Class 对象称为类型标记。还可以使用自定义键类型。例如,可以使用 DatabaseRow 类型表示数据库行(容器),并使用泛型类型 Column<T> 作为它的键。
关注公众号获取更多内容,有问题也可在公众号提问哦:
强哥叨逼叨
叨逼叨编程、互联网的见解和新鲜事