Effective Java读书笔记(4)—— 泛型

本文介绍了《Effective Java》关于泛型的使用建议,包括避免使用原生态类型,消除非受检警告,优先选择列表和泛型方法,利用有限制通配符提升API灵活性,以及在泛型和可变参数结合时的注意事项。通过这些最佳实践,可以提高代码的类型安全性,减少潜在的运行时错误。
摘要由CSDN通过智能技术生成

先简单说一下泛型,这是从Java5开始的一个重要新特性:比方说,我现在想建立一个容器,容器里存储的元素类型可以使任意类型(可以存储dog,也可以存储cat),当然一个容器对象只能存储某一种,不能一个容器寄存dog又存cat。在泛型出现之前,我们想实现上述的功能需要这么做:

ArrayList dogs = new ArrayList();  // 元素都是Object类型
dogs.add(new Dog());  // 向上转型为Object对象
Dog dog = (Dog)dogs.get(0);  // 手工向下转型为Dog类型
dogs.add(new Cat());  // 一个糊涂的程序员不小心加入了一个cat,不会报错,因为Cat也可以向上转型为Object类型
dog = (Dog)dogs.get(1);  // 报类型转换异常,Cat转Dog转不了

上面这个例子展现了泛型出现前的困境:

  1. 用一个存储Object对象的容器来存储任意类型的时候,首先我们需要用容器名dogs来表明这个容器存的是什么类型的对象;
  2. 在添加对象的时候一旦添加了错误的对象,编译时期并不会报错。一般来说我们希望有错误能够及早发现,最好在编译器就发现,而不是运行的时候再发现;
  3. 获取对象的时候需要手动进行类型转换,比较繁琐,而且一旦出现了第二点中的问题,会报类型转换异常。

有了泛型后,只需要这样做:

ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
Dog dog = dogs.get(0);
dogs.add(new Cat());  //  编译期报错

可以看到,上面的三个问题都得到了解决:

  1. 泛型类型表明容器存储的对象类型;
  2. 添加错误类型的对象,编译期报错;
  3. 获取对象的时候无需手动类型转换 ;

泛型类型可以是具体类型,也可以是?通配符,也可以是<? extends ClassName>或<? super ClassName>。

接下来介绍书里的建议:

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

上面的代码里,ArrayList<Dog>是泛型类型,ArrayList是原生态类型,之所以保留原生态类型是为了向前兼容(保留原生态类型,也就是说加不加泛型类型对于JVM而言都是一样的,这就是Java泛型的类型擦除,Java的泛型和C++不同,更多的是一种语法糖)。

即使对于容器中存储的元素类型没有限制,也应当使用Collections<?>的形式,也就是通配符,而不应该直接使用原生态类型。

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

用泛型编程时会遇到许多编译器警告:非受检转换警告、非受检方法调用警告、非受检参数化可变参数类型警告。

要尽可能地消除每一个非受检警告,消除所有警告可以确保代码是类型安全的。

如果无法消除警告,且可以确保引起警告的代码是类型安全的,才可以用@SuppressWarnings("unchecked")注解来禁止警告。

并且,@SuppressWarnings("unchecked")的范围应该尽可能的小(后面直接跟一行代码),且给出注释,说明为什么一定是类型安全的。

第三条:列表优于数组

数组和泛型的两大不同点:

  • 数组是协变的(若Son为Father的子类,那么Son[]便是Father[]的子类),泛型是可变的(List和List不存在继承关系)。
// 运行时才会出错
Object[] objects = new Long[1];
objects[0] = "hello";  // 运行时抛出ArrayStoreException

// 编译期就报错
List<Object> objects = new ArrayList<Long>();  // 报错Incompatible types
  • 数组是具体化的,运行时知道元素类型;泛型原理是类型擦除,运行时并没有类型信息。不能创建泛型类型的数组(因为数组是具体化的,但是泛型类型在运行时不带类型信息),创建带通配类型的数组是合法的。
public class Chooser<T>{
	private final T[] choiceArray;

	public Chooser(Collections<T> choices){
		choiceArray = choices.toArray();
	}
}

在上面的代码中,我们试图调用Collections对象的toArray方法将其转换为T[]类型,其实Collections的泛型信息被擦除后相当于存储的是Object类型,Object[]转换成T[]会报错。更好的做法是吧choiceArray声明为List<T>。

