第5章:泛型

第26条:请不要使用原生态类型

26.1 泛型介绍

  1. 如果类/接口的声明中,具有一个或多个类型参数(type parameter),就叫做泛型(generic:通用)类或泛型(通用)接口。泛型类、泛型接口都是泛型(通用)类型(generic type)
//例如List接口声明时,定义了一个表示类型的参数E,这个接口就是一个泛型接口
//这个接口全称是List<E>,读作E的List,但人们通常简称其为List
public interface List<E> extends Collection<E> {
	...
}
  1. 泛型(通用)类型定义了一组参数化的类型(给定了具体参数的类型),每个参数化的类型都是由类/接口名+<+实际类型参数+>组成,比如List就是这组参数化的类型中的一个,表示它定义的变量,是String的列表。List这里的E叫做形式类型参数
  2. 原生类型(raw type):不带任何实际类型参数的泛型(通用)类型,List的原生类型为List,原生类型就像从类型声明中删除了所有的泛型信息一样,它们存在主要为了与泛型出现前的代码兼容
  3. 由于兼容性需要,导致可以将参数化类型转为原生类型,反之也可以

26.2 原生类型缺点

  1. 不利于查找错误原因
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Stamp {
	private static final Collection stamps = new ArrayList();

	public static void main(String[] args) {
		stamps.add(new Coin());
		for (Iterator i = stamps.iterator(); i.hasNext();) {
			//1. 取出时,会报错,Coin cannot be cast to Stamp,但放入时只有提醒
			//2. 出错之后应该尽快发现,最好是编译时就发现
			//3. 本例中直到运行时才发现,那么如果取出的地方,与放入不在一个类中,且离放入的地方非常远,那么你就很难定位放入错误元素的位置
			Stamp stamp = (Stamp) i.next();
		}

	}
}

class Coin {

}
  1. 修正
//利用参数化的类型,声明变量,错误的插入,编译器会报错
private static final Collection<Stamp> stamps = new ArrayList();
...
//编译报错,无法通过编译,因为编译器发现stamps使用泛型声明的,因此会插入隐式的转换,当想把Coin强转为Stamp,发现他们没父子关系,因此报错了
stamps.add(new Coin());
  1. 虽然原生态类型合法,但永远不要使用它,它只是为了提供兼容性
26.2.1 原生类型List与参数化的类型List<Object>的区别
  1. List是为了兼容,而List<Object>是为了告诉编译器这个集合可以保存任意类型的对象
  2. List<String>是List的子类,而不是List<Object>的子类
  3. 使用List会失去类型安全性,而List<Object>不会
import java.util.ArrayList;
import java.util.List;

public class TestHan {

	public static void main(String[] args) {
		List<String> strings = new ArrayList<>();
		unsafeAdd(strings, Integer.valueOf(42));
		//由于unsafeAdd方法中,第一个参数,形参使用了原生类型定义变量,导致泛型信息被擦除,因此Integer可以插入元素
		//而strings.get时,系统发现strings上有泛型信息,自动想为其转换为String类型,Integer转String,报错
		String s = strings.get(0);

	}

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

//调整方法签名
private static void unsafeAdd(List<Object> list, Object o) {
	list.add(o);
}
//方法调用时,编译器不再提示warning,而是直接报错,无法通过
unsafeAdd(strings, Integer.valueOf(42));
26.2.2 原生类型与无限制的通配符类型List<?>的区别
  1. 原生类型不安全,由于可以将任何元素放入,很容易破坏该集合的类型的约束条件,和上面的例子相同
  2. 通配符类型安全,因为无法将null之外任何元素放入,也无法猜测获得哪种对象,如果无法接受这些限制,可以使用泛型方法,或有限制的通配符类型

26.3 使用原生类型的场景

  1. 必须在表示类的字面意思中使用原生态类型
//合法
List.class;
String[].class;
int.class;
//不合法
List<String>.class;
List<?>.class;
  1. 使用instanceof操作符时
//不允许使用o instanceof Set<String>
//但可以使用 o instanceof Set<?>
//一旦想使用o,为了安全,就应该将其转换为参数化的类型,下面写法为instanceof后跟泛型类的一个最佳实践
if (o instanceof Set) { // Raw type
	Set<?> s = (Set<?>) o; // Wildcard type
	...
}

26.4 最佳实践

  1. 使用原生态类型会在运行时导致异常,不安全,不要使用
  2. 原生态类型只是为了与引入泛型前的遗留代码进行兼容和互用而提供的

第27条:消除非受检的警告

