effective Java-泛型

本文介绍了Java泛型的使用技巧,包括避免原始类型、消除非受检警告、使用列表代替数组、优先选择泛型和泛型方法等。此外,还讨论了如何利用通配符提高API灵活性及创建类型安全的异构容器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

泛型

26. 请不要使用原生态类型

泛型类和泛型接口统称为泛型generic
每一种泛型定义一组参数化的类型parameterized type
每一种泛型都定义一个原生态类型raw type:即不带任何实际类型参数的泛型名称,如List原生类型是List,主要为了泛型出现之前的代码进行兼容。

如果使用原生态类型,就失掉了泛型在安全性和描述性方面的所有优势

原生态类型List和参数化的类型List 的区别:

不严格的说,List逃避了编译器泛型检查,而List<Object>告诉编译器它能够持有任意类型的对象。
虽然可以将List<String>传递给类型List的参数,但是不能将它传给类型List<Object>的参数。
泛型有子类型化subtyping的规则,List<String> 是原生态类型List的一个子类型,而不是参数化类型List<Object>.

若使用像List这样的原生态类型,就会失掉类型安全性,但是若使用像List这样的参数化类型,则不会

可以使用通配符或者有限制的通配符代替原生态类型(不确定或者不关心实际的类型参数)
Set<?>,Set<? extends T>,Set<? super T> 代替Set

不能将任何元素除了null 放到Collection<?>中

例外:必须在类文字class literal 中使用原生态类型即List.class

利用泛型使用instanceof操作符的首选方法:

if (o instanceof Set) {
	Set<?> s = (Set<?>) o;
}
确定o是set就转换为通配符类型,这是受检checked转换,不会编译警告

27.消除非受检的警告

泛型编程会遇到许多编译器警告:非受检转换警告(unchecked cast warning),非受检方法调用警告,非受检参数化可变参数类型警告(unchecked parameterized vararg type warning),以及受检转换警告(unchecked conversion warning)

要尽可能地消除每一个非受检警告:避免ClassCastException

如果无法消除警告,同时可以证明引起警告的代码是类型安全的,此时才可以用@SuppressWarnings(“unchecked”)注解来禁止这条警告

应该始终在尽可能小的范围内使用SuppressWarnings注解
如下:声明新的局部变量result,将修饰方法的SuppressWarnings注解修饰局部变量,将禁止非受检警告的范围减到最小

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(elements, 0, a, 0, size);
	if (a.length > size)
		a[size] = null;
	return a;
}

28.列表优于数组

数组与泛型相比,有两个重要的不同点。
首先,数组是协变的covariant,即若Sub为Super的子类型,那么数组类型Sub[]就是Super[]子类型。
相反,泛型则是可变的invariant:对于任意两个不同的类型Type1和Type2,List既不是List的子类型,也不是List的超类型。

综上数组有缺陷:

Object[] objArray = new Long[1];
objArray[0] = "test";  // Throws ArrayStoreException
然而列表不会
// won't compile 无法通过编译
List<Object> obj = new ArrayList<Long>(); // Incompatible types
obj.add("test")

数组与泛型间第二大区别在于,数组是具体化的reified。因此数组会在运行时知道和强化它们的元素类型。
而泛型通过擦除erasure来实现,泛型只在编译时强化它们的类型信息,并在运行时丢弃或擦除它们的元素类型。擦除就是使泛型可与没有使用泛型的代码随意进行互用(反射).

技术角度说:E,List,List这样的类型称作不可具体化的nonreifiable类型。直观说,不可具体化的类型non-reifiable类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型 无限制的通配符类型

创建无限制通配类型的数组合法,创建泛型数组不合法

混用数组和集合得到编译时报错或警告需要用列表代替数组

29.优先考虑泛型

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;
		return result;
	}
	
	public boolean isEmpty() {
		return size == 0;
	}
	
	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}

对于Stack类,应该先被参数化,但没有,可在后面将它泛型化generify,即可将其参数化,而不破坏原来非参数化版本的客户端代码

