EffectiveJava(v3) - chapter4: Generics

Generics

自从Java5开始, Java引入了泛型. 在此之前, 每次从Collection中的读取一个对象都需要进行手动转换(cast), 如果错误的插入一个对象, 就会在运行时出现转换Error. 通过泛型, 告诉编译器该集合支持哪些类型对象, 编译器也会自动帮你转换对象, 如果你不小心插入一个错误的对象时就会在编译时就显示报错. 这样会让代码更加安全和简单. 这些优点的实现是需要付出一些代价的, 本章的关注就是如何最大化这些优点, 最小化产生的代价.

Introduce

EffectiveJava 第三版读书笔记,如果各位觉得翻译的不对或者内容有误,请及时联系我,敬请斧正。原文链接.

Item 26: Don't use raw types

一般一个类或者接口在声明时都会使用一个或者多个类型参数. 如List接口中的定义为List<E>, 其中E就是类型参数, 使用尖括号包起来. List<E>就被叫做通用类型类, 可以用它来定义定义一些具体的类, 如: List<String>, 这就是一个参数化类型类. 暗示该集合中所有对象类型为String. 而原始类型(raw type)为擦除类型参数之后的对象, 如List<E>的原始类型就为List. 而原始类型的存在主要是为了兼容之前版本的代码.

//Raw collection type - don't do this
//My stamp collection. Contains only Stamp instances
private final Collection stamps = ...;

//Erroneous insertion of coin into stamp collection
stamps.add(new Coin(...));	//Emits "unchecked call" warning

//Don't get error, until now 
//Raw iterator type - don't do this 
for (Iterator i = stamps.iterator(); i.hasNext();) {
	Stamp stamp = (Stamp) i.next();	//Throws ClassCast Exception 
		stamp.cancel();
}
复制代码

在泛型引入之前, 使用原始类型是标准使用方式, 然而在引入泛型之后, 原始类型虽然还兼容, 但是远远不是标准了. 原始类型最大的问题便是如上所示. 设计程序的时候, 最希望的就是如果程序出现了问题, 应该尽可能快的反馈, 最好就是在编译期. 在这个例子中, 代码在编译期是不会报错的, 直到这个对象被使用的时候才会出现. 这时候你需要搜寻代码查找问题, 这时候编译器并不会帮你, 因为它不会理解你的注释. 但是如果使用泛型的话, 一切都会变得很简单:

private final Collection<Stamp> stamps = ...; //Not use commet to indicate it.
复制代码

这样编译时, 在我们插入Coin时就会报错, 这样取出对象时, 也不需要显式进行转换(编译器自动进行了转换并保证了成功). 但是如果你使用原始类型就会丢失泛型的所带来的安全性和便利性. 之所以允许原始类型的存在, 就是为了兼容之前的版本, 让其可以无痛迁移到新版本的Java中.

如果想要在集合中插入任意的对象, 那么使用List<Object>则是非常合适的. 那么就有人问ListList<Object>有什么区别呢? List则是显式说明需要退出泛型系统, List<Object>则是告诉编译器该集合可以存放任何类型的对象. 可以显式传递List<String>List, 却不能传递给List<Object>. 因为List<String>是原始类型List的亚型(Subtype), 而不是List<Object>的参数化类型. 简单来说, 就是使用原始类型List就会放弃了类型安全的检查.

如:

public static void main(String[] args) {
	List<String> strings = new ArrayList<>();
	unsafeAdd(strings, Integer.valueOf(32));
	String s = strings.get(0);	//Has compiler-generated cast 
}

private static void unsafeAdd(List list, Object o) {
	list.add(o);
}
复制代码

这时候程序可以正常编译, 但是会在unsafeAdd中提示一个unchecked警告. 最终运行时, 在String s = strings.get(0)时出现ClassCastException, 因为编译器进行转换的时候, 转换失败了. 这就是典型的使用原始类而导致放弃类型检查出现的问题. 最简单的方法就是在unsafeAdd中使用List<Object>, 那样在编译时就会在调用unsafeAdd时报错, incompatible types.

有时候你不关注集合中的对象类型, 使用原始类型来存储任意的类. 如:

static int numElementsInCommon(Set s1, Set s2) {
	int result = 0;
	for (Object o : s1) 
		if (s2.contains(o))
			result++;
	return result;
}
复制代码

这个方法可以正确执行, 但是使用了原始类, 这里推荐使用无界通配符?. 如List<E>的无界通配符的标示为: List<?>.

static int numElementsInCommon(Set<?> s1, Set<?> s2) {
	int result = 0;
	for (Object o : s1) 
		if (s2.contains(o))
			result++;
	return result;
}
复制代码