  1. 常见的非受检的警告
    1. unchecked cast warnings
    2. unchecked method invocation warnings
    3. unchecked parameterized vararg type warnings
    4. unchecked conversion warnings
  2. java7菱形语法消除警告
//没有<>会有警告
Set<Lark> exaltation = new HashSet<>();
  1. unchecked warnings:这里应该是表示由于没检查类型导致的警告,消除所有的unchecked warnings,就可以确保代码是类型安全的,也就意味着不会在运行时出现ClassCastException
  2. 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,才可以用注释@SuppressWarnings(“unchecked”)来禁止这条警告
  3. SuppressWarnings注解可以用在任何粒度的级别中,从局部变量到整个类,但应始终在尽可能小的范围内使用
  4. 每当使用@SuppressWarnings(“unchecked”)都应该添加一条注释,说明为什么这么做是安全的,可以帮助别人理解代码,减少其他人修改代码后导致计算不安全的概率
//ArrayList中的toArray方法,它在长度不止一行的方法/构造器中会用了SuppressWarnings注释,实际上可以把它移到一个局部变量的声明中
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

//修改后
public <T> T[] toArray(T[] a) {
	if (a.length < size) {
		//这里新建了一个变量,并将注释SuppressWarnings放到了局部变量的声明中,最后加上了注释,写明为何这样是安全的
		// This cast is correct because the array we're creating
		// is of the same type as the one passed in, which is T[].
		@SuppressWarnings("unchecked") T[] result =
		(T[]) Arrays.copyOf(elements, size, a.getClass());
		return result;
	}
	System.arraycopy(elements, 0, a, 0, size);
	if (a.length > size)
		a[size] = null;
	return a;
}

第28条:列表优于数组

28.1 数组与列表区别

28.1.1 协变与可变
  1. 数组是协变(covariant)的:Sub是Super的子类型,Sub[]就是Super[]的子类型
  2. 泛型是可变的(invariant):Sub是Super的子类型,List<Sub>不是List<Super>的子类型
28.1.2 具体化与非具体化
  1. 数组是具体化(reified)的:即运行时知道自己的元素类型,因此下面例子中报错ArrayStoreException。其实就是运行时的信息比编译时的多
  2. 而泛型只在编译时知道自己存储的元素类型,在运行时丢弃,这叫做擦除。擦除功能就是为了兼容老版本jdk产生的。擦除我理解就是,泛型信息不会带入到运行中,这也导致了原生类型和参数化的类型可以互转
//由于泛型只在编译时知道存储的元素类型,一旦被擦除,
List<Long> o2 = new ArrayList<Long>();
//原来理解是将参数化类型的变量传给原生类型会发生擦除,实际上擦除是泛型本身的一个特性,即编译时知道元素信息,运行时候去掉,只不过我们是利用了这个特性,可以让检查实效
List ol = o2;
ol.add("I don't fit in");
System.out.println(ol.get(0));

28.2:数组的缺陷

//编译不报错,但运行时抛 java.lang.ArrayStoreException
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; 
//编译时就报错,cannot convert from ArrayList<Long> to List<Object>
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");

28.3 无法通过new创建泛型数组的实例

  1. 因为太危险,所以java给这种语法拒绝了
//1. 假设有数组泛型,下面写法表示一个数组,这个数组中的元素都是集合,而这个集合中只允许放入String类型的元素
//List<String>[] stringLists = new List<String>[1]; 
//2. 由于数组是协变的,因此Object[]是List<String>[]的父类型,而一旦转为Object[],我们就可以通过objects向 stringLists中添加非List<String>类型的数据,这是因为泛型通过擦除实现,参数化类型可以转为原生类型,原生类型可以转为Object
//Object[] objects = stringLists; 
//List<Integer> intList = List.of(42);
//objects[0] = intList;
//3. 现在出现问题,系统认为stringLists[0].get(0)是String型,无需强制转换,但实际上这个类型Integer,因此会抛ClassCastException,而Java认为,编译没有警告的地方,就不应该发生转换异常,上面没警告,下面又抛异常,因此Java不允许这种情况发生,但如果第一行为List<String>[] stringLists = new List[1]; 就可以编译通过,因为这种写法,不符合泛型的语法规则,会有警告,带有警告的内容,抛异常,是被允许的
//这种问题出现是由于混用可具体化的数组,与不可具体化的泛型而导致的,如果都是具体化的,或都是不可具体化的,就不会有这种问题
//4. 为防止这种情况发生,就必须在第一行就报错
//String s = stringLists[0].get(0);
//实际上定义一个泛型数组的变量是允许的,只不过不能通过new创建其实例,因为一旦有实例,就可以通过其引用stringLists,修改实例的值
List<String>[] stringLists = null;

  1. 实际上这个说法并没有说服我
//现在如下代码也是可以通过编译的,但实际上下方代码使用c中元素时一定会造成报错,那实际上和上面一样,可能报错可能不报错
//也就是说其实泛型数组应该可以使用,是不一定报错的,只要使用得当
List<String> a = new ArrayList<String>();
List b = a;
List<Integer> c = b;

28.4 创建无限制通配类型的数组合法

