泛型 -核心使用原则和原理

22 篇文章 0 订阅
18 篇文章 0 订阅

26. 不要使用原生类型

Java的泛型比较受限。
所有的泛型标记,都会在编译之后被完全擦除,变成其上界的类(很多时候是Object),而且Java的泛型也不是强制的,所以我们会情不自禁地想,为何不直接使用List?它不就等于List吗?
答案是否定的。

我们期望程序的错误尽可能在编译而不是在运行时发出警告。泛型,正好提供了很多这样的安全限制,它将很多错误从运行时移动到编译时,防止我们的程序在某天给我们惊喜。

这我们非常好理解,如果使用了原生态类型,就是去了泛型在安全性和描述性方面的所有优势。

List和List的区别

区别在于,后者是不安全的,我们来看例子。

List<String> strings = new ArrayList<>();
// 编译通过,运行显然会出问题
List list = strings;
list.add(45);
// 运行出错,类型转换错误
String s = strings.get(0);
// 编译失败
List<Object> list = strings;

由于泛型不是协变的(查看泛型相关文章),所以实际上List是List的父类,而List不是,所以编译器会阻止这种转换,保证安全性,正如你在第八行看到的。

通配符类型的使用

有时候我们根本不知道类型,例如下面:

// Use of raw type for unknown element type - don't do this!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
        	result++;
    return result;
}

这种写法是不安全的——如果我们以为不清楚s1和s2内部的保存类型,而在方法中随意添加元素,这就会出错。
最佳实践是使用通配符。通配符规则是,使用通配符之后,不得向其中添加除null外的所有元素,因为编译器无法得知“?”代表什么类型。这一点,请参考<? extends Fruit>

// Uses unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

原生类型两个可用场景

可以使用原生类型的例外有两个:

  • 一个是类标识相关,这时候要用原生类型
// 合法
List.class;
String[].class;
int.class;
// 非法
List<String.class>
List<?>.class
  • instanceof
// Legitimate use of raw type - instanceof operator
// 泛型使用不了instanceof,但在下面的场合又是有用的,所以放心使用
if (o instanceof Set) { // Raw type
    Set<?> s = (Set<?>) o; // Wildcard type
    ...
}

27. 消除非受检警告

  • 应该尽可能尝试消除每一条警告。
  • 如果无法消除掉警告,并且可以证明是安全的,可以用@SuppressWarnings(“unchecked”)来消除。使用该注解的时候需要注意尽可能的聚焦小范围,而且需要添加注释,说明你证明了这个未受检警告是安全的。

以便确保自信你的程序不会在运行时出现异常,另一方面,当真正的警告出现的时候,可以快速排查到位置。需要注意,SuppressWarnings不能滥用,它应该保持尽可能小的范围,尽可能聚焦,同时在必要的地方也要适当地加入。

28. 列表优于数组

列表和数组的本质区别在于数组是协变的,而列表不是。所谓的协变,指的是如果Apple是Fruit的子类,则Apple[]是Fruit[]的子类,所以Fruit[]能够持有Apple[]的引用。而这一点在列表上是不可行的,例如List并不是List的父类,所以持有引用无法通过编译。
数组这种协变性会带来一些安全问题,例如:

// 编译通过,协变性
Object[] objectArray = new Long[1];
// 绕过编译,导致运行时异常
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

// 无法通过编译
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

从另一个角度而言,泛型只在编译时期强化类型信息,并在运行时丢弃,而数组则是会在运行时强化类型信息,这就容易引起一些运行时未预期的错误。
在Java中无法新建泛型数组,需要通过一些补偿手段进行新建,而使用起来也不是特别方便。所以在任何时候,使用泛型数组收到警告的时候,都应该优先考虑使用列表。这可能会使得代码变得冗长一点,但这是值得的,列表更加安全。

29. 优先考虑泛型

比起设计Object容器,让客户端自由存放,在客户端自己转型,使用泛型限制容器元素的类型要安全得多,而且,由于封装性,客户端不需要去管太多细节,所以尽可能低考虑使用泛型。
然而,安全使用泛型,尤其是泛型数组,并不是特别容易。虽然上一条才建议列表优先数组,但是有些时候我们只能使用泛型数组,例如ArrayList的底层就是数组实现。
安全地使用泛型数组,首先我们应该知道,为什么泛型数组不安全。

// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

泛型数组常见的初始化手段,但这会引出未检查警告,提醒你不安全性,这种不安全性主要来源有两个:

  1. 编译器无法知道泛型代表的具体类型是什么,因而也就无法却确保这种转换是安全的(安全的向下转型)
  2. 数组的协变性使得这种转型不太安全,因为这种转型,后期添加元素可能绕过编译器限制,引起运行时异常。例如父类[] = 子类A[],这时候添加子类B可能导致运行时异常。

