本文结合《Effective Java》第五章泛型和自己的理解及实践,讲解了Java泛型的知识点。文章发布于专栏Effective Java,欢迎读者订阅。
1、创建List时,删掉ide帮你自动生成的尖括号,然后发现编译器的警告,就按照ide的提示加上注解来消除警告
就像这样
@SuppressWarnings("unchecked")
Set result = new HashSet(s1);
2、写一个方法处理String类型的列表,入参是List<String>,然后下一次发现要处理Integer类型的列表,就再写一个方法,入参改为List<Integer>
如果你经常写这样的代码,那么,是时候好好学学Java泛型了。
声明中具有一个或者多个类型参数的类或者接口,就是泛型。
直观来看,我们常用的List,
List<String> list = new ArrayList<>();
尖括号里可以传入一个类型参数,它是泛型;
再比如
Map<String,Object> = new HashMap<>()
尖括号里可以传入两个类型参数,它也是泛型;
我们自己定义一个类,public Class MyGeneric<K,V,T>,这也是泛型。
其实,对于每一个泛型,都可以采用原生态类型(raw type)的方法去创建对象,即在定义的时候不带任何实际类型参数,比如,我们使用原生态类型,创建一个存放邮票的集合
Collection stamps = ... ;
这时候,下面这句代码是可以编译通过的
stamps.add(new Coin());//往stamps集合放入了Coin类型的对象
接着,麻烦来了,我们在遍历该集合时,理所当然的以为,这个集合里所有的对象都是Stamp类型
for( Iterator i = stamps.iterator();i.hasNext(); ) {
Stamp s = (Stamp)i.next();//throws ClassCastException
}
于是,当我们遍历到那个滥竽充数的coin时,就会强转失败,抛出ClassCastException
还好,我们有泛型,我们使用泛型,重新定义stamps
Collection<Stamp> stamps = ... ;
这时候,编译器就知道这个stamps里面,只能包含Stamp对象,下面这句代码,也就会在编译时就报错
stamps.add(new Coin());//编译错误
而且在遍历的时候,我们也就不需要强转了
for( Iterator i = stamps.iterator();i.hasNext(); ) {
Stamp s = i.next();//无需强转
}
总结一下,为什么要使用泛型?因为,如果采用原生态类型,就会导致类型安全性问题,同时,在表述性方面也会变差。Java之所以还支持原生态类型,仅仅是出于兼容性考虑。
这两种list,都表示 集合的元素可以是任何对象,那区别在哪呢,三行代码告诉你差别
List<String> listString = new ArrayList<>();
List listRawType = listString;//编译通过
List<Object> listObject = listString;//编译失败 Type mismatch: cannot convert from List<String> to List<Object>
也就是说,我们可以将List<String>的对象传递给List类型的参数,但是不能传递给List<Object>;
再往深了说,List<A>永远不会是List<B>的子类型,泛型List之间不存在父子关系。
这一点和数组是不同的,假如 A extends B,那么,A[] 就是 B[]的子类型,A[]可以传递给B[]类型的参数,但是,这些在List是不存在的。
再举一个例子,这次引用书中的代码
// Uses raw type (List) - fails at runtime! - Page 112
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0); // Compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
这段代码是可以编译通过的,而在实际运行中,就会导致给List<String>中,插入了一个Integer类型的对象;
修改的方法就是在unsafeAdd方法声明中用List<Object>代替List,这样,编译时就会无法通过。
因为数组是运行时类型安全,而列表是编译时类型安全,可以在编译时就避免错误。
什么意思?举个例子,采用数组,下面这段代码编译通过
Object[] objecArray = new Long[1];
objecArray[0] = "hey dude";
但是运行时,由于往一个long类型的数组插入字符串的元素,会抛出ArrayStoreException异常
如果使用List
List<Object> objectLIst = new ArrayList<Long>(); //编译错误
objectLIst.add("hey dude");
第一行代码编译错误,原因和上面提到的一样,List<Long>不是List<Object>的子类型
这一小节,我们来看看泛型实际运用中的一个例子,泛型方法。
这里引用书中的例子,下面这个方法,返回两个集合联合后的集合
// Raw type method
public static Set unionUnSafe(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
这个方法很明显不是类型安全的,调用者可以给方法传递元素类型不同的s1和s2,在编译时会发现很多unchecked告警。 现在,我们来对这个方法进行泛型化改造
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
这就是泛型方法,和普通方法不同的地方在于,要在方法修饰符和返回类型之间,声明类型参数列表
public static <E> Set<E> union (Set<E> s1, Set<E> s2)
这样的方法声明,通过泛型解决了类型安全的问题,但是有一个局限性,就是要求输入的两个集合的类型必须要相同。当然,我们很容易解决这个问题,比如你可以这样声明你的方法
public static <E extends T,K extends T,T> Set<T> union2(Set<E> s1, Set<K> s2) {
Set<T> result = new HashSet<T>(s1);
result.addAll(s2);
return result;
}
我们在类型参数列表中,声明了E、K、T三个泛型,其中,E和K都是T的子类,E是s1的类型,K是s2的类型,T是返回值类型。
更优雅的,你还可以使用有限通配符类型来改造这个方法
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
关于有限通配符类型,这是下一小节要讲的内容。
有限通配符是为了解决什么问题呢?
假设我们定义了下面这个方法,用来处理Number类型的列表
public static void processNumber(List<Number> numbers) {
for (Number num : numbers) {
System.out.println(num);
}
}
我们会理所当然的以为,这个方法也可以处理Integer、Long等 Number子类的元素的集合,
然而,就像我们之前所说的,List<Integer>不是List<Number>的子类,
因此我们的愿望落空了,下面这段代码,连编译都不通过
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3);
processNumber(integers);//The method processNumber(List<Number>) in the type GenericDemo1 is not applicable for the arguments (List<Integer>)
}
这个时候,就要用到有限通配符了
我们要做的,只是把processNumber方法的声明改为
public static void processNumber(List<? extends Number> numbers)
这个方法就可以满足我们处理所以Number类型的List的需求。
既然有extends,那么自然就会有super
public static void processNumber(List<? super AAA> numbers)
该方法可以处理子元素是AAA父类的集合
关于泛型中extends和super的用法,书中还提到了PECS法则,也即producer-extends,consumer-super,有兴趣的同学可以去看看。
《Effective Java》指出,如果使用通配符类型作为返回类型,那么就会强制调用者在调用时使用通配符类型, 而通配符类型对于调用者来说应该是无形的, 通配符类型的作用是使方法能够接受它们应该接受的参数,拒绝应该拒绝的参数。
泛型擦除
简单说就是,List<String> 在运行时会被擦除为List
为什么要使用泛型? 1、类型安全 2、提高API的通用性 这两点,分别对应的文章开头提出的两个问题。
《Effective Java》第二版