  1. 从技术角度,E、List<E>、List<String>都称作不可具体化的类型,即运行时表示法包含的信息比编译时表示法包含的信息更少。而List<?>、Map<?,?>是可具体化的,因此它编译时信息和运行时的一样多
  2. 因此创建无限制通配符类型的数组合法,但不常用
//下面代码可以通过编译
List<?>[] a = new List<?>[1]; 

28.5 可变参数的方法中传入泛型

@SafeVarargs
public static void test(List<String> ...a) {
	
}
//如果上面方法没有@SafeVarargs注释,下方代码会有warning
//这是由于当调用可变参数的方法时,会创建一个数组来存放方法声明中的那个可变参数(varargs),如果这个数组的元素类型是不可具体化的,就会得到警告
test(new ArrayList<String>(),new ArrayList<String>());

28.6 如何替代泛型数组

  1. 由于泛型数组很危险,也无法创建,最好的解决办法是使用集合类型List<E>替代E[]
  2. 这样做会损失一些性能或简洁性,但能具备更高的类型安全性和互用性
  3. 一旦发现将泛型和数组混合使用,并得到编译警告,第一反应就是用List替代数组
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class Chooser {
	private final Object[] choiceArray;

	public Chooser(Collection choices) {
		choiceArray = choices.toArray();
	}
	public Object choose() {
		Random rnd = ThreadLocalRandom.current();
		return choiceArray[rnd.nextInt(choiceArray.length)];
	}
	public static void main(String[] args) {
		List bbb = new ArrayList();
		bbb.add(5);
		bbb.add("1234");
		Chooser a = new Chooser(bbb);
		//1. 如果想使用choices中存放的元素,必须对choose的结果进行强制转换,如果转错了,运行时会报错
		//2. 下方代码有时报错,有时不报错
		//3. 为了避免自己转换,想引用泛型解决
		String b = (String)a.choose();
	}
}
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class Chooser<T> {
	private final T[] choiceArray;
	public Chooser(Collection<T> choices) {
		//此处编译器会报warning,因为数组是具体化的,编译时啥也检查不出来,因此它也无法保证运行时,这个强制转换能正常完成,因为它压根不知道运行时T到底是什么,Object的对象如果不是T的子类的实例,转T时是可能失败的
		//toArray方法,返回的是一个Object[],由于数组是协变的,所以实际上Object[]是存在转为T[]的可能的,但如果toArray这个方法中,返回的并不是一个T类型的数组,那么这个方法就会报错,可以想象自己乱写一个toArray,返回一个数组,里面包含各种不同类型元素
		//如果我们改为使用List,由于List不具体的,也不协变,即,当传入泛型时,如果想通过编译,toArray方法根本没法返回一个包含非T子类元素的集合,因此就不会出现这个问题
		choiceArray = (T[])choices.toArray();
	}
	public T choose() {
		Random rnd = ThreadLocalRandom.current();
		return choiceArray[rnd.nextInt(choiceArray.length)];
	}
}
//选择用List代替数组,消除warning,运行时不会得到ClassCastException
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class Chooser<T> {
	private final List<T> choiceArray;
	public Chooser(Collection<T> choices) {
		//List并不协变,因此不会出现转换有问题,即将new ArrayList<T>(choices)赋值给choiceArray,只要编译成功,运行时一定成功,因此编译器不再报错
		choiceArray = new ArrayList<T>(choices);
	}
	public T choose() {
		Random rnd = ThreadLocalRandom.current();
		return choiceArray.get(rnd.nextInt(choiceArray.size()));
	}
}

第29条:优先考虑泛型

