这一次,让我们来好好聊聊Java泛型

本文结合《Effective Java》第五章泛型和自己的理解及实践,讲解了Java泛型的知识点。文章发布于专栏Effective Java,欢迎读者订阅。

你经常这样写代码吗

1、创建List时,删掉ide帮你自动生成的尖括号,然后发现编译器的警告,就按照ide的提示加上注解来消除警告

就像这样

@SuppressWarnings("unchecked")
Set result = new HashSet(s1);


2、写一个方法处理String类型的列表,入参是List<String>,然后下一次发现要处理Integer类型的列表,就再写一个方法,入参改为List<Integer>


如果你经常写这样的代码,那么,是时候好好学学Java泛型了。


什么叫泛型(generic)

声明中具有一个或者多个类型参数的类或者接口,就是泛型。

直观来看,我们常用的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 和 List<Object>的区别

这两种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》第二版






你可能感兴趣的:(java,泛型)