写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。
目录
一、泛型入门
1、泛型基本介绍
2、泛型的作用
(1)提供安全保证
(2)简化代码
二、和的区别
1、声明泛型类和泛型方法
2、使用泛型类和泛型方法
3、需要注意的点
三、泛型的上下限
1、下限
2、上限
3、上下限引起的问题
(1)上界不能往里存,只能往外取
(2)下界不影响往里存,但往外取只能放在Object对象里
4、使用建议
四、泛型擦除
1、证明泛型擦除
2、泛型擦除的规则
(1)擦除类定义中的类型参数 - 无限制类型擦除
(2)擦除类定义中的类型参数 - 有限制类型擦除
(3)擦除方法定义中的类型参数
五、泛型在异常中的使用
1、不能抛出也不能捕获泛型类的对象
2、不能再catch子句中使用泛型变量
一、泛型入门
1、泛型基本介绍
泛型是JDK1.5版本引入的,以泛型类为例,申明泛型类时需带上参数类型T,成员变量
就可以被限制为T的类型。
class Example<T>{
private T variable;
public T getVariable() {
return variable;
}
public void setVariable(T variable) {
this.variable = variable;
}
}
不过需要注意的是,申明为泛型类,不代表新建对象时必须增加泛型限制。
Example<Integer> ex = new Example<Integer>();
Example ex2 = new Example();
2、泛型的作用
主要目的有2个
(1)提供安全保证
这里我们以集合为例,我们初始化一个ArrayList,并限制器内部元素为Integer,当我们add其他类型进入容器中时,编译器就会告诉我们这样做是不允许的。通过这种方式,我们可以确保容器中元素都属于同一个类,避免了get时异常的发生。
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add("abc"); // 编译错误
(2)简化代码
假设我们需要写一个两数相加的功能,如果没有泛型的话,代码需要进行多次重载
public int add(int x, int y) {
return x + y;
}
public float add(float x, float y) {
return x + y;
}
public double add(double x, double y) {
return x + y;
}
而有了泛型之后,我们可以简化为下面这样(T extends Number利用到了泛型的上下限,这里的作用是限制T的类必须是Number的子类),避免了代码的冗余。
public <T extends Number> double add(T x, T y) {
return x.doubleValue() + y.doubleValue();
}
二、<T>和<?>的区别
先说结论:
<T>适用于声明一个泛型类或泛型方法。
<?>适用于使用泛型类或泛型方法。
下面我们来详细解释下:
1、声明泛型类和泛型方法
参数类型<T>可以申明泛型类,无界通配符<?>则不行
class Example<T>
{
private T variable1;
private T variable2;
public <T> T Function(T x){
return null;
}
}
class Example<?>
{
private ? variable1; // 编译不通过
private ? variable2; // 编译不通过
public <?> ? Function(? x){ // 编译不通过
return null;
}
}
2、使用泛型类和泛型方法
这里和方面就反过来了,申明引用时不能使用<T>,而无界通配符<?>就可以
List<T> list1 = new ArrayList<String>(); // 编译不通过
List<?> list2 = new ArrayList<String>();
3、<?>需要注意的点
List<?>虽然可以通过编译,但是一般不推荐使用,因为编译器并不会像上面泛型擦除一样直接替换为Object,而是标上一个占位符capture#。原因在于编译器虽然知道这里要去匹配一个类型,但是却不知道具体要匹配哪个类型,于是干脆就都不允许。(这块内容请务必理解清晰,这会涉及下一节的上下限)
List<?> list2 = new ArrayList<String>();
list2.add("abc"); // 编译不通过
// The method add(capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (String)
list2.add(111); // 编译不通过
// The method add(int, capture#2-of ?) in the type List<capture#2-of ?> is not applicable for the arguments (int)
不过,虽然没办法直接往里面加入元素,但是我们可以直接把另一个引用的对象直接复制过来,并且能正常取出。
List<?> list2 = new ArrayList<String>();
List<String> list3 = new ArrayList<String>();
list3.add("abc");
list2=list3;
System.out.println(list2.get(0).getClass());
三、泛型的上下限
首先来看以下样例,参考自知乎上的一个回答《super和extend》
假设现在有如下继承关系
class Father{}
class Son extends Father{}
class Example<T>
{
private T var;
public Example(T t){
var=t;
}
public T getVar() {
return var;
}
public void setVar(T var) {
this.var = var;
}
}
按照我们的理解,父类引用可以指向子类对象,这是多态的基础,但是这个功能在泛型里就不适用了。
Example<Father> ex = new Example<Son>(new Son()); // 编译不通过
// Type mismatch: cannot convert from Example<Son> to Example<Father>
虽然Son IS-A Father
但是Example<Father> IS-NOT-A Example<Son>
虽然容器里的东西有继承关系,但容器之前并没有继承关系。为了解决这一问题,JDK引入了<? extends T>和<? super T>来处理这上面的问题。
1、下限
<? extends T> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
Example<? extends Father> ex = new Example<Son>(new Son());
2、上限
<? super T> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
Example<? super Father> ex = new Example<Object>();
3、上下限引起的问题
上下限虽然解决了容器中继承的问题,但也带来了新的问题
(1)上界<? extends T>不能往里存,只能往外取
Example<? extends Father> ex = new Example<Son>(new Son());
ex.setVar(new Son());
// The method setVar(capture#1-of ? extends Father) in the type Example<capture#1-of ? extends Father> is not applicable for the arguments (Son)
具体原因我们在第二节中已经解释过了,这里不再赘述。
(2)下界<? super T>不影响往里存,但往外取只能放在Object对象里
Example<? super Father> ex = new Example<Father>(new Father());
ex.setVar(new Father());
Father fa2 = ex.getVar(); // 编译不通过
// Type mismatch: cannot convert from capture#2-of ? super Father to Father
Object obj = ex.getVar();
虽然能取出,但是元素的类型信息会全部丢失。
4、使用建议
《Effictive Java》中建议为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
四、泛型擦除
java中的泛型是伪泛型,只在编译前起作用,编译时会被替换掉,即擦除。
1、证明泛型擦除
我们创建一个泛型类,然后将编译后的class文件利用javap -c class文件路径反编译查看
class Example<T>
{
private T variable;
public T getVariable() {
return variable;
}
public void setVariable(T variable) {
this.variable = variable;
}
}
我们可以看到,泛型T被替换为了Object,这里相当于允许任何Object的子类加入,原先的限制等于被取消了,这边是泛型擦除。
第一节中我们说过,泛型的作用之一就是限制参数类型,这里我们同样限定一个参数类型为Integer的ArrayList,当我们尝试将字符串加入其中时,编译器会告诉我们这样是不允许的。但这并不代表我们就没有办法把字符串加入进去。
上文中提到了,泛型限制在编译后就被擦除了,那么自然就想到了利用反射来将add方法延后到运行时执行,代码如下。
public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
// list.add("abc"); 编译错误
list.getClass().getMethod("add", Object.class).invoke(list, "abc");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
结果如下,至此,我们证明了泛型限制在编译后即不复存在。
1
abc
2、泛型擦除的规则
内容参考自《Java泛型的类型擦除》
(1)擦除类定义中的类型参数 - 无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T>和<?>的类型参数都被替换为Object。
(2)擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
(3)擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
五、泛型在异常中的使用
try-catch中catch块中如果有多个异常,必须由上到下满足继承关系。
1、不能抛出也不能捕获泛型类的对象
假设有如下自定义异常,注意,如下代码是无法通过编译的,这里只是假设成立来论证会产生的问题
public class MyException<T> extends Exception {
}
try{
} catch(MyException<A> e) {
} catch(MyException<B> e) {
}
A、B可以是任意类型,满足try-catch要求即A是B的子类即可。由第三节泛型擦除可以得出,在编译后,try-cathc块会变成如下代码
try{
} catch(MyException<Object> e1) {
} catch(MyException<Object> e2) {
}
类型信息被擦除后,那么两个地方的catch都变为原始类型Object,这样便违反了try-cathc的基本规则。
2、不能再catch子句中使用泛型变量
假设我们可以在cathc中使用泛型变量,在泛型擦除后,T会被替换为Throwable,和第一点一样,catch中的继承关系被打破,因此必然不被允许。
public <T extends Throwable> void myException(Class<T> t) {
try {
}
catch (T e) { // 编译错误
}
catch (IndexOutOfBounds e) {
}
}