  1. 本例使用泛型原因:客户端必须强制转换从堆栈中弹出的对象,而这些强制转换可能在运行时失败
import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {
	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;

	public Stack() {
		elements = new Object[DEFAULT_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; // Eliminate obsolete reference
		return result;
	}

	public boolean isEmpty() {
		return size == 0;
	}

	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}
import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack<E> {
	private E[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	//e. 一旦你认定类型安全,就可以使用@SuppressWarnings,来禁止警告
	@SuppressWarnings("unchecked")
	public Stack() {
		//1. elements = (E[]) new E[DEFAULT_INITIAL_CAPACITY];无法使用new创建泛型数组的对象
		//1. 解决方案一
		//a. 此处会有警告,因为编译器不能保证运行时,这个Object数组,一定可以转换成E[]
		//b. 这个地方的处理比较有趣,我们知道Integer[] a = (Integer[])new Object[2],这种代码,运行时是不可能通过的,因为Object[]是父类,如果不是指向子类的实例,运行时发生转换是会报错的。但此处为何不会报错呢,这是因为由于擦除的解决方案,编译器,只有在使用参数化类型的引用为真实类型的变量赋值时,才加上转换,也就是说elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]在编译器中由于elements为参数化类型的变量,而Object数组为真实的类型引用,所以编译后的字节码,并不将Object[]转为String[],但一旦满足条件,例如Stack<String> a = new Stack<>();String[] b(真实类型)= a.elements;(参数化类型)那么编译器会将字节码变为String[] b= (String[])a.elements,运行时,由于Object[]转String[]报错。
		//c. 实际上在后面pop方法E result = elements[--size];中才会发生转换
		//d. 当你确定数组中元素不能被外界访问,也无法被push外方法访问,即无法被外界修改为非E类型,那么这样数组元素就一定是E这个类型,我们认为就是安全的
		//f. 至此,你可以确保使用Stack中pop方法时,无需显式转换,也不用担心ClassCastException
		elements = (E[]) new Object[DEFAULT_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; // Eliminate obsolete reference
		return result;
	}

	public boolean isEmpty() {
		return size == 0;
	}

	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
	public static void main(String[] args) {
		Stack<String> a = new Stack<>();
	}
}
//方案二
private Object[] elements;
...
public Stack() {
	elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
...
public E pop() {
	if (size == 0)
		throw new EmptyStackException();
	@SuppressWarnings("unchecked")E result = (E) elements[--size];
	elements[size] = null; // Eliminate obsolete reference
	return result;
}
  1. 通常使用方案一
    1. 可读性更强:数组声明为E[],清楚表明该数组只能包含E的实例
    2. 更简洁:只需在创建数组时转换一次,第二种方法每读取一次元素,都要转换一次
    3. 会造成堆污染:即把一个不带泛型的对象赋值给一个带泛型的变量,但这种情况下堆污染没有什么危害
//Object[]中很可能存放的并不是E类型的元素,如果使用elements数组元素时,系统自动将其元素转为E类型,可能会出问题。但此例中,Object[]中不可能放非E的元素,因此不会有危害
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
  1. 不可能总是或总想一遇到泛型数组就是使用列表替代,因为Java不是生来就支持列表,因此有些泛型中就使用了泛型数组,例如ArrayList。甚至为提升性能,比如HashMap的泛型也在数组上实现的
  2. 类型参数没有限制,例如Stack,可以创建Stack<Object>、Stack<int[]>、Stack<List>,但不能创建基本类型的Stack,例如Stack<int>,编译会报错,但可以通过使用基本包装类型避开这个限制
  3. 有限制的类型参数
//实际的类型参数必须是Delayed的一个子类型
//允许DelayQueue内部,或客户端中,在DelayQueue的元素上,直接使用Delayed的方法(因为是他的子类型),而无需显式的转换,也没有出现ClassCastException的风险
//类型参数E称为有限制的类型参数
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
  1. 使用泛型比使用那种需要在客户端代码中手动进行转换的类型更安全、方便。当设计有转换的新类时都应该做成是泛型的。时间允许还应该把现有的类型都泛型化,这样新用户用起来更轻松,老用户的客户端也不会受影响

第30条:优先考虑泛型方法

30.1 编写泛型方法

//未使用泛型时,该方法有很多警告
public static Set union(Set s1, Set s2) {
	Set result = new HashSet(s1);
	result.addAll(s2);
	return result;
}
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
	//没添加泛型,会有警告
	Set<E> result = new HashSet<>(s1);
	result.addAll(s2);
	return result;
}

30.2 泛型单例工厂

  1. 这种工厂,可以多次返回同一个实例,但根据传给泛型方法的实际参数的不同,返回的这个实例可以传给不同的类型参数的引用
//1. 恒等函数:即f(x)=x这种函数,java类库中的Function.identity这个静态方法,会创建一个Function对象,这个Function对象的apply方法会原封不同返回该方法的参数。那么这个Function对象就是一个恒等函数,而identity方法,能够产生一个恒等函数,书中叫它恒等函数分发器
//2. 我们自己模拟实现一个恒等函数的分发器
//首先我们考虑直接创建一个恒等函数(UnaryOperator继承了Function,也有apply方法)
UnaryOperator<String> IDENTITY_FN = t->t;
String a = "123";
String b = IDENTITY_FN.apply(a);
UnaryOperator<BigDecimal> IDENTITY_Big = t->t;
BigDecimal a1 = new BigDecimal("123");
BigDecimal b1 = IDENTITY_Big.apply(a1);
//但这产生了一个问题,想要对不同类型的对象,产生恒等函数,需要建立多个UnaryOperator对象,如果建立一个也可以,但需要强制转换
UnaryOperator IDENTITY_FN_OBJ = t->t;
BigDecimal a2 = new BigDecimal("123");
BigDecimal b2 = (BigDecimal) IDENTITY_FN_OBJ.apply(a2);
//因此我们考虑使用泛型单例工厂
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
	//此处和之前例子中,E[] a = (E[])new Object[5],原理大致相同,都不会真正转换
	//会产生warning,因为这样写转换关系,实际上是编译后的内容,在这不会有转换,只会将调用IDENTITY_FN.apply()的返回值转为T类型,而编译器并不确定,在apply的返回结果,可以转为T类型,但其实由于这个函数的特殊性,总返回参数中传入的对象,而我们限定的参数中传入的是T,那么实际上就一定可以转为一个T类型的变量,因此不会产生转换问题,所以可以用@SuppressWarnings("unchecked")注释,屏蔽他的warning
	return (UnaryOperator<T>) IDENTITY_FN;
}
//此时客户端代码如下
public static void main(String[] args) {
	String[] strings = { "jute", "hemp", "nylon" };
	UnaryOperator<String> sameString = identityFunction();
	for (String s : strings)
		System.out.println(sameString.apply(s));
	Number[] numbers = { 1, 2.0, 3L };
	UnaryOperator<Number> sameNumber = identityFunction();
	for (Number n : numbers)
		System.out.println(sameNumber.apply(n));
}

30.2 递归类型限制:<E extends Comparable>

30.2.1 递归类型限制在Comparable接口中的应用
//TestNew具有了public int compareTo(T o)方法,T可以是任何类型
//下面写法表示:TestNew对象可以与任何对象进行比较
TestNew implements Comparable
//下面写法表示:TestNew对象可以与任何String对象进行比较
TestNew implements Comparable<String>
//下面写法表示:TestNew对象可以与任何TestNew对象进行比较
TestNew implements Comparable<TestNew>
//想定义一个方法,可以根据元素的自然顺序,找到集合中最大的那个元素。这意味着,要求集合中的每个元素都能与列表中的其他元素进行比较
//下面写法表示,该泛型方法中的这个E,必须实现Comparable接口,且只能与自身对应的这种类型的对象进行比较,实际上一个不只能与自身比较的E也能被传入,只不过max这个方法中根本不允许它去和别人比
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;
}
30.2.1 递归类型限制实现模拟自类型的功能
//可以看做一个自类型接口
@FunctionalInterface
public interface Builder<T extends Builder<T>> {
	T self();
}
//实现该接口的类,拥有一个可以返回自身类型的self方法
class Wusihan implements Builder<Wusihan> {
	@Override
	public Wusihan self() {
		return null;
	}

}

30.3 最佳实践

与泛型类的优点,以及最佳实践相同

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

31.1 如果提升灵活性

  1. Iterator与Iterable接口
public interface Iterator<E> {
    boolean hasNext();
    E next();
}
//Iterable接口实际上只是给与实现他的类、接口,一个iterator方法,能够返回Iterator
public interface Iterable<T> {
  Iterator<T> iterator();
}
//List实现了Iterable,所以可以调用list.iterator获得迭代器
Iterator it = list.iterator();
while (it.hasNext()) {
    System.out.print(it.next() + ",");
}
  1. for each原理
for (Integer i : list) {
    System.out.println(i);
}
//编译后内容
//java的for循环只是一个语法糖(我理解语法糖就是把一些东西简写了,很好用),内部其实是通过iterator迭代器方式实现的
Integer i;
for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(i)){
    i = (Integer)iterator.next();        
}
  1. 新增pushAll方法
//假如在JDK中的Stack类中增加如下代码
public void pushAll(Iterable<E> src) {
	for (E e : src)
		push(e);
}

//客户端代码
Stack<Number> a = new Stack<>();
Iterable<Integer> integers = List.of(123,456,789);
//无法编译通过,因为Iterable<Integer>不是Iterable<Number>的子类型
a.pushAll(integers);

//改进后的pushAll,表示Iterable中的泛型应该是E的子类型
public void pushAll(Iterable<? extends E> src) {
	for (E e : src)
		push(e);
}
  1. 新增popAll方法
//该方法将Stack中所有元素弹出,并添加到指定集合中
public void popAll(Collection<E> dst) {
	while (!isEmpty())
		dst.add(pop());
}

//客户端代码
Stack<Number> a = new Stack<>();
//正常逻辑,Number属于Object,那么存放Number的栈,其内元素应该可以放入存放Object的集合中,但此处编译报错,因为Collection<Object>无法转为Collection<Number>
a.popAll(new ArrayList<Object>());

//修改后popAll方法,表示集合中元素应该是E的超类型
public void popAll(Collection<? super E> dst) {
	while (!isEmpty())
		dst.add(pop());
}

31.2 PECS:便于记忆的一个字符串