ListList<?>有什么区别呢? 正如前面的unsafeAdd, List可以添加任意对象, 而List<?>不能, 甚至List<?>只能添加null. 记住, 你不能往Collection<?>中添加任何对象, 除了null.

当然原始类型也有一些地方需要用到, 如类定义的时候. List.class是合法的, 而List<String>.class是不合法的. 同理可得instanceof调用, 传递参数的时候, 如: if(xxx instanceof Set){}.

总而言之, 不要直接使用原始类型集合, 那只是用来兼容历史遗留代码使用. Set用以存储任何对象, Set<?>用于存储不清楚内部存储对象元素时. 推荐使用以替换原始类型. 原始类型只在获取类定义时有些作用.

Item 27: Eliminate unchecked warnings

当使用泛型的时候, 往往在编译的时候会提示各种类型的警告: unchecked cast warning, unchecked method invocation warning, unchecked parameterized vararg type warningunchecked conversion warnings. 当我们对泛型用的越多, 往往警告越少, 但是不要期待一开始写出的代码就完全没有警告, 更多的需要后续的改进和优化.

这些警告往往都是特别容易消除的. 如:

Set<Lark> exaltation = new HashSet();
复制代码

这时候会提示: warning: [unchecked] unchecked conversion. required: Set<Lark>, found: HashSet. 这时候只要简单的在HashSe后面添加尖括号即可:

Set<Lark> exaltation = new HashSet<>();
复制代码

有些警告是比较难消除的, 需要花费一些大工夫. 但是这是值得的, 尽量消除每一个unchecked warning是最终目标. 这样可以保证代码在运行期间肯定不会出现ClassCastException, 保证代码的健壮性.

对于一些警告, 你没有办法进行消除, 但是你可以证明弹出警告的代码是安全的, 只有在这种情况下, 你可以通过@SuppressWarnings("unchecked")注解压制这个警告. 如果你不能证明该代码是安全的, 那么你只能获得一个虚假的安全感, 在运行时往往还有可能出现ClassCastException. 并且相反, 如果你知道该代码(弹出警告)是安全的, 并且你不进行压制. 那么可能后续的代码(不安全的)弹出新警告就会混在其中, 容易让你忽视.

对于@SuppressWarnings("unchecked")注解, 可以用在任何地方, 从一个局部变量到整个类都是可以的. 但是请遵循一个原则, 尽可能限制小的使用范围. 通常的方法往往是使用在一些小方法内, 构造函数, 甚至局部变量内. 千万不要将注解使用在一整个类中, 这很有可能隐藏很多致命的警告.

如果将注解放在一个函数外时, 你发现函数很长, 往往不止一行. 这时候推荐你将注解使用在内部变量声明中. 如ArrayList中的toArray方法.

public <T> T[] toArray(T[] a) {
	if (a.length < size) 
		return (T[]) Arrays.copyOf(elements, size, a.getClass());
	System.arraycopy(elemtns, 0, a, 0, size);
	if (a.length > size)
		a[size] = null;
	return a;
}
复制代码

编译的时候, 会在return (T[]) Arrays.copyOf(elements, size, a.getClass());这行报[unchecked] unchecked cast ...警告. 这时候直接在方法外部声明注释@SuppressWarnings("unchecked")是可以的. 但是更好的方式是在内部使用:

public <T> T[] toArray(T[] a) {
	if (a.length < size)  {
		@SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
		return result;
	}
	System.arraycopy(elemtns, 0, a, 0, size);
	if (a.length > size)
		a[size] = null;
	return a;
}
复制代码

另外, 在使用@SuppressWarnings("unchecked")时, 添加一行注解, 解释一下为什么这是安全的. 这样便于后人理解代码. 如果你发现则很难写注释, 你可能最后会发现这并没有你想象的安全.

总而言之, 尽量消除每一个unchecked警告. 每一个unchecked警告说明代码中存在ClassCastException的风险. 如果实在无法消除, 且可以保证该代码是安全的, 合理的使用@SuppressWarnings("unchecked")进行注解, 但是请保证使用实在最小的范围内, 并为每一个注解添加注释, 解释一下为什么这是安全的.

Item 28: Prefer lists to arrays

数组和泛型有很大的区别, 主要体现在两个方面. 第一, 数组是协变的(covariant), 简单来说就是: 如果SubSuper的子类, 那么Sub[]数组也是Super[]数组的子类. 而对于泛型来说, 却是不变的(invariant). 对于两个不同的类,Type1Type2, List<Type1>既不是List<Type2>的子类, 也不是父类. 从这里看, 你可能会觉得泛型的功能比不上数组, 但是结果往往相反. 如

Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";	//Throws ArrayStoreException

//Won't compiler
List<Object> ol = new ArrayList<Long>();	//Incompatible types
ol.add("I don't fit in");
复制代码