public class Stack<E> {
	private E[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	public Stack() {
		// 此处报错 不可用不可具体化的类型创建数组
		elements = new E[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;
		return result;
		}
	}
	
	private void ensureCapacity() {
		if (size == elements.length) {
			elements = Arrays.copyOf(elements, size * 2 + 1);
		}
	}
}

其中elements = new E[DEFAULT_INITIAL_CAPACITY];得到错误提示或 警告,每当编写用数组支持的泛型时,都会出现。解决问题有两种方法:
一:直接绕过创建泛型数组的禁令,即创建一个Object的数组,并将它转换成泛型数组类型。

elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
错误消除但是编译器产生一条警告,此法不是类型安全的,需要人为保证类型安全,即elements数组放在私有域且保存的唯一元素是通过push方法得到的E类型元素

二:将elements域的类型从E[]改为Object[]

elements = new Object[DEFAULT_INITIAL_CAPACITY];
此时下列代码产生错误
E result = elements[--size];
改为
E result = (E) elements[--size];
此时获得和一一样的警告

一常用但是会引起堆污染(32条)(heap pollution):数组的运行时类型与他的编译时类型不匹配,除非E刚好是Object

每个类型都是都是它自身的子类型

有限制的类型参数
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
则DelayQueue<Delayed>合法

30.优先考虑泛型方法

方法也可以从泛型中受益,静态工具方法尤其适用于泛型化。如Collections中的所有算法方法都泛型化了 sort binarySearch

public static Set union(Set set1, Set set2) {
	// 此处警告类型不安全
	Set result = new HashSet(s1);
	// 此处警告类型不安全
	result.addAll(s2);
	return result
}

将以上方法泛型化解决类型安全警告,声明类型参数的类型参数列表,处在方法的修饰符及其返回值之间 即类型参数列表为,返回类型Set

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

以上泛型化的方法其局限性在于参数类型必须完全相同。利用有限制的通配符类型bounded wildcard type可使方法变得更加灵活

而创建一个不可变但又适用于不同类型的对象,可给所有必要的类型参数使用单个对象,如此需要泛型单例工厂,常用于函数对象

递归类型限制recursive type bound 通过某个包含该类型参数本身的表达式来限制类型参数如Comparable接口

public interface Comparable<T> {
	int compareTo(T o);
}

public static <E extends Comparable<E>> E max(Collection<E> c);
类型限制<E extends Comparable<E>>,可以读作“针对可以与自身进行比较的每个类型E”
即Comparable声明方法时不知道子类E的存在,通过继承和泛型,动态返回适配子类型

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

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

以上方法编译时正确,然而当src元素类型与Stack的E不匹配时会报错,即Iterable 传入一个Integer

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
报错

解决方法:引入通配符

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

对于pop

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

在以下代码也会报错

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
报错

此时使用<? super E> 即E的某种超类的集合

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

综上<? extends E>用于生产者

<? super E> 用于消费者 **为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型**,若某个输入参数既是生产者又是消费者,则需要严格的类型匹配而非通配符。 **PECS表示producer-extends,consumer-super** 即参数化类型表示一个生产者T,使用<? extends T>;表示一个消费者T,使用<? super T> 例如Stack实例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E> **不要用通配符类型作为返回类型!** **如果类的用户必须考虑通配符类型,类的API或许就会出错** *** ``` public static
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)));
}

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

泛型可变参数数组

static void dangerous(List<String>... stringLists) {
	List<Integer> intList = List.of(42);
	Object[] objects = stringLists;
	// Heap pollution
	objects[0] = intList;
	// ClassCastException
	String s = stringLists[0].get(0);
}

将值保存在泛型可变参数数组参数中是不安全的

显式创建泛型数组非法,用泛型可变参数声明方法却合法:

Arrays.asList(T...a),Collections.addAll(Collection<? super T> c, T ...elements),以及EnumSet.of(E first,E...rest)都是类型安全的泛型可变参数声明方法

SafeVarargs注解是通过方法的设计者做出承诺,声明这是类型安全的
若方法没有在数组中保存任何值,也不允许对数组的引用转义(这可能导致不被信任的代码访问数组),那么他就是安全的。即,若可变参数数组只用来将数量可变的参数从调用程序传到方法,那么该方法安全:如Arrays.asList

static <T> T[] toArray(T...args) {
	return args;
}
这个数组的类型是由传到方法的参数的编译时类型来决定的,因为该方法返回其可变参数数组,他会将堆污染传到调用堆栈上。
static <T> T[] pickTwo(T a, T b, Tc) {
	switch(ThreadLocalRandom.current().nextInt(3)) {
		case 0: return toArray(a, b);
		case 1: return toArray(a, c);
		case 2: return toArray(b, c);
	}
}

执行以下方法报错

public static void main(String[] args) {
	String[] attributes = pickTwo("test", "test1", "test2");
}

因为堆污染,toArray返回一个Object[]数组而pickTwo试图返回String[]数组,有一个隐藏的String[]转换,但是转换失败,这是因为从实际导致堆污染的方法处移除了两个级别,可变参数数组在实际参数存入之后没有进行修改
故而允许另一个方法访问一个泛型可变参数数组是不安全的

泛型可变参数方法在下列条件下安全:

  1. 它没有在可变参数数组中保存任何值
  2. 它没有对不被信任的代码开放该数组(或者其克隆程序)

解决方法:用一个List参数代替可变参数,同时配合java9的List.of()方法

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;
}

audience = flatten(List.of(friends, romans, countrymen));

总结:

可变参数和泛型不能良好的合作,因为可变参数设施是构建在顶级数组之上的一个技术露底,泛型数组有不同的类型规则。

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

泛型最常用于集合,如Set,Map<K, V>以及单个元素的容器,ThreadLocal.如此限制了每个容器只能有固定数目的类型参数
有时需要更多的灵活性,即能以类型安全的方式访问所有列:将键key进行参数化而不是容器container参数化,然后将参数化的键提交给容器来插入或者获取值。用泛型系统来确保值的类型与它的键相符。
以下为实例:

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 faroriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);
    System.out.println(String.format("{%s},{%x},{%s}", faroriteString, favoriteInteger, favoriteClass.getName()));
}
打印:{java},{cafebabe},{Favorites}

Favorite的完整实现:

public class Favorites{

    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type) {
    // 类型转换前做了安全判断
    return type.cast(favorites.get(type));
}
}

以上Favorite实例是类型安全的typesafe,当请求String的时候不会返回一个Integer。同时它是异构的heterogeneous:不像普通的映射,它的所有键都是不同类型的,因此我们将Favorites称作类型安全的异构容器typesafe heterogeneous container
其中getFavorite方法是核心,因为putFavorite方法放弃了键和值之间的"类型联系"而getFavorite方法通过Class的cast方法将对象引用动态地转换dynamically cast成了Class对象所表示的类型

Favorites类有两种局限性:
一. 客户端可以通过原生态形式raw form使用Class对象,
确保运行时类型安全的方式是让putFavorite方法检验instance

只需要一个动态的转换
    public <T> void putFavorite(Class<T> type, T instance) {
    	favorites.put(type, type.cast(instance));
    }

二. 它不能用在不可具体化的non-reifiable类型中,即可以保存String,String[]但不能保存List,若保存则程序无法编译,原因在于List.class是个语法错误

Favorites使用的类型令牌type token是无限制的,即可以接收任何Class对象,可以通过有限制类型参数或有限制通配符限制可以表示的类型

若有一个类型为Class<?>的对象,且要将它传给一个需要有限制的类型令牌的方法,此时可将对象Class<?>转换成Class<? extends Annotation>,但这种转换时非受检的

public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
	Objects.requireNonNull(annotationClass);
	return annotationClass.cast(declaredAnnotations().get(annotationClass));
}
而Class类提供了一个安全且动态地执行这种转换的实例方法,称作asSubclass,它将调用它的Class对象转换成用其参数表示的类的一个子类。若转换成功,返回它的参数;失败抛出ClassCastException
以下案例利用asSubclass方法在编译时读取类型未知的注解
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));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值