  1. 即producer-extends,consumer-super:表示如果这个类型参数对应的变量,对于这个泛型,是生产者,那么就使用extends,如果是消费者就使用super
  2. 对于pushAll方法, 参数类型为Iterable<? extends E>,其对象src,对于这个泛型?来说,src生产E的实例,给Stack使用,因此src的参数类型应该为Iterable<? extends E>。而对于popAll方法,dst参数通过Stack,消费E实例,因此其类型参数为Collection<? super E>
  3. 如果某个参数既是生产者又是消费者,那么通配符对你没什么好处,因为你需要严格的类型匹配
  4. 案例一
//1. 修改之前的Chooser类中的构造器中的泛型,从而允许List<Integer>类
//3. choices传给choiceArray,而choiceArray最后用于创造T,因此此处应该使用extends
public Chooser(Collection<? extends T> choices) {
	choiceArray = new ArrayList<T>(choices);
}
public static void main(String[] args) {
	List<Integer> l = new ArrayList<Integer>();
	//2. 未修改Chooser构造器前,该方法无法通过编译
	Chooser<Number> a  = new Chooser<>(l);
	System.out.println(a.choose());
}
  1. 案例二
//改造之前的union方法,使其可以将Set<Number>的s1和Set<Integer>的s2合并到一起
//注意s1和s2都生产E的对象,传给新的result,因此应该使用extends
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
	Set<E> result = new HashSet<>(s1);
	result.addAll(s2);
	return result;
}
//1. 注意不要使用通配符类型作为返回类型,即返回值Set<E>不要改造成Set<? extends E>,因为如果这样写,虽然提供了灵活性,但也导致客户端代码定义其返回值类型时,必须使用通配符类型
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
	//没添加泛型,会有警告
	Set<E> result = new HashSet<>(s1);
	result.addAll(s2);
	return result;
}
//客户端代码
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
//2. 如果1中返回值使用Set<? extends E>,那么客户端代码需要如下
//Set<? extends Number> numbers = union(integers, doubles);
//java8之前,类型推导不够智能,下面代码会报错,可以做如下修改,提供一个显式的类型参数
//Set<Number> numbers = Chooser.<Number>union(integers, doubles);
Set<Number> numbers = union(integers, doubles);
  1. 通配符类型对于使用类的用户来讲,应该是几乎无形的,如果类的用户总要考虑通配符代表什么类型,那可能是这个API设计的有问题
  2. 案例三
//c可以产生E的对象,并返回,因此对于Collection<E>应该使用extends,而对于参数类型<E extends Comparable<E>>中,可以想象Comparable的对象,拥有一个compare方法,它用来消费E(compare方法传入参数E),因此应该使用super
//注意所有的Comparable和Comparator都是消费者
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;
}
//改造后方法
public static <E extends Comparable<? super E>> E max(Collection<? extends E> c) 

//对于原方法,下面方法报错,新方法不会
//因为ScheduledFuture继承了Delayed,Delayed继承了Comparable<Delayed>,即ScheduledFuture实际上是继承了Comparable<Delayed>,系统无法推导E的类型,因此编译报错,但修改后就不会有问题
//即需要用通配符支持那些不直接实现Comparable或Comparator,而是实现了Comparable<自身类型的父类型>的这种类
List<ScheduledFuture<?>> scheduledFutures = null ;
max(scheduledFutures);

31.3 类型参数与通配符

  1. 如果类型参数只在方法声明中出现一次,可以用类型通配符取代
//无限制类型参数用无限制通配符取代,有限制类型参数用有限制的通配符取代
//类型参数E只使用了一次,因此用类型通配符代替比较方便
public static <E> void swap (List<E> list,int i,int j);
public static void swap (List<?> list,int i,int j);
  1. 编写私有辅助方法捕捉通配符类型
public static void swap(List<?> list, int i, int j) {
	//set时会有问题,因为List<?>的对象list,由于不知道它的泛型类型,因此只能向里面放入null,因此下方代码无法编译通过
	list.set(i, list.set(j, list.get(i)));
}
//改造,编写一个可以捕捉通配符类型的方法
public static void swap(List<?> list, int i, int j) {
	swapHelper(list, i, j);
}

//这个辅助方法,可以允许我们导出简单的这种基于通配符的方法声明,这样swap的客户端不必面对复杂的swapHelper声明
private static <E> void swapHelper(List<E> list, int i, int j) {
	list.set(i, list.set(j, list.get(i)));
}

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

32.1 泛型和可变参数并用导致的问题

// 1. 如果方法声明中,其可变参数的类型为非具体化的(例:List<String>),会提示警告:Type safety: Potential heap pollution via varargs parameter a,意思是可能会造成堆污染
// 这是因为当一个参数化的类型的变量,a,如果指向一个不是这个参数化的类型的实例时,会产生堆污染。堆污染导致编译器自动生成的转换失败,破坏了泛型系统的基本保证
// 正常情况下,编译器是不允许参数化的类型的变量指向一个不是这个参数化的类型的实例的。而可变参数类型为参数化的类型这种情况,为堆污染提供了便利的条件,因此会提示这个warning
public static void test(List<String>... a) {

}

//可变参数的类型为参数化的类型,而引发的问题
static void dangerous(List<String>... stringLists) {
	List<Integer> intList = List.of(42);
	Object[] objects = stringLists;
	// 会造成堆污染,相当于修改了参数化类型List<String>的变量stringLists的指向,指到了一个List<Integer>实例上
	objects[0] = intList; 
	// 堆污染导致了ClassCastException,因为实际上编译器会产生一个不可见的转换,Integer转String
	// 由此证明,可变的参数中,使用了参数化的类型是不安全的
	String s = stringLists[0].get(0); 
}

32.2 为何只是警告

  1. List<String>[] stringLists = new List[1]; 会报错,但可变参数中使用泛型时,也相当于创建了一个泛型数组,为何不报错,只是警告
  2. 这是因为可变参数中,类型为泛型、或为参数化的类型这种情况,在实际中用处很大,Java设计者选择容忍这一矛盾存在
  3. Java类库中的这种方法