两种方法都不能成功添加, 但是第一种方法是在运行时才报错, 而第二种是在编译时报错. 那当然第二种更好了.

第二, 数组是具体化的(reified), 即数组在运行时可以知道并且强制限制其内部的成员类型. 就如前面往Long数组中添加String类型对象, 报ArrayStoreException异常. 而泛型这是通过擦除实现的, 即内部成员类型的保证是在编译期确定的, 在运行期间, 由于擦除了类型信息, 无法获知和保证成员类型信息.

这两个巨大的区别导致, 泛型和数组往往不能很好的配合. 如创建泛型数组是不合法的, 类似List<E>[], List<String>[]等都是不合法的, 在编译期就会报错. 为什么泛型数组是不合法的呢? 因为这不是类型安全的, 如果允许泛型数组就可能在运行时出现转换问题, 导致ClassCastException. 如:

//Pretend the line is legal.
List<String> strings = new List<String>[1];
List<Integer> initList = List.of(43);
Object[] objects =  strings;		//Obtain objects from strings.
objects[0] = initList;				//This is legal for array.
String s = strings[0].get(0);		//ClassCastException.
复制代码

从上面可以知道, 非常容易就出现了异常. 其中objects[0] = initList;是合法的, 因为在编译器中通过擦除实现的, List<Integer>List<String>都是一样的.

E, List<E>, List<String>这种类型都是非具体化的类型. 也就是说这种类型在运行时拥有的信息比编译时要少很多. 因为擦除. 唯一具体化的参数类型是?, List<?>, 但是很少使用这种类型来创建数组.

禁止泛型数组的创建, 有时候是非常烦人的. 如你在一个泛型对象中是没有办法返回内部元素的数组对象如T[], 这时候推荐使用List<E>, 而不是数组E[].

public class Chooser<T> {
	private final T[] choiceArray;
	
	public Chooser(Collection<T> choices) {
		choiceArray = choices.toArray();	//Won't compile
	}
	
	...// The other is omitted.
}
复制代码

这时候, 这段代码是不会运行的, 编译出错. 也许你经验丰富, 添加一个转换语句:

choiceArray = (T[]) choices.toArray();
复制代码

这时候代码是不会编译出错的, 但是却会抛出一个警告: unchecked cast. 编译器不能保证在运行时,这行代码可以正确转换, 你必须自己进行证明, 并添加在注释中, 然后压制这个警告@SuppressWarnings("unchecked"). 但是更好的方法是使用List<E>.

public class Chooser<T> {
	private final List<T> choiceAList;
	
	public Chooser(Collection<T> choices) {
		choiceAList = new ArrayList<>(choices);	//Won't compile
	}
	
	...// The other is omitted.
}
复制代码

虽然这个版本, 可能在性能上会差一点, 但是却带来了程序的可读性和健壮性, 不用担心潜在的转换异常.

总而言之, 数组和泛型有很大的区别. 数组是协变的和具体化的. 泛型是不变的和擦除的. 作为结果, 数组提供运行时的类型安全检查而不是编译时的类型安全检查, 对于泛型却恰恰相反. 所以, 数组和泛型不能很好在一起工作. 当你发现数组和泛型使用时, 存在很大问题时, 第一个可选的方法可以考虑使用list替换数组.

Item 29: Favor generic types.

我们经常使用JDK提供的各种泛型, 我们自己写的话, 就有点复杂了, 但是这种努力和时间是值得的. 如:

//Obejct-based collection - a rpime candidate for generics 
public class Stack {
	private Object[] elements;
	private int size = 0;
	private static final int DEAFAULT_INITIAL_CAPACITY = 16;
	
	public Stack() {
		elements = new Object[DEAFAULT_INITIAL_CAPACITY];
	}
	
	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}
	
	public Object pop() {
		if (size == 0) 
			throw new EmptyStackException();
		Object result = elements[--size];
		elements[size] = null;
		return result;
	}
	
	public boolean isEmpty() {
		reutrn this.size == 0;
	}
	
	private void ensureCapacity() {
		if (elements.length == size) 
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}
复制代码

这个类一开始设计的时候就应该考虑泛型化, 但是没有. 现在可以对这个类进行泛型化处理, 并且不影响之前的使用. 首先在类的声明中添加一个或者多个参数类型, 这里只需要添加一个参数化类型, 这里声明为E. 然后将所有的Object替换为合适的参数化类型.

public class Stack<E> {
	private E[] elements;
	private int size = 0;
	private static final int DEAFAULT_INITIAL_CAPACITY = 16;
	
	public Stack() {
		elements = new E[DEAFAULT_INITIAL_CAPACITY];
	}
	
	public void push(E e) {
		ensureCapacity();
		elements[size++] = e;
	}
	
