泛型是Java的一种机制。
但设计者为了向下兼容,出现了一些新的问题。
泛型利用参数化类型(参数化类型:将所操作的数据类型被指定为一个参数。例如定义方法时用形参,调用方法时用实参)使得编写的代码适用于广泛的类型。同时,在使用参数类型的时候,可以根据需要指定它使用的类型。
1. 泛型保证程序的类型安全并消除了一些烦琐的类型转换
在引入泛型之前,一般的程序都使用多态和继承来提高代码的灵活性和重用性。
List list1 = new ArrayList();
List list2 = new ArrayList();
list1.add(new Integer());
list2.add(new String("字符串"));
Integer i = (Integer)list1.get(0);
String s = (String)list2.get(0);
问题1:很多时候明明确定List中存放的类型,却仍然需要显示转换。
List integerList = new ArrayList();
integerList.add(new Integer(1));
integerList.add(new Integer(1));
...
integerList.add(new Integer(1));
for(Objct i:integerList){
System.out.println((Integer)i);
}
问题2:只允许存放Integer的List中添加字符串类型时,编译器并不会报错,这样会导致运行时从List中取出的元素无法转化为Integer类型并抛出异常。
List integerList = new ArrayList();
integerList.add(new Integer(1));
...
integerList.add(new String("失误"));
integerList.add(new Integer(1));
for(Objct i:integerList){
System.out.println((Integer)i);
}
泛型机制的设计很好地解决了上面这些问题。
2. 泛型使得Java编写的代码具有更加广泛的表达能力
Java语言是一种强类型语言。强类型语言通常是指在编译或运行时,数据的类型有较为严格的区分,不同数据类型的对象在转型时需要进行严格的兼容性检查。
Integer i;
String s = "i";
age = s;//在Java中是不合法的;但在Python中合法。
问题:强类型的语言对提高程序的健壮性,提高开发效率有利,但强类型语言导致一个问题:数据类型与算法在编译时绑定,这意味着必须为不同的数据类型编写相同的逻辑代码。
public Integer add(Integer a1, Integer a2) {
return a1+a2;
}
public Float add(Float a1, Float a2) {
return a1+a2;
}
public Double add(Double a1, Double a2) {
return a1+a2;
}
继承是解决这个问题的方法之一:为适用这一算法的多种数据类型抽象一个基类(或者接口),针对这个基类(或者接口)编写算法。但在实际程序设计过程中,专门为特定算法修改数据类型的设计,这不是个好习惯。另外,如果过使用已经设计好的数据类型,问题就无法解决。
泛型是解决这一问题的有效方法之一。泛型的最大价值在于:在保证类型安全的前提下,把算法与数据类型解耦。
在介绍之前,需要了解泛型字母的含义:
字母 | 意义 |
---|---|
E-Element | 在集合中的元素 |
T-Type | Java类 |
K-Key | 键 |
V-Value | 值 |
N-Number | 数值类型 |
定义方法
修饰符 class 类名<代表泛型的变量> {}
//单个
public class Person<T> {}
//多个,类里面不包含Person中的T
public class Teacher<V, S> extends Person {}
//多个,类里面包含Person中的T
public class Teacher<T, S> extends Person<T> {}
使用方法
Person<Integer> p = new Person<>(1);
定义
修饰符 <泛型> 返回值类型方法名(参数列表(使用泛型)){方法体;}
//单个
public <T> void method1(T t) {}
//多个
public <K,V> void method2(K k, V v) {}
//静态
public static <T> void method3(T t) {}
使用
method1("1");
method3("1");
method2("1", 1);
定义
修饰符 interface 接口名<代表泛型的变量> {}
public interface GenericInterface<E> {
public abstract void add(E e);
public abstract E getE();
}
使用
//方法一
public class GenericInterfaceImpl01 implements GenericInterface<String>{
@Override
public void add(String s) {}
@Override
public String getE() {return null;}
}
//方法二
public class GenericInterfaceImpl02<E> implements GenericInterface<E> {
@Override
public void add(E e) {}
@Override
public E getE() {return null;}
}
泛型的作用之一在于:使得Java编写的代码具有更加广泛的表达能力。意思就是,对于同样的算法逻辑,我仅仅编写一个方法即可。
但是存在一个问题,若方法体内传入的参数是泛型类,则这个作用就无法得到实现。
public static void printArray(ArrayList<Number> list) {}
public static void testArray(){
ArrayList<Number> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
ArrayList<String> list3 = new ArrayList<>();
printArray(list1);
printArray(list2);//不合法
printArray(list3);//不合法
}
Java设计者为此添加了通配符类型。它可以表示任何类型,通配符类型的符号是“?”。
public static void printArray(ArrayList<?> list) {}
public static void testArray(){
ArrayList<Number> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
ArrayList<String> list3 = new ArrayList<>();
printArray(list1);
printArray(list2);
printArray(list3);
}
边界是指为某一区域划定一个界限,在界限内是允许的,超出了界限就不合法。Java的泛型边界是指为泛型参数指定范围,在范围内可以合法访问,超出这个边界就是非法访问。
Java泛型系统允许使用类型通配符上限(extends)和通配符下限(super)关键字设置边界。extends仅允许泛型值为其本身或其子类;super仅允许泛型值为其本身或其父类。(实际上类似于判断放入的类是否满足规定,True就允许,然后将类型隐式转化为规定类;False就警告)
设置含边界的泛型类是为了该类为某些特定的类使用。
public class NumberFactory<T extends Number> {
public void printNumberClass(Class<T> clazz) {}
}
public class Test {
public static void main(String[] args) {
NumberFactory<Integer> integerFactory = new NumberFactory<>();
integerFactory.printNumberClass(Integer.class);
NumberFactory<String> stringFactory = new NumberFactory<>();//不合法
}
}
设置含边界泛型方法是为了该方法为某些特定的类使用。与上同理,这里不再举例。
通配符与边界使用表示传入的参数类型需要满足特定的规范。
public static void getElement1(Collection<? extends Number> coll) {}
public static void getElement2(Collection<? super Number> collection) {}
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
/*Collection<? extends Number> coll*/
getElement1(list1);
getElement1(list2);//不合法
getElement1(list3);
getElement1(list4);//不合法
/*Collection<? super Number> collection*/
getElement2(list1);//不合法
getElement2(list2);//不合法
getElement2(list3);
getElement2(list4);
警告:泛型与继承存在误区。泛型的类型设定不满足继承关系,而放入类型满足继承关系。
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
numberList = integerList;//不合法
numberList.add(new Integer(1));//合法
//通配符和上下界结合作用:
List<? extends Number> numberList = new ArrayList<Number>();
List<? extends Number> integerList = new ArrayList<Integer>();
numberList = integerList;//合法
泛型擦除是指泛型代码在编译后,都会被擦除成原生类型。
List<Integer> integerList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
integerList.getClass().equals(stringList.getClass());//true
虽然是同样的编译结果,但是Java的编译器在类型参数擦除之前对泛型的类型进行了安全验证,以确保泛型的类型安全。例如:
List<Integer> integerList = new ArrayList<>();
integerList.add("str");//不合法
擦除并不是一种语言特性,而是Java泛型实现的一种折中办法。因为泛型在JDK5之后才是Java的组成部分,因此这种折中是必需的。擦除的核心动机是使得泛化的代码可以使用非泛化的类库,反之亦然,这称为“迁移性兼容”。
因此Java泛型必须支持向后兼容性,即现有的代码和类库依然合法,而且与在没有泛型的JDK上运行效果一致。为了让非泛型代码和泛型代码共存,擦除成为一种可靠实用的方法。
擦除的实质是在编译过程中:
例如:
public class Zoo<T extends Animal> {
private T t;
public Zoo(T t) {
this.t = t;
}
public T pop() {
return this.t;
}
}
经过编译后:
public class Zoo {//Zoo<T extends Animal>
public Zoo(Animal t) {//T
this.t = t;
}
public Animal pop() {//T
return t;
}
private Animal t;//T
}
对于单边界的泛型,擦除时使用其上界替换参数类型。对于多边界的泛型擦除,则与边界声明的顺序有关,Java编译器将选择排在前面的边界进行参数替换。例如:
public class Zoo<T extends Flyable&Speakable> {
private T t;
public Zoo(T t) {
this.t = t;
}
public T pop() {
return this.t;
}
}
经编译后:
public class Zoo {
public Zoo(Flyable t) {
this.t = t;
}
public Flyable pop() {
return t;
}
private Flyable t;
}
交换Flyable和Speakable后编译代码为:
public class Zoo {
public Zoo(Speakable t) {
this.t = t;
}
public Speakable pop() {
return t;
}
private Speakable t;
}
擦除机制使得泛型类在运行时丢失了泛型信息,因此一些被认为时理所当然的功能,在Java泛型系统中并得不到支持。
1. 类型参数的实例化
public class Zoo<T> {
private T t = new T();//不合法
}
为了保证类型安全,Java并不允许使用类型参数类实例化对象。因为运行时参数类型信息被擦除,无法确定类型参数T所代表的具体类型,拥有无参的构造函数,甚至T所代表的具体类型可能不能被实例化,例如抽象类。
2. instanceof判断类型
Zoo<Bird> birdZoo = new Zoo<bird>();
if(birdZoo instanceof Zoo<Bird>) {}
代码看上去没有问题,但是JVM总是提示Cannot perform instanceof check against parameterized tpye Z, Use instead its raw form Zoo since generic tpye information will be erased at runtime错误,说明instanceof不能用于参数化类型的判断上,建议使用原生类型,因为类型信息将在运行时被擦除。
对于一个泛型类来说,即使其参数类型有多种不同,但在运行时他们都共享着一个原生对象。
3. 抛出或捕获参数类型信息
在Java异常系统中,泛型类对象时不能被抛出或捕获的,因为泛型类是不能继承实现Throwable接口及其子类的。
public class GenericException<T> extends Exception {}//不合法
泛型的擦除有可能导致与多态发生冲突。例如:
public class Animal<T> {
public void set(T t) {}
}
public class Bird extends Animal<String> {
public void set(String name) {
super.set(name);
}
}
public class GenericTest {
public static void main(String[] args) {
Bird bird = new Bird();
Animal<String> animal = bird;
animal.set("Bird");
}
}
由于Java的方法调用采用的时动态绑定的方式,所以呈现出多样性。但擦除导致了一个问题:由于擦除的原因,泛型方法set(T t)被擦除成set(Object t),而在子类中存在方法set(String name),我们本意时让子类的set方法覆盖父类的set方法,但擦除致使他们成了两个不同的方法,类型擦除与多态产生了冲突。
为了解决这个问题,Java编译器会在子类中生成一个桥的方法。例如,上面的Bird类经过编译后:
public class Bird extends Animal {
public Bird() {}
public void set(String name) {
super.set(name);
}
public volatile void set(Object obj) {
set((String) obj);
}
}
在父类引用animal调用set方法时,因为父类的set被重写为桥方法,JVM会首先调用桥方法。然后由桥方法调用子类Bird的set(String name)方法。除了传入参数的多态冲突,同时还存在另外一种获取参数的多态冲突:
public class Animal<T> {
public T get() {return null;}
}
public class Bird extends Animal<String> {
public String get() {return null;}
}
public class GenericTest {
public static void main(String[] args) {
Bird bird = new Bird();
Animal<String> animal = bird;
animal.get();
}
}
Bird类编译后的代码为:
public class Bird extends Animal {
public Bird() {}
public String get() {return null;}
public volatile Object get() {
return get();
}
}
因为擦除会导致运行时类型信息的丧失,JVM为了运行Java时保证类型安全和取消不必要的类型转换,把类型安全控制和类型转换放到了编译时进行。例如:
List<Integer> list = new ArrayList<>();
list.add(3);
Integer i = list.get(0);
编译后:
List list = new ArrayList();
list.add(Integer.valueOf(3));
Integer i = (Integer) list.get(0);
经过翻译后,从list取出元素时,编译器自动增加了转型代码,这就是在使用list时,无需手动转型的原因。可以推断在编译的时候,编译器也做了其他泛型操作以确保类型安全。
Java中不能声明泛型类数组,例如:
List<Integer>[] list = new ArrayList<Integer>[2];
该代码编译时时无法通过的,因为擦除后List会失去泛型的特性变成List[]。
排除使Java程序在运行时丧失了类型变量的具体信息,因此在使用泛型时要牢记一下内容: