10 - Java泛型
泛型是一种“代码模板”,可以用一套代码套用各种类型。
1. 泛型
Java标准库提供的ArrayList可以看作“可变长度”的数组,实际上它内部就是一个Object[]数组,配合存储一个当前分配的长度,就可以充当“可变数组”。如果用ArrayList存储String类型,会有以下缺点:①需要强制转换;②不方便,易出错。如:
ArrayList list = new ArrayList(); list.add("Hello!"); String first = (String) list.get(0); // 强制转换 list.add(new Integer(123)); String second = (String) list.get(1); // 抛出java.lang.ClassCastException异常
对此,我们可以为String单独编写一个StringArrayList,存入的必须是String,取出的也一定是String,不需要强制转换,因为编译器会强制检查放入的类型。然而,对于其他所有的class,如Integer、Long、Double、Person等,我们都要为其单独编写一种ArrayList,JDK的class就有上千个,加上其他人编写的class,这种方法是不可行的。为此,我们需要把ArrayList变成一种模板:ArrayList<T>,T可以是任何class。编写一次模板,就可以创建任意类型的ArrayList。
既实现了编写一次,万能匹配,又通过编译器保证了类型安全,就是泛型。
向上转型
在Java标准库中ArrayList<T>实现了List<T>接口,它可以向上转型为List<T>(T不能改变):
public class ArrayList<T> implements List<T>{ …… } List<String> list = new ArrayList<String>();
注意:ArrayList<Integer>和ArrayList<Number>两者完全没有继承关系,不能把ArrayList<Integer>向上转型为ArrayList<Number>(T不能变成父类)。
2. 使用泛型
使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object。此时,只能把<T>当作Object使用,没有发挥泛型的优势。当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>;当我们定义泛型类型<Number>后,List<T>的泛型接口变为强类型List<Number>。编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型,如:
List<Number> list = new ArrayList<Number>(); // 省略后面的泛型类型 List<Number> list = new ArrayList<>();
泛型接口
除了ArrayList使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable这个泛型接口。
3. 编写泛型
步骤:
①按照某种类型编写类,如String;
②标记所有的特定类型(String);
③把特定类型String替换为T,并申明<T>。
静态方法
编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。对于静态方法,可以单独改写成“泛型”方法,只需要使用另一个类型(如<K>)即可。这样才能清楚地将静态方法的泛型类型和实力类型的泛型类型区分开。
多个泛型类型
泛型还可以定义多种类型。Java标准库的Map<K, V>就是使用两种泛型类型的例子,它对Key使用一种类型,对Value使用另一种类型。
4. 擦拭法
泛型是一种类似“代码模板”的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)。擦拭法是指虚拟机对泛型其实一无所知,所有的工作都是编译器做的。Java使用擦拭法实现泛型,导致①编译器把类型<T>视为Object;②编译器根据<T>实现安全的强制转型。
Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全的强制转型。
Java泛型的局限性:
-
<T>不能是基本类型,如int,因为实际类型是Object,Object类型无法持有基本类型;
-
无法取得带泛型的Class。因为T是Object,我们对Pair<String>和Pair<Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>。
-
无法判断带泛型的Class。原因与第二点一样,并不存在Pair<String>.class,而是只有唯一的Pair.class。
-
不能实例化T类型。要实例化T类型,必须借助额外的Class<T>参数。
不恰当的覆写方法
编译器会阻止一个实际上会变成覆写的泛型方法定义。
泛型继承
一个类可继承自一个泛型类。在继承了泛型类型的情况下,子类可以获取父类的泛型类型。
5. extends通配符
上界通配符:如使用<? extends Number>的泛型定义,把泛型类型T的上界限定在Number了。
通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number类型给setFirst(? extends Number)。
使用类似<? extends Number>通配符作为方法参数时表示:
-
方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
-
方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。
extends通配符的作用
是一个对参数List<? extends Integer>进行只读的方法(恶意调用set(null)除外)。
使用<? extends Integer>通配符表示:
-
允许调用get()方法获取Integer的引用;
-
不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。
使用extends限定T类型
如使用类似<T extends Number>定义泛型类时表示:泛型类型限定为Number以及Number的子类。
6. super通配符
void set(Pair<? super Integer> p, Integer first, Integer last){ p.setFirst(first); p.setLast(last); }
Pair<? super Integer>的set方法:方法参数接收所有泛型类型为Integer或Integer父类的Pair类型。
Pair<? super Integer>的get方法:唯一可以接收getFirst()方法返回值的是Object类型。
使用<? super Integer>通配符表示:
-
允许调用set(? super Integer)方法传入Integer的引用;
-
不允许调用get()方法获得Integer的引用。唯一例外的是可以获取Object的引用(Object o = p.getFirst();)。
即使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。
对比extends和super通配符
作为方法参数,<? extends T>类型和<? super T>类型的区别在于:
-
<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
-
<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
PECS原则
Producer Extends Consumer Super
如果需要返回T,它是生产者,要使用extends通配符;如果要写入T,它是消费者,要使用super通配符。
实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:
void sample(Pair<?> p){ }
不允许调用set(T)方法并传入引用(null除外);
-
不允许调用T get()方法并获取T引用(只能获取Object引用)。
即既不能读,也不能写。
<?>通配符有一个独特的特点:Pair<?>是所有Pair<T>的超类。从Pair<Integer>是Pair<?>的子类,可以安全地向上转型。
7. 泛型和反射
Java的部分反射API也是泛型,如Class<T>。