	public E pop() {
		if (size == 0) 
			throw new EmptyStackException();
		E result = elements[--size];
		elements[size] = null;
		return result;
	}
	
	public boolean isEmpty() {
		reutrn this.size == 0;
	}
	
	private void ensureCapacity() {
		if (elements.length == size) 
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}
复制代码

编译代码, 对错误的地方进行修复. 首先出现的问题是: elements = new E[DEAFAULT_INITIAL_CAPACITY];, 然后程序编译出错. 这是一个最常见的问题, 泛型数组的问题. 这里首先声明为Object, 采用类型转换(T[]), 这时候提示警告: Unchecked cast. 编译器无法保证在运行时这段代码可以正确进行转换. 这时候, 我们把理由写上, 并压制警告:

// The elemets 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("uncheckned")
public Stack() {
	elements = (E) new Object[DEAFAULT_INITIAL_CAPACITY];
}
复制代码

当然你也可以声明elements为Object[], 然后在弹出的时候进行类型转换, 同样进行压制注释等等. 但是前面这种的可读性会更好, 并且减少了cast的调用次数. 所以前面这种使用的也更加广泛. 第一种也是存在问题的, 会导致堆污染(Heap pollute, Item 32): 运行时类型和编译时类型不匹配. 如果这个污染严重的话, 可以考虑第二种方法. 这里并不是很严重, 采用第一种.

这时你可能觉得这不是违反了Item28说的, 优先使用list来替代array吗. 是的, 这个是违反了. 那个是优先并不是强制要求. 首先对于list, JDK并没有本地支持, 对于一些泛型类ArrayList必须使用数组来说实现, 另外对于一些追求性能的类, 如HashMap也是使用数组来实现, 来保证性能.

另外, 这里的参数类型是没有进限制的, 即你可以传递和设置任意的参数, 如: Stack<Object>, Stack<int[]>, Stack<List<String>>等等, 注意这里由于JDK本身的限制, 不能直接使用原始类型, 不过可以使用封装类型进行替换. 你这里可以限制传递的参数类型. 如java.utilconcurrent.DelayQueue:

class DelayQueue<E extends Delayed> implements BlockingQUeue<E>
复制代码

这里的参数类型E限制为java.util.concurrent.Dealyed的子类, 这样在DelayQueue中就可以显示使用Dealyed的方法, 而不需要进行显示转换. 这就是常称的有界通配符. 注意的是, 虽然说是E extends Delayed, 但是传递Dealyed也是合理的.

总而言之, 泛型可以更加安全和方便使用, 如果你的类没有泛型化, 那么可以好好考虑发现化.

Item 30: Favor generic methods.

正如类可以被泛型化, 方法同样可以泛型化. 如: Colletions内所有的工具方法都是泛型化的. 写泛型方法, 也非常类似泛型类, 比如:

//Use raw types - unaccpetable
public static Set union(Set s1, Set s2) {
	Set result = new HashSet(s1);
	result.addAll(s2);
	return result;
}
复制代码

这个方法编译的时候, 提示两个警告. Set result = new HashSet(s1); : unchecked call HashSet as raw type. result.addAll(s2) : unchecked call to addAll as raw type Set. 这暗示调用的过程是使用raw type的. 要消除则警告就要对这个方法使用的三个集合进行泛型化. 添加也简单, 在方法描述符和返回值之间添加泛型参数, 使用<>包起来, 修改方法添加对应的泛型参数.

//Generics method 
public static <T> Set<T> union(Set<T> s1, Set<T> s2) {
	Set<T> result = new HashSet<>(s1);
	result.addAll(s2);
	return result;
}
复制代码

对于简单的方法, 这样做就可以了. 消除了所有的警告, 也方便类调用.

Set<String> guys = Set.of("Amy", "Jake", "Tom");
Set<String> stooges = Set.of("Alika", "Tikala", "Matten");
Set<String> all = union(guys, stooges);
复制代码

这里有一个小小的限制, 那就是三个Set的参数化类型必须完全一样. 可以通过有界通配符来更加灵活的完成方法的泛型化.

有时, 你需要创建一个不变的对象, 但是要应用在很多不同类型的对象上. 因为泛型内部实现使用的是擦除, 你可以创建一个通用的全局的对象, 然后通过静态方法为不同的泛型返回不同的泛型的该对象. 这种模式就是常用的泛型单例工厂模式. 如Collections.reverseOrder.

@SuppressWarnings("unchecked")
public static <T> Comparator<T> reverseOrder() {
	return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
复制代码

假设你想要写一个identity方法, 简单的返回自身. 这时候只需要定义一个通用的对象, 然后按照不同的泛型参数类型进行返回.

private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

public static <T> UnaryOperator<T> identityFunction() {
	return (UnaryOperator<T>) IDENTITY_FN;
} 
复制代码

注意这里会抛出一个警告unchecked cast, 编译器认为UnaryOperator<Object>不一定是UnaryOperator<T>, 有可能出现转换失败情况. 但是这个函数是特殊的, 这个函数没有修改参数, 只是单纯的返回自身. 可以知道对于任意参数, 这是安全的. 可以压制这个警告, 保证所有的调用都是干净的.

这里有一个特殊的情况, 那就是递归类型绑定. 就是类型参数中夹杂着别的类型参数. 最经典的就是Comparable接口.

public interface Comparable<T> {
	int compareTo(T o);
}
复制代码

这个类型参数T一般是自身, 因为对象比较时一般只允许和自身进行比较. 如String就实现Comparable<String>, Integer就实现Comparable<Integer>. 对于实现这个接口的对象, 说明这些对象是有顺序的, 可以进行比较的. 那么放在集合中就可以求最大值, 最小值, 排序等操作. 如:

public static <E extends Comparable<E>> E max(Collection<E> c) {
	if (c.isEmpty()) 
		throw new IllegalArgumentException("Empty collection");
	E result = null;
	for (E e : c) 
		if (result == null || e.compareTo(result) > 0)
			result = Objects.requireNonNull(e);
	return result;
}
复制代码

上面就是一个典型的求最大值的工具方法. 声明中的<E extends Comparable<E>>, 就是递归类型绑定. 幸运的是, 这种使用比较少见.

总而言之, 泛型方法类似泛型类, 更加安全和便于使用, 使用时就不需要显式对参数和返回值进行转换. 当你写的方法, 发现经常需要进行转换时, 考虑将它泛型化.

Item 31: Use bounded wildcards to increase API flexibility.

正如前面Item28说的, 泛型是不变的和擦除的, List即不是List的子类, 也不是其的父类. 虽然这有点奇怪, 但是不难理解: 一个容器装了父亲, 一个容器装了儿子, 我们不能光凭这个就说明前面是后者的父类, 因为这个不仅仅只有父亲, 还有很多别的东西, 如容器, 数量等等. 但是这个特性有时候还是会带来一些不便. 如我们设计的一个简单的Stack类:

public class Stack<E> {
	public Stack();
	public void push(E e);
	public E pop();
	public boolean isEmpty();
}
复制代码

这是基本的方法, 后面我们按照需求添加类一个新的接口pushAll(传递一个集合, 将集合中的所有对象添加到原来的对象中):

public void pushAll(Iterable<E> src) {
	for (E e : src) 
		push(e);
}
复制代码

这个方法成功编译了, 并且运行良好. 但是这还是不够的. 假设我们有一个Stack<Number>的数据, 突然产生了一组List<Integer>的数据, 想要将这些数据通过这个方法放入内部. 按照push方法是可以成功放入的, 因为IntegerNumber的子类, 可以正常放入. 但是调用时, 却爆出了error: Iterable<Integer> can't be convert to Iterable<Number>. 很明显这就是因为泛型的不变性导致的.

为了解决这个问题, Java提供了一个很好的工具: 有界通配符. 为了在pushAll中兼容E的子类(Number的子类, Integer), 在该方法的声明中使用<? extends E>:

public void pushAll(Iterable<? extends E> src) {
	for (E e : src) 
		push(e);
}
复制代码

这样就可以成功兼容所有E的子类, Stack可以通过该方法, 放入任何包含子类的集合了. 这时候我们想对应该方法, 书写一个popAll方法, 传递一个集合, 然后弹出所有该栈中的对象, 放入集合中.

public void popAll(Collection<E> dst) {
	while(!isEmpty()) 
		dst.add(pop());
}
复制代码

同样可以编译成功. 但是也同样功能不够齐全, 如果我们想将Stack<Integer>的数据放入List<Number>中, 这个代码同理也会报错. 按照逻辑来说也不应该报错的. 这时候, 可以使用<? super E>:

public void popAll(Collection<? super E> dst) {
	while(!isEmpty()) 
		dst.add(pop());
}
复制代码

这样也就解决了兼容性的问题. 到这里我们可以得出一个结论: 为了最大化灵活性, 为消费者函数和生产者函数使用有界通配符, 是一个很好的选择. 如果函数的输入参数, 既是消费者, 也是生产者, 那么就不要使用通配符, 还是使用确定的参数类型. 这里有一个简单的口诀来记录: 消费者函数使用super, 生产者函数使用extends. 如这里的pushAll是典型的生产者, 生成E实例给Stack, 那就使用extends, popAll是典型的消费者, 消耗Stack内的对象, 就使用super.

带着这样的窍门, 我们重温一下之前的方法和函数. Item28中的:

public Chooser(Collection<T> choices);
复制代码

所有的构造函数都是生产者, 这里使用extends.

public Chooser(Collection<? extends T> choices);
复制代码

Item30中的:

public static <T> Set<T> union(Set<T> s1, Set<T> s2);
复制代码

可以很清楚的知道, 传递过来的两个参数都是生产者, 最后生产一个集合.

public static <T> Set<T> union(Set<? extends T> s1, Set<? extends T> s2);

Set<Integer> integers  = Set.of(1,2,3);
Set<Double> doubles = Set.of(1.2, 3.2, 1.3);
Set<Number> numbers = union(integers, doubles);
复制代码

注意这里不要在返回值里使用通配符?, 因为这样会强制用户在使用时添加通配符, 增加了用户的复杂度. 真正良好的通配符应该是无感的, 用户是没有感觉的, 但是却良好地完成工作: 接受该接受的对象, 拒绝该拒绝的对象. 如果你的通配符需要用户在使用时需要顾虑的话, 那你的API可能就不太对.

这里有一点需要注意, 那就是在Java8之前, 编译器的推理功能还没这么强大, 如果需要使用上述的语句时, 需要手动告诉编译器. 不然编译器会不识别.

Set<Number> numbers = Uunion.<Number>union(integers, doubles);
复制代码

Item30中的:

public static <E extends Comparable<E>> E max(Collection<E> c);


//revised 
public static <E extends Comparable<? super E>> E max(Collection<? extends E> c) {
复制代码

这里的Collection<E> c很明显是生产者(原材料), 使用extends, 而最终的结果E为消费者, 并且E必须是可比较的, 使用super, 这里的含义是调用者(E)可以不实现Comparable接口, 实际使用的(?)可以是父类, 父类实现了Comparable接口. 注意, 所有的Comparable<T>Comparator<T>都是消费者, 推荐优先使用Comparable<? super T>Comparator<? super T>.

这里花了很大代价来实现这个功能, 这样做有效果吗? 是的, 是有的.

List<SchduledFuture<?>> schuduledFutures = ...// invoke max 
复制代码

这个在之前的版本是不可以的, 但是在修订的版本是可以的. 因为SchduledFuture没有实现Comparable接口, 所以第一个版本拒绝编译. 而修订版本, 发现该接口的父接口Delayed, 实现了Comparable接口, 允许调用.

通配符还有一点需要讨论的是. 如果我们需要实现一个静态工具方法, 将一个集合中的两个对象交换位置. 有两种实现方式:

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
复制代码

你会选哪种, 那肯定是第二种呀. 第二种简单明了. 你可以传递任何类型的通配符进行匹配. 这里有一个规则, 如果一个泛型参数, 只在方法中出现了一次. 那么推荐使用通配符?进行替代. 这里你可能实现的版本为:

public static void swap(List<?> list, int i, int j) {
	list.set(i, list.set(j, list.get(i));
}
复制代码

但是可惜的是, 程序并不能编译通过. error: incompatible type, object can't be convert to CAP#1. 为什么会这样呢? 因为List<?>为无界通配符, 你不能放入任何对象, 除了null. 因为编译器不能理解?, 会自动去猜对应的值, 然后赋予一个自认为的类型CAP#1, 这明显不能匹配任何对象. 怎么解决这个问题呢? 通过辅助方法.

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));
}
复制代码

这样就可以完美解决了, 通过一个媒介方法, 帮助编译器明白传递的参数类型. 你会发现这个辅助方法和之前的第一个方法, 完全一样.... 虽然用户使用方便了, 但是内部的复杂由后台承受了.

总而言之, 为你的方法添加泛型, 这会使得方法更加灵活. 如果你想让你的API被广泛使用, 合理的使用通配符. 记住PECS原则(Produce extends, Consumer super). 所有的ComparableComparator都是消费者.

Item 32: Combine generics and varags judiciously.

可变参数和泛型都是在Java5加入JDK的. 但是可变参数和泛型却不能很好的配合: 可变的参数的实现, 内部通过编译器传递一个数组来存储这些对象. 而数组是具体化的, 泛型却是相反的, 在运行期是擦除了信息的. 所以当我们声明泛型的可变参数时, 编译器会提示一个警告. 而当我们在方法内部调用该泛型参数时,也会提示警告. 警告类似: unchecked possible heap pollution .... Heap pollution就是当参数化类型引用指向的对象不是该类型时, 就会产生. 这个会导致编译器自动产生的cast有可能失败, 违背了泛型的原则.

//Mixing generics and varags can violate type safety!
static void dangerous(List<String>... stringLists) {
	List<Integer> intList = List.of(42);
	Object[] objects = stringLists;
	object[0] = intList;					//Heap pollution 
	String s = stringLists[0].get(0);		//ClassCastException
} 
复制代码

这么这个例子就可以简单说明泛型的可变参数时不安全的. 其中最后一行代码爆出的异常, 就是由编译器自动生成的cast方法调用出错. 这说明了一个很重要的问题: 往泛型参数的可变数组内存储对象(或者修改)是非常不安全的.

也许你会问, 为什么声明泛型的可变参数合理, 声明泛型数组却不合理呢? 两者本质都是数组呀. 是的, 这是前后矛盾的. 主要是JDK的设计者发现泛型的可变参数在实际使用时, 非常方便, 提供了很好的辅助作用, 也就默认这个的存在. 并且在JDK中, 如Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements)等泛型可变参数都是类型安全的, 并不像前面的这个例子这么危险.

