泛型
泛型概述
(有错误或者不明白的地方请指出,多沟通,谢谢。)
从Java 5开始,泛型出现在了Java中。在泛型出现之前,我们可能并不能完全的知道一些方法接收什么类型的参数、以及每个集合存储什么样的类型等等,那么就会经常在运行的时候,出现ClassCastException。有了泛型之后,编译器就得到了每个泛型对象会接收哪些类型的信息,当在编译的时候,自动为我们的插入进行转换,如果插入了错误的类型,那么就会报错,相当于将错误从运行提前到了编译期,使程序更加安全。
定义
具有一个或多个类型参数的类或者接口,就是泛型类或者接口。泛型类或者接口,统称为泛型。泛型告诉编译器每个容器中可以接收哪些对象类型,并在编译期自动进行插入和转换,如果出现类型转换错误,那么在编译期就会报错,防止运行时候报错。
泛型中常见术语
原生态类型
每一种泛型都定义了一个原生态类型,即不带任何实际类型参数的泛型名称。例:List<E> -> List,List即为List<E>的原生态类型。
类型参数
(1) 形式类型参数
在方法调用过程中,我们希望方法的适应性更加广泛,因此可以通过形式类型参数来告诉编译器参数类型或者参数的限制范围。类型参数一般指形式类型参数。
- E
- 有限制类型参数,例:E extends Number, E super Number
- 递归类型限制,例:<E extends Comparable<T>>,T extends Comparable<? super T>,关于这部分的讲解在下面的其他-递归类型中。
(2) 实际类型参数
- String
- 通配符。无限制通配符类型,?,可以接收任意实际类型参数的参数化类型;有限制通配符类型,? extends Number,? super Number,关于这部分的讲解在下面的其他-通配符中。
参数化类型
每一种泛型都对应着一组参数化类型,比如List<E>对应的参数化类型有 List<Object>,List<String>等等。
通配符类型
通配符类型分为无限制通配符类型(如List<?>)和有限制通配符类型(如List<? extends Number>)。
泛型方法
在方法的修饰符和返回类型之间添加类型参数的声明列表,然后可以在返回值、参数中使用该泛型。
泛型擦除
泛型擦除是指在编译后的class文件中,泛型信息被清除,变为原生态类型。List<String> 和List<Integer>为同一个类。如果没有指定上限,那么类型为Object,如果指定了类型参数上限,类型参数为上限类型。
类型令牌
例:String.class
堆污染
一个泛型变量赋值给不是泛型的变量,这种错误在编译器会被编译器警告,但可以忽略,运行时可能会报错。
原生态类型
原生态类型缺陷
原生态类型的使用,主要是针对Java 5以前的代码,是为了代码的移植兼容性而保留的。在实际的使用中,不正确的使用原生态类型可能导致ClassCastException异常问题。
如下面代码中,add方法中List参数为原生态类型,可以接收任意类型的List参数,执行代码后报“java.lang.ClassCastException”。因此要注意在代码中尽量不使用原生态类型,因为这样就失去了泛型在安全性和描述性方面的优势。
package testResource;
import java.util.ArrayList;
import java.util.List;
public class TestMain {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
add(list, 1);
System.out.println(list.get(0));
}
public static void add(List list, Integer i) {
list.add(i);
}
}
原生态类型与参数化类型
-
原生态类型(如List)逃避过了编译器的泛型检查,编译器将检查代码安全性的责任交给了开发者;而参数化类型(如List<Object>)则是由编译器对代码进行检查,由编译器保证代码运行的安全性。两种使用都可以放入任意对象,但是我们在开发的时候应该尽可能使用后者,减少代码运行异常。
-
原生态类型作为方法参数时,可以接收任意参数化类型的实际参数,如上面例子中,add方法可以接收List<Object>、List<String> 等List的所有参数化类型,并且编译不会报错,但是这样的缺陷就是我们失去了方法中对于List中存放类型的控制,如果放入了错误的类型,那么就可能导致运行时异常。而add(List<Object>)则只能接收List<Object> 类型的参数,如果给它传递List<String>时,则会出现编译异常,例子如下
package testResource; import java.util.ArrayList; import java.util.List; public class TestMain { public static void main(String[] args) { List<String> list = new ArrayList<>(); add(list, "asd"); // The method add(List<Object>, Object) in the type TestMain is not applicable for the arguments (List<String>, String) } public static void add(List<Object> list, Object add) { list.add(add); } }
必须使用原生态类型的情况
- 必须在类文字中使用原生态类型,如List.class,String[].class等,不能出现List<String>.class,List<?>.class等。
- 使用instanceof方法的时候
泛型和数组
在《Effective Java》中,泛型部分花了很多篇幅去讲述数组和泛型应用部分应该注意的内容,比如为什么不能创建泛型数组,怎么去使用泛型数组,使用泛型数组应该注意的事项,可变参数等。在Java中,数组和泛型部分目前结合的不是很好,因为数组和泛型设计的使用目的不同。
泛型和数组的不同点
-
数组是协变的,泛型是不变的。
可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量。可变性包含协变、逆变、不变。协变:当A≤B时有f(A)≤f(B)成立,如果≤代表继承关系,A是B的子类,A[]是B[]的子类。逆变:当A≤B时有f(B)≤f(A)成立。不变:当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
在代码中表现为Object[] arr = new String[] { "123", "asd" }; List<Object> list = new ArrayList<String>(); // Type mismatch: cannot convert from ArrayList<String> to List<Object>
第一行编译通过,第二行报错。因此,List<String>并不是List<Object>的子类,List<Object>并不能接收类型为List<String>等子类型的变量。如果List<Object> = List<String>可以通过,那么List<Object>中也可以放入Integer类型的对象,那么就失去了泛型的类型安全性,因此从一开始就禁止了这种操作。而目前数组还存在下面问题
package testResource;
public class TestMain {
public static void main(String[] args) {
String[] arr = new String[] { "asd", "zxc" };
test(arr); // java.lang.ArrayStoreException
System.out.println(arr[0]);
}
public static void test(Object[] arr) {
arr[0] = 1;
}
}
-
数组是具体化的,泛型是可擦除的。
数组是具体化(reifiable)的,数组会在运行时知道和强化它们的元素类型。泛型是不可具体化的(non-reifiable),经过了擦除,只有在编译的时候知道它们的类型信息,在运行时不知道它们的类型信息,因此运行时包含的信息要比编译时少,如下面例子
,编译出现异常,这是因为经过了类型擦出后,类中会出现两个test(List)方法。public static void test(List<Object> list) { } // rasure of method test(List<Object>) is the same as another method in type TestMain public static void test(List<String> list) { }
泛型与数组
在Java中是禁止创建泛型数组的,因为可能会出现类型安全问题,假设Object[] arr = new List<String>[3]通过,那么其实已经失去了泛型对类型限制的保证,因此这是被禁止的;最好的办法就是无论任何时候,都尽量使用集合而不是使用泛型数组,下面有一个例子
public static <T> T[] test(List<T> list) {
return (T[]) list.toArray();
} // java.lang.ClassCastException
//经过泛型擦除
public static Object[] test(List list) {
return (Object)list.toArray();
}
由此可以看见,泛型与数组的结合还是比较困难的。有两中方法可以将泛型与数组结合使用,第一种方法是通过强转创建泛型数组,第二种方法是通过强转单个对象,可以参考ArrayList。
-
方法一
package testResource;import java.util.List; public class Container<T> { private T[] arr; public Container(List<T> list) { this.arr = (T[]) list.toArray(); } public T getObject() { return arr[0]; } public T[] getArr() { return arr; } } package testResource; import java.util.ArrayList; import java.util.List; public class TestMain { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("asd"); Container<String> c = new Container<>(list); String s = c.getObject(); System.out.println(s); String[] arr = c.getArr(); System.out.println(arr[0]); } }
main方法调用中,第一个输出为"asd",第二个报class转换异常。我们通过泛型擦除可以知道,其实Container中是一个Object[]数组,而这个数组是不能强转为String数组的,因此报错。在这个Container中,我们改进方法最好为将T[]使用List<T>,这样就可以通过编译器保证类型安全,要么就将T[]变为私有变量,禁止外部访问,不然就容易出现异常。
-
方法二
package testResource;import java.util.ArrayList; import java.util.List; public class TestMain { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("asd"); Container<String> c = new Container<>(list); String s = c.getObject(); System.out.println(s); } } package testResource; import java.util.List; public class Container<T> { private Object[] arr; public Container(List<T> list) { this.arr = list.toArray(); } public T getObject() { return (T) arr[0]; } }
这种方式在集合底层代码中经常使用,比如ArrayList
通配符的使用
在前面可变性的例子中,我们知道泛型是不变的,List<Object>和List<String>并不是父子类的关系,如果要打破这个限制,希望在方法传参的时候,既可以传List<Object>,也可以传List<String>,那么就需要使用通配符。
先看下面两个方法,两个方法也都可以接收任意类型List,但是泛型List<T>可以放入任何T类型的对象,也可以输出任何T类型的对象,而通配符?相当于 ? extends Object,它不能放入除null之外的任何对象,只能输出Object类型的对象。将?extends Object进行下修改,List<? extends Number>,这个限制通配符类型便能够接收任意Number及Number子类修饰的List,并且它输出的每个对象都为它们的共同基类Number,但是由于我们不知道它们到底是哪个子类型,因此我们不能往这个List中放入任意对象。于此相反,List<? super Number>,它可以接收任意Number及Number的父类修饰的List,由于它们的下限为Number,因此我们不确定List中存放的到底是什么类型,它不能输出对象,但是它可以接收任意Number及Number子类对象。
public static <T> T getFirst1(List<T> list) {
return list.get(0);
}
public static Object getFirst2(List<?> list) {
return list.get(0);
}
这里有一个PECS(Producer Extends Consumer Super)原则。输出=extends,消费=super
<T extends Comparable<T>> 和 <T extends Comparable<? super T>>
《Effective Java》泛型部分比较难懂的另一部分就是这里了。这两个都属于递归类型,解读如下。
-
<T extends Comparable<T>>,这里的T限制为必须实现Comparable接口,并且这个接口有一个方法compareTo接收的参数必须为T类型。
package testResource;public class People implements Comparable<People> { private Integer age; public People(Integer age) { super(); this.age = age; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public int compareTo(People o) { return this.age - o.getAge(); } } package testResource; public class Student extends People { public Student(Integer age) { super(age); } } package testResource; import java.util.ArrayList; import java.util.List; public class TestMain { public static void main(String[] args) { List<People> list = new ArrayList<>(); list.add(new People(25)); list.add(new Student(20)); System.out.println(max(list).getAge()); List<Student> stuList = new ArrayList<>(); stuList.add(new Student(20)); System.out.println(max(stuList)); // The method max(List<T>) in the type TestMain is not applicable for the arguments (List<Student>) } public static <T extends Comparable<T>> T max(List<T> list) { return list.get(0); } }
这个main方法中,第一个编译和输出都没有问题,第二个编译报错。关于这里的部分,书上解释比较少,个人进行了自己的理解。Student初始化的时候,优先初始化父类People,因为People实现了Comparable接口,那么这里的compareTo方法实际参数为Comparable<People>,我们并不能在这里面传Comparable<Student>对象。
-
<T extends Comparable<? super T>
public static <T extends Comparable<? super T>> T max(List<T> list) { return list.get(0); }
修改为这个方法后,main方法可以完美执行。解读为,这里的compareto 方法接收的参数为 Comparable<? super People>,根据通配规则,那么这里可以接收Comparable<People>以及Comparable<Student>等。