//这些方法都是很小心编写的,都是类型安全的
Arrays.asList(T a)
Collections.addAll(Collection<? super T> C,T...elements)
Enum.of(E first ,E...rest)

32.3 消除调用方法处警告

  1. 消除
public static void main(String[] args) {
	// 1. 调用上面那种方法的地方也会有一个warning:Type safety: A generic array of List<String> is
	//如果在上面定义方法上加上注释@SuppressWarnings("unchecked"),只是方法定义位置不再警告,但调用处还是会警告
	//必须在定义方法处@SafeVarargs注释,表示方法设计者承诺,该方法时类型安全的,才能使所有客户端警告取消
	//必须确定上面那个方法确实类型安全,才可以使用@SafeVarargs注释
	test(new ArrayList<String>(), new ArrayList<String>());
}

32.4 如何确定方法签名中包含可变参数的泛型是类型安全的

  1. 实际上泛型数组是在调用这种方法时创建的
  2. 该方法没有在可变参数数组中保存任何值
  3. 该方法没有对不信任的代码开放该数组(或其它克隆程序)
  4. 换句话说可变参数数组只用于将数量可变的参数,从调用程序传到方法
  5. 那么该方法就是类型安全的
public static void test(List<String>... a) {
	//违反第3条,将数组传给了不被信任的代码
	Object[] b = a;
	//不被信任的代码
	b[0] = List.of(42);
}
//B方法
//1. 直接使用该方法toArray("123","456")时,会返回一个String[]的实例
static <T> T[] toArray(T... args) {
	return args;
}

static <T> T[] pickTwo(T a, T b, T c) {
	switch (ThreadLocalRandom.current().nextInt(3)) {
	case 0:
		//2. pickTwo方法中,相当于使用该方法toArray(a,b),由于编译时,还不知道a,b具体类型,因此会返回一个Object[]的实例
		//3. 客户端相当于不被信任的代码,pickTwo将数组泄露给客户端,违背了第3条,因此pickTwo也不安全
		return toArray(a,b);
	case 1:
		return toArray(a,c);
	case 2:
		return toArray(b,c);
	}
	throw new AssertionError();
}
//客户端
public static void main(String[] args) {
	//java.lang.ClassCastException
	String[] a = pickTwo("1234", "12345","6666");
}

32.5 @SafeVarages注释

  1. 将数组传给另一个用@SafeVarages正确注释过的可变参数方法是安全的,因为这种方法本身很小心,规避了那些可能造成类型转换异常的代码,将数组传给没有可变参数的方法也是安全的
  2. 该注释只能被用在无法被覆盖的方法上,因为它不能确保每个可能覆盖的方法都安全
  3. Java8中,该注释只在static方法或final方法中才合法,Java9中,在私有实例方法上也合法

32.6 使用List替换泛型数组

//正常可变参数类型为泛型的方法写法,需要确定类型安全并加上@SafeVarargs注释,保证客户端不会warning
@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;
}
//优点:使用List替换,编译器可以证明该方法类型安全,因此不再提示warning,可以省略@SafeVarargs注释
//缺点:客户端代码繁琐(客户端得先组一个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;
}
//之前的pickTwo方法,使用List替换泛型数组
static <T> List<T> pickTwo(T a, T b, T c) {
	switch (rnd.nextInt(3)) {
	case 0:
		return List.of(a, b);
	case 1:
		return List.of(a, c);
	case 2:
		return List.of(b, c);
	}
	throw new AssertionError();
}

33 优先考虑类型安全的异构容器

33.1 Java中的容器

  1. Set<E>、Map<K,V>、ThreadLocal<T>、AtomicReference<T>
  2. 他们的类型参数,只能是某一种类型
    1. 例如Set:那么其类型参数就只是String,也只能向这个Set中放入类型为String的元素
    2. 例如Map<String,Number>:那么其类型参数K值为String,V值为Number,无法再代表其他类型
  3. 使用容器存储数据库中的一行的问题:由于一个表中,每一列都是不同的类型
//如果将一行中每个列的元素,都使用Object存放,也可以,但会出现代码过于复杂
//key表示列明,String类型,Object表示列的值,由于不确定是哪种类型,只能用Object类型接收
Map<String,Object> a = new HashMap<>();
//省略将数据每列数据放入a的代码
....
//取出数据时,我们不知道acct_no是String类型,因此必须转换,非常不方便,也不确定是否可以转换成功
String acct_no = (String)a.get("acct_no");
int acct_seqn = (Integer)a.get("acct_seqn");