在Java7之前, 人们使用泛型的可变参数时是非常难受的, 因为每次调用这类方法编译器都会抛出警告, 你为了压制这些警告, 只能在调用这类方法的方法外部添加SuppressWarnings("unchecked")来压制. 这是非常乏味且影响代码阅读的. 在Java7之后呢, 引入了一个新的注解: @SafeVarargs, 含义就是告诉编译器这个泛型的可变参数时类型安全的, 不会出现问题的. 编译器也就不会抛出警告了. 这样其它方法调用的时候, 也就不会得到警告了.

对于@SafeVarargs, 这是和编译器的一个约定. 但是这个约定需要你自己来完成: 在完全确定泛型可变参数方法是类型安全之后再添加该注解. 那怎么确保该方法(包含泛型可变参数)时类型安全的呢? 这里有两条准则: 对于泛型的可变参数数组不要进行任何的修改, 不要让泛型的可变参数数组的引用逃逸出方法外部, 即保证只能在方法内部使用. 如果保证满足这两个条件的话, 就可以说这个方法是类型安全的. 如这里举一个例子说明:

//UNSAFE - generic parameter array reference escaped out the method 
static <T> T[] toArray(T... args) {
	return args;
} 
复制代码

这个方法看起来就是一个简单的工具方法, 没什么问题, 但是却是非常危险的, 它会将Heap pollution传播到方法调用者. 假设基于这个方法实现一个工具方法:

