泛型
1. 请不要使用原生态类型
Java 5 引入了 泛型 (generic),编译器会自动根据泛型信息做类型转换。
带有参数化类型的类或接口称为泛型类或泛型类型,它们有对应的原生类型。
- 泛型类型:
List<E>
(E 的列表) - 原生类型:
List
除了一种情况下,必须使用原生类型:类文字(class literal
)中使用原生类型,如 List.class
其余任何情况都不要使用原生类型,编译器没有强制禁止原生类型仅仅是为了兼容之前的代码。
2. 消除非受检的警告
使用泛型编程时会遇到很多编译器警告,比如:
//这里会有一个非受检警告
Set<String> set = new HashSet(); ❌
//消除非受检警告
Set<String> set = new HashSet<>(); ✔
要尽可能的消除每一个受检警告,否则都可能在运行时出现 ClassCastException
如果无法消除警告,又可以证明该警告是安全的,那么可以使用 @SuppressWarnings("unchecked")
注解来消除警告。
3. 列表优于数组
- 数组是协变(covariant)的,也就是
Child[]
也是Parent[]
的子类型 - 列表是可变的,
List<Child>
与List<Parent>
是两个独立类型
你可能认为列表是有缺陷的,实际上数组才是有缺陷的:
//这段代码在编译器是合法的
Object[] objectArray = new Long[1];
objectArray[0] = "haha";
//这段代码不合法
List<Object> list = new ArrayList<Long>();
list.add("haha");
实际上上面两种版本都不能将 String
放进 Long
的容器中,但数组在运行期才发现错误;而列表在编译器就发现了错误。
案例: 利用泛型列表实现一个游戏用的色子 (传入一个集合,返回集合中的随机一个元素)
class Chooser <T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
4. 优先考虑泛型
在设计新类的时候,优先考虑泛型,它们需要在代码中进行类型转换就可以使用。
5. 优先考虑泛型方法
泛型方法也可以从泛型中获益,尤其是静态工具方法, Collections
中的所有算法方法都泛型化了。
6. 利用有限制通配符来提升 API 的灵活性
- 以
Stack
类为例子
public class Stack<E> {
public void push(E e);
public E pop();
}
- 假设我们要增加一个方法,让它按顺序将一系列元素全部放到堆栈中去
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
- 上面的代码有局限性,
Stack<Number>
应该支持pushAll()
类型为Iterable<Integer>
的src
// 使用 <? extends E> 有限制通配符来实现这种灵活性
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或另一个进行声明
//类型参数方式声明
public static <E> void swap(List<E> list,int i, int j){}
//通配符方式声明
public static void swap(List<?> list,int i, int j){}
对于公共 API 来说,通配符方式声明更简单好用;一般来说,如果类型参数只在方法声明中出现一次,就可以使用通配符取代它。
不过对于通配符方式有一个问题,下面这个简单的实现不能编译
public static void swap(List<?> list,int i, int j){
list.set(i,list.set(j,list.get(i)));
}
对于这个问题可以编写一个泛型辅助方法来解决,但暴露出去的 API 仍然保留了通配符方式,对客户端更加友好。
public static void swap(List<?> list,int i, int j) {
swapHelper(list,i,j);
}
private static <E> void swapHelper(List<E> list,int i,int j) {
list.set(i,list.set(j,list.get(i)));
}
7. 谨慎并用泛型和可变参数
当调用一个可变参数方法时,会创建一个数组来存放可变参数;这个数组应该是一个实现细节,它是可见的。
因此,当可变参数带有泛型时,就产生了泛型数组的问题。
可为什么显示创建泛型数组是非法的,但用泛型可变参数是合法的呢?
因为带有泛型可变参数的方法在实践中作用很大,Java 设计者选择容忍这一矛盾的存在:
// Arrays.asList()
@SafeVarargs
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
// Collections.addAll()
@SafeVarargs
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
boolean result = false;
for (T element : elements)
result |= c.add(element);
return result;
}
Java 7 引入了 @SafeVarags
注解,方法设计者做出承诺,这个泛型可变参数方法是安全的,忽略警告。
不要随意使用 @SafeVarags
注解,除非它是真正安全的,需要满足两大条件:
- 它没有在可变参数数组中保存任何值
- 它没有对不被信任的代码开放该数组 (或者其克隆程序)
8. 优先考虑类型安全的异构容器
泛型最常用于集合,如 Set<E>
和 Map<K,V>
,以及单个元素的容器,如 ThreadLocal<T>
和 AtomicReference<T>
。
在这些容器中它们都限制了每种容器都只能存放固定类型的元素,但是有时候你可能需要更多的灵活性,比如数据库的行可以存储任意数量的列。
幸运的是,有一种方法可以很容易地做到这一点,将键(key
)进行参数化,而不是将容器参数化:
案例: Favorites
类,它允许客户端保存和获取一个任意类型的最喜欢的实例。
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
类存在运行时安全问题,那就是值是 Object
类型,恶意的客户端可以为 Integer
类型的 key
设置一个 String
类型的值; 就像客户端可以利用 HashSet
原生态类型,将 String
放进一个 HashSet<Integer>
中。
对此,可以付出一点点代价改进,就可以获得运行时的类型安全:
public <T> void putFavorite(Class<T> type,T instance) {
//检验 instance 是否真的是 type 类型的实例
favorites.put(Objects.requireNonNull(type),type.cast(instance));
}
注解 API 广泛应用了这种模式,例如 java.lang.reflect.AnnotatedElement
接口
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
- 硬编码获取注解实例
elment.getAnnotation(MyAnnotation.class);
- 动态获取注解实例
static Annotation getAnnotation(AnnotatedElement element,
String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
}catch (Exception e) {
throw new IllegalArgumentException(e);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
方法中 asSubClass(Anotation.class)
的用途
该方法确保加载的类是一个注解类型,如果传入的是 java.lang.String
, 就会抛出 ClassCastException
,这一步是安全校验的关键。