第四条:优先考虑泛型

当我们自己编写一个容器类(或者其他不指定具体类型的类)时,优先考虑使用泛型,而不是维护一个Object数组。

在编写泛型类的时候会出现一些编译期的警告,我们在确保类型安全的情况下可以禁止警告,比如我们定义一个泛型类Stack:

@SuppressWarnings("unchecked")
public Stack(){
	elements = new E[DEFUALT_INITIAL_SIZE];
}

elements字段类型是E[],上面代码中new一个E类型的数组是不合法的,会报未受检警告。可以这么写:

@SuppressWarnings("unchecked")
public Stack(){
	elements = (E[])new Object[DEFUALT_INITIAL_SIZE];
}

当然,也可以直接把elements定义为Object数组,在取元素的时候转化成E(泛型类可以保证加进去的元素都是E类型的,所以虽然会有警告,但是同样可以通过@SuppressWarnings注解来禁止警告)。

但是上面代码里展示的方法可读性更强(通过elements的类型就知道装的是什么元素)也更简洁(取元素的时候直接取就行不用转换),所以上面的写法更好。这种写法的缺点在于会导致堆污染(但在这个场景下不会造成什么危害):数组的运行时类型与编译时类型不匹配。

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

泛型方法就是带有泛型类型的方法,这个泛型类型可能是返回值或参数的类型。

什么时候需要用泛型方法呢?看下面的例子:

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

上面的代码里,两个参数用的都是Set类型,即原生态类型,编译会报出未受检警告,类型不安全。更好的做法是将参数改为Set<E>类型,这样整个方法就变成了泛型方法,签名如下:

public static <E> Set<E> union(Set<E> s1, Set<E> s2)
第六条:利用有限制通配符来提升API的灵活性

上面提到,List<Father>和List<Son>不存在继承关系,因此如果我们想在List<Number>里添加一个Integer对象是不可以的,虽然直觉上来说,Integer也是一个Number。

解决的方式呢,就是把List<Number>改成List<? extends Number>就ok了。这是在向容器中生产元素的情况。

当我们从容器中消费元素的时候,

Integer i = (Integer)stack.pop();

那么这个Stack的类型应当是Stack<? super Integer>。在选择通配符的时候,有一个PECS准则:producer-extends,consumer-super。

类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行生命。比如下面两个方法声明可以实现相同的效果:

public static <E> void swap(List<E> list, int i, int j);

public static void swap(List<?> list, int i, int j)

一般来说,如果泛型类型仅在方法声明中出现,方法体中没有出现,用通配符比较好。

但是我们不能把除null以外的任何值放入List<?>类型的容器中,只能这么做:

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

虽然辅助函数还是用到了具体类型泛型,但是这是一个private方法,对客户端是不可见的。

第七条:谨慎并用泛型和可变参数

可变参数和泛型并不能良好地相互作用。可变参数的作用是让客户端能够将不定数量的参数传给方法,其实就是传了一个参数数组。

允许另一个方法访问一个泛型可变参数数组是不安全的,但将数组传给另一个用@SafeVarargs正确注解的可变参数方法是安全的,将数组传给只计算数组内容部分函数的非可变参数方法也是安全的。

第八条:优先考虑类型安全的异构容器

一般来说泛型类用来存储一个或多个同类型的元素,但时候我们需要更多的灵活性。

比如对于一个数据库表而言,比方说所有行都是Record类型,那么我们想以行为单位存储和访问的话,只需要构建List<Record>就可以了,但如果我们想以列为单位存储呢?每一列代表不同的字段,类型是不同的。为了存储不同类型的元素,我们可以将键进行参数化we不是将容器参数化,然后将参数化的键提交给容器来插入或获取值。

以Favorites类为例,允许客户端保存并获取一个“最喜爱”的实例。

public class Favorites{
	public <T> void putFavorite(Class<T> type, T instance);
	public <T> T getFavorite(Class<T> type);
}

完整实例如下:

public 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实例的类型安全(比如参数使用原生态的Class类型对象)
  • 不能用在不可具体化的类型中,比如这个type不能为List<String>。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值