static <T> T[] pickTwo(T a, T b, T c) {
	switch (ThreadLocalRandom.current().nextInt(3)) {
		case 0: return toArray(a, b);
		case 1: return toArray(b, c);
		case 2: return toArray(a, c);
	}
	throw new AssertionError();	//Can't get here
}

public static void main(String[] args) {
	String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
复制代码

这一切代码都可以正常编译, 没有任何问题. 但是我们运行时, 却会在pickTwo调用时爆出ClassCastException. 为什么会这样呢? 因为我们运行时调用pickTwo时, 编译器并不能理解T是什么, 于是调用toArray方法时就创建了Object[]数组来进行存储和返回. 而我们实际使用的却是String[], Object[]并不是String[]的父类或子类, 无法成功进行转换(cast), 所以爆出这个异常. 这也就是toArrayHeap pollute传播到这里导致的.

这个例子说明了第二点: 让别的方法可以访问的泛型可变参数数组是非常危险的. 这里有两个例外: 除非别的方法是@SafeVarargs类型的方法, 或者别的方法是固定参数个数, 并且只是单纯对数组内的元素进行值的计算. 上面的pickTwo方法虽然是固定参数的, 但是却不是对数组进行简单计算, 而是直接传播出去了. 这里举一个简单的安全使用的例子:

@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
	List<T> result = new ArrayList<>();
	for (List<? extends T> list : lists) 
		result.addAll(list);
	return result;
} 
复制代码