因为编译器无法证明你的转换是安全的,那么就需要程序员自己证明,如果能够确保上述两种情况都是安全的,那么我们就可以用@SuppressWarnings(“unchecked”),并添加注解,表明确认安全。从例子的注释来看,因为stack的添加元素通过push方法,而push方法只会添加受检的E元素,所以它是安全的。
另一种实现方式是总是在泛型集合内部使用Object数组,这样可以避免编译类型和运行类型不一致的问题(编译的时候强化类型信息为E,可是实际类型却为Object)。

30. 泛型方法

声明类型参数

// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

泛型方法声明类型参数其实很简单,就是在方法修饰符和返回值之间添加泛型声明。

自限定的类型

在泛型中有一个奇怪的习惯用法:

class Demo <T extends Demo<T>>

这种类似递归的用法就让人头晕目眩。泛型其实影响的只有两个方面,一个是类中的参数类型,一个是类中方法的返回类型。
我们先来看一个简单更加常见的例子:

interface Comparable<T> {}
// 为啥这是递归的用法呢,我们来看使用
class A implements Comparable<A> {
}

从上述例子我们可以理解这种自限定用法的含义:A代表一种类,A实现的对比接口,必须接收A类参数。
回到以下情况,它限定了继承Demo的类,泛型参数必须是Demo本身的一种子类。

class Demo <T extends Demo<T>>

来看实际的作用就能理解:

class Demo <T extends Demo<T>>{}
// 继承Demo泛型参数可以为自身
class A extends Demo<A>{}
// 继承Demo泛型参数必须为Demo的子类
class B extends Demo<A> {}

class C {}
// 编译失败
class D extends Demo<C>{}

31. 利用有限制通配符提升API灵活性

其实就是讨论如何更好地使用有限制通配符,包括extends,super,?。当我们使用E的时候,实际上只能操作特定的某种类型,但有时候,我们期望多态能够在泛型生效,这种时候,就需要使用extends或super来增加读写(生产消费的灵活性)。另一种情况下,我们想要为E指明边界,使得擦除之后,使用泛型仍能调用边界类的方法。

PECS

producer-extends,consumer-super,这是关于extends和super使用的一个原则,因为二者其实选用的时候其实很容易弄错。
关于extends和super的原理可以参考 Java泛型
我们使用一个案例来分析PECS。
src作为生产者,生产出E类的元素e,所以它应该使用extends。你也可以看作“从src中需要读取泛型E的对象”。

// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
    	push(e);
}

dst作为消费者,dst会消费E泛型的对象,所以它必须使用super。你也可以看作“dst会写入E泛型的对象”。

// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
    	dst.add(pop());
}

假设上述src或dst,在方法中即是消费者又是生产者,那么有限制通配符对你来说就没有什么意义,因为你需要严格的类型匹配,这不是通配符可以得到的。

?通配符

尽管我们知道?在大多数情况下是可以直接替换成E的,但是,对于公共API来说?其实是更好的选择,因为它更加简单。

// Two possible declarations for the swap method
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

如果既要保证使用简单的?对外,而内部又必须使用E保证泛型对象可以写入,则可以使用一个helper方法,来兼得二者。

public static void swap(List<?> list, int i, int j) {
	swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
	list.set(i, list.set(j, list.get(i)));
}	

最佳实践

  • 如果使用得当,通配符对于类的用户是无形的。否则,如果用户必须考虑通配符类型,那么API的设计可能就是有问题的。
  • 通配符的使用需要技巧,但对于广泛使用的API来说,这种努力是值得的,这会让API变得更加灵活。
  • 记住comparable和comparator都是消费者。

32. 谨慎并用泛型和可变参数

Java不允许新建泛型数组,但是可变参数和泛型混用确实允许的,我们知道可变参数实际上上内部就是新建了数组,这样一来就出现了矛盾,这是一种技术露底。但是由于泛型可变参数非常有用,所以Java实际上是容忍了这一矛盾的存在。
以下是一个不安全使用的例子,关键问题在于它出现了堆污染——违背了原来的泛型参数限制类型,引用了其他变量或被其他类型引用。

// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
    Object[] objects = args;
    objects[0] = Integer.valueOf(1);
    return args;
}

安全条件:

  • 没有在可变参数数组中保存任何值
  • 没有对不信任的代码开放该数组

上述代码实际上会在编译中产生警告,如果我们确定泛型可变参数的使用是安全的,这时候可以加上@SafeVarargs,取消这种警告——保证真正的警告出现时,我们能定位并解决。

33. 优先考虑类型安全的泛化容器

有时候我们会想要灵活性——容器内存放多种类型的对象,举例而言,很多框架中都包含向context写入各种公共数据的功能。
以下这种用法就是类型的实现,非常常见,它叫做类型安全的泛化容器。

// Typesafe heterogeneous container pattern - implementation
class Favorites {
    // 特殊用法,?不代表不能写入
    private Map<Class<?>, Object> favorites = new HashMap<>();
    // Achieving runtime type safety with a dynamic cast
    public <T> void putFavorite(Class<T> type, T instance) {
        // 在这里增加类型转换可以保证获取的时候类型安全
        favorites.put(type, type.cast(instance));
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值