33.2 类型安全的异构容器

  1. 将键(key)进行参数化,而不是容器进行参数化。之后用参数化的键来插入或获取值。用泛型系统确保值的类型与它的键相符
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
//1. 假设现在有一个表Favorites,它的每一行,会保存不同的人所喜爱的不同类型的内容,例如其第一列保存最喜爱的String,第二列保存最喜爱的Integer,第三列保存最喜爱的Class,等等
public class Favorites {
	//2. 将键参数化,而不是将容器参数化,即通配符代表的是键Class的类型,而不是容器Map中的K的类型
	//3. 对于String.class,属于Class<String>类型,Integer.class属于Class<Integer>类型
	//4. Favorites的值的类型,只能是Object,因为Java目前无法保证Map中的键和值的类型之间的关系(比如这种定义:Map<Class<T>, T>),即键是Class<String>,但值是Object,不能保证值是String
	private Map<Class<?>, Object> favorites = new HashMap<>();

	public <T> void putFavorite(Class<T> type, T instance) {
		favorites.put(Objects.requireNonNull(type), instance);
	}
	//9. 由于Java中无法做到在Map中定义键和值的关系,那么只能人为去进行转换,将取到的Object类型,转成键的参数的类型T
	public <T> T getFavorite(Class<T> type) {
		//10. type的cast方法,是对Java转换符的动态模拟,当一个实例,是type的类型参数T	的实例,就返回该实例,使用如下方法替代程序也能运行,但会有异常
		//(T)favorites.get(type);
		return type.cast(favorites.get(type));
	}

	public static void main(String[] args) {
		Favorites f = new Favorites();
		//5. String.class为类型令牌:当一个类的字面被用在方法中,来传达编译时和运行时类型信息时,就叫类型令牌
		f.putFavorite(String.class, "1234");
		f.putFavorite(Integer.class, 0xcafebabe);
		f.putFavorite(Class.class, Favorites.class);
		//6. Favorites的实例是类型安全的:当你向他请求String,它一定不会反回一个Integer给你
		//7. 同时它也是异构的:每个键都有一个不同的参数化类型
		//8. 因此称Favorites为类型安全的异构容器
		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());
	}
}

33.3 类型安全的异构容器局限性

  1. 恶意客户端可以通过传入原生类型的Class对象,而破坏类型安全
//此时编译不报错
f.putFavorite((Class)String.class, 1234);
//直到下面客户端代码才报错
String favoriteString = f.getFavorite(String.class);
//可以通过修改putFavorite代码,在放入时,就确认要放入的instance是否为type所表示的类型的实例,这样就可以重新获得类型安全,编译还是能通过,但在放入元素时就会报错,而不是等到取出元素时
public <T> void putFavorite(Class<T> type, T instance) {
	favorites.put(type, type.cast(instance));
}

//Jdk为防止恶意传入原生类型对象,破坏类型安全的做法
//java.util.Collections类中提供了checkedSet、CheckedList、CheckedMap等方法,根据一个集合,返回另一个集合,这个集合可以确保放入这个集合中的元素,与其参数化的类型相符
//不受检查的集合
List<String> list = Arrays.asList("12","23");
List obj = list;
//此时不会抛异常
obj.add(112);

//受检查的集合
List<String> list = Arrays.asList("12", "23");
List<String> safeList = Collections.checkedList(list, String.class);
List obj = safeList;
//在运行时,添加错误的元素就会报异常,不会等到取出元素时
obj.add(new Date());
  1. 无法用在不可具体化的类型中
//如下代码,编译就会报错,因为List<String>.class语法是错误的,List<String>.class和List<Integer>.class共用一个Class对象,即List.class
f.putFavorite(List<String>.class, new ArrayList<String>());

33.4 有限制的类型令牌

  1. 用有限制的类型参数或有限制的通配符,可以限制类型令牌所能表示的类型
//例如AnnotatedElement接口中的该方法,T只能是Annotation的子类型,因此也限制了类型令牌只能是Annotation子类型.class,即该方法中只能传入注解类型的类型令牌
//被注解的元素本质上是一个类型安全的异构容器,容器的键就是注解的类型,值是注解对象本身
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
  1. 使用asSubclass去除警告
//该方法会有警告,因为转换是非受检的,可以使用asSubclass方法去除这种警告,和之前的type.cast(favorites.get(type));方法很相似
Class<? extends Annotation> b = (Class<? extends Annotation>) a;
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
	Class<?> annotationType = null; 
	try {
		annotationType = Class.forName(annotationTypeName);
	} catch (Exception ex) {
		throw new IllegalArgumentException(ex);
	}
	//将调用它的class对象转换成其参数(Annotation.class)表示的类的对象,转换成功返回调用它的class对象,否则抛出ClassCastException
	//和下面方法一样,只不过下面方法时未受检的,会有warnning
	//return element.getAnnotation((Class<Annotation>) annotationType);
	return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

33.5 最佳实践

  1. 对于类型安全的异构容器,可以使用Class对象作为键,以这种方式使用的Class对象称为类型令牌
  2. 也可以使用定制的键类型,例如DatabaseRow类表示一个数据库行(容器),用泛型Column<T>作为它的键
发布了32 篇原创文章 · 获赞 0 · 访问量 936

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/102998988