这里再次重申一下两个准则:

  • ** 对于泛型的可变参数数组不要进行任何的修改 **
  • ** 不然让泛型的可变参数数组被不安全的代码接触 **

当然这里还有一个折中的方法, 正如Item28所说的, 使用List来替代数组:

static <T> List<T> flatten(List<List<? extends T>> lists) {
	List<T> result = new ArrayList<>();
	for (List<? extends T> list : lists) 
		result.addAll(list);
	return result;
} 
复制代码

这样就不用担心类型安全的问题了, 唯一的缺点就是相比前面的代码, 性能会差一点.

总而言之, 可变参数和泛型不能很好的搭配使用, 因为可变参数的本质是数组, 而数组和泛型有很多冲突. 但是这依然是合理的, 如果你确定你的泛型可变参数方法是类型安全的, 添加@SafeVarargs注释, 来让这个方法更加方便使用.

Item 33: Consider typesafe heterogeneous containers.

在我们使用泛型时, 一般就是通过一些集合, 如Set<E>, Map<K,V>, 或者一些单元素的容器, 如ThreadLocal<T>,AtomicReference<T>等等. 一般来说泛型参数的数量都是固定的, 如Set<E>只有一个E代表Set中的元素类型, Map<K,V>只有两个K,V代表Map中的键和值两个对象. 一般正常使用是可以满足的. 但是如果你想获得更大的灵活性: 自定义多少个泛型参数.

在Java中如果想要实现自定义多少个泛型参数这种做法是可以实现的. 原理是: 前面的例子都是对容器进行泛型化(如对Set进行泛型化为Set<E>), 而我们可以对key进行泛型化, key就如同Map中的key, 用来放置和取出对象. 然后将这个泛型化的key放入容器中来进行存储, 最后利用泛型来保证key对应的对象是正确的类型.

如我们设计一个类Favorites, 来存储任意类型我们喜欢的实例. 这时候, 我们的key就可以设置为Class, 因为Class<T>是泛型, 如Integer.class就为Class<integer>, String.class就为Class<String>. 并且Class的文字信息可以在方法中进行传递(无论编译期还是运行期), 这就被称作类型秘钥(type token).

Favorites就如同一个简单的Map, 不过其中的key是泛型的, 这是API方法:

//Typesafe heterogeneous container pattern - API
public class Favorites {
	public <T> void putFavorite(Class<T> type, T instance);
	public <T> T getFavorite(Class<T> type);
}
复制代码

这里是简单的测试函数:

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 %s %s%n", favoriteString, favoriteInteger, favoriteClass);
}
复制代码

注意这里使用%n来保证平台兼容性的换行. 这里的Favorites类是类型安全的, 当你想要获取什么类型时, 自动获取什么类型. 并且是异构的, 不像传统的Map, Favorites内的key是不同类型的, 泛型化的.

//Typesafe heterogeneous container pattern - implementation
public class Favorites {
	private Map<Class<?>, Object> favorites = new HashMap<>();

	public <T> void putFavorite(Class<T> type, T instance) {
		favorites.put(Object.requireNonNull(type.class), instance);
	}
	
	public <T> T getFavorite(Class<T> type) {
		return type.cast(favorites.get(type));
	}
}
复制代码

这里简单说明一些, 首先从代码中可以知道, 所有的对象都是存储在HashMap中. 有人可能会认为Map中使用无界通配符, 怎么还可以往里面放入元素(一般来说含通配符的对象只能放入null), 但是这里需要注意的是, 这里的通配符是嵌套的, 并不是指Map是无界的, 而是说内部的key是无界的. 意思就是说, key可以为任何类型的Class. 这就是异构(heterogeneous)的来源.

第二个需要说明的是, 这里的map存储的是ClassObject, 也就是说map是不会保证classobject的类型对应的. map不会帮你确认存入的object是不是就是对应class的实例. 但是实际上, 这是可以得到保证的, 只是Java type system并没有明说而已, 下面详细说明:

putFavorite简单往map中进行放入对象, 使用泛型来保证类型一致. getFavorite方法通过Classcast进行转换(cast中如果不是对应类型的就抛出ClassCastException. 这样可以保证, 只要客户端编译通过了(即编译时泛型校验成功了, 可以正确放入), 那么第二个方法取出时就肯定不会出错, 也就是类型安全的(typesafe).

这里有两点是需要注意, 第一, 就是如果客户端使用原始类型Map, 不使用Map<Class<?>, Object>的话, 很有可能编译通过, 但是运行时出现ClassCastException. 这时候为了防止运行时出现这种情况, 可以在putFavorite方法中添加校验:

public <T> void putFavorite(Class<T> type, T instance) {
	favorites.put(Object.requireNonNull(type.class), type.cast(instance));
}
复制代码

这种做法在Collections中被广泛使用, 如CheckedList, CheckedMap等, 保证运行时的类型安全.

第二就是, 该类不支持泛型. 因为泛型通过擦除, 所有的泛型类的Class都是一样的.

另外这里使用的是无界通配符, 如果想要添加限制也是可以的, 可以使用有界通配符. 如:

public <T extends Annotation> T getAnnotation(Class<T> annotationType);
复制代码

这里有一个特殊情况, 如果你在一个无界通配符中想要调用一个有界通配符的方法. 就比如上面的getAnnotation方法, 直接使用<? exnteds Annotation>进行修改也是可以, 但是这个转换是unchecked的. 这里推荐使用Class.asSubclass方法.

static Annotation getAnnotation(AnnotationElement element, String annotationTypeName) {
	Class<?> annotationType = null;
	try  = 
		annotationType = class.forName(annotationTypeName);
	} catch(Exception ex) {
		throw new IllegalArgumentException(ex);
	}
	return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
复制代码

这样就没有任何警告和错误了.

总而言之, 普通的泛型限制了泛型参数的个数. 可以通过类型安全的异构容器来实现: 不对容器进行泛型化, 而是对于key进行泛型化. 常用的key为Class对象, 当然你可以自定义key对象.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值