泛型
用于限定实例持有的数据类型,在类/接口内部使用统一的数据类型,也保证了容器类写入的数据类型单一化,避免运行时的类型转换异常,传入其他数据类型在编译时期就会报错提示。
类型参数
T 叫做类型参数,是一个占位符,实例化Demo的时候会被替换成具体的类型。方法的泛型可以单独定义,不一定要用包裹它的类/接口的。
//当类定义了泛型
class One<T>{
public void method0(String param){} //普通方法,没必要出现
public void method1(T param){} //使用类定义的泛型类型
public <R> void method2(R param){} //使用自己单独定义的泛型类型
public static <R> void method3(R param){} //静态方法必须声明自己的泛型,因为静态优先于实例加载
public void method5(Demo<T> param){} //形参为泛型类的时候,使用类定义的泛型类型
public <S> void method6(Demo<S> param){} //形参为泛型类的时候,使用自己定义的泛型类型
}
//当类未定义泛型
class Two{
public void method0(String param){} //普通方法
public void method1(T param){} //不存在这种东西,使用泛型见下一行
public <T> void method2(T param){} //使用自己单独定义的泛型类型
public static <R> void method3(R param){} //静态方法必须声明自己的泛型,因为静态优先于实例加载
public void method5(Demo<T> param){} //不存在这种东西,使用泛型见下一行
public <S> void method6(Demo<S> param){} //形参为泛型类的时候,使用自己定义的泛型类型
}
//使用
One<String> one1 = new One<String>(); //老式写法已淘汰
One<String> one2 = new One<>(); //推荐简写
One one3 = new One<String>(); //不是泛型,无法自行推导
类型擦除
在运行时,泛型信息会被擦除,因此无法检查传入对象的类型参数是什么(但是可以检查对象的类型)。这样的好处是兼容旧版本和节省内存,看似不安全,但在编译的时候需要指定泛型类型来确保只能传入对应类型的对象。
public <T> void demo(T param){
//无法检查对象的泛型类型
if(param instanceof List<String>){}
//可以检查对象是否是List类型
if(param instanceof List){}
if(param instanceof List<?>){}
}
子类和子类型
List<T>是个泛型类,那么List<Cat> 和 List<Animal> 正常情况下是没有关系的两个不同的数据类型(类型擦除保证了数据安全)。若Cat 是 Animal 的子类,使用类型参数约束或者通配符约束,变量和方法参数就获得了协变和逆变的支持,List<Cat>和 List<Animal> 便存在子父类型关系,可以相互赋值,但为了安全会有读写限制。
List<Animal> animal = new ArrayList<Animal>(); //正常创建,子类ArrayList对象赋值给父类List声明,是多态与泛型无关,把List改写成ArrayList一样
List<Animal> animal2 = new ArrayList<Cat>(); //报错,<Animal>和<Cat>是不同的两个数据类型,无法赋值
List<? extends Animal> animal3 = new ArrayList<Cat>(); //上限通配符解决了 子类类型 赋值给 父类类型 的问题
//数组可以,因为没有类型擦除
Animal[] animal4 = new Animal[10];
animal4[1] = new Cat();
类型参数 约束
上限定
可以传入T及其子类类型。
不同于通配符,可以用在类/接口:
多个上限定使用 & 符号连接。没有下限定。
//类、接口
class Demo<T extends Person>{} //正确的
class Demo<T extends Person & IEat & IRun>{} //多个上届约束用&连接
//方法
public <T extends Person> void method(T param){}
public <T extends Person> void method(Demo<T> param){} //当形参是泛型类的时候
通配符 约束
什么时候用什么,PECS:producer-extends,consumer-super。
协变covariance:上限定通配符 < ? extends T >
- 用在变量:该类型的引用声明可赋值为T及T的子类类型对象。该引用对象不能调用包含类型参数的方法,也不能给包含类型参数的字段赋值(除了赋值为null),只能用不能修改的意思。
- 用在方法:该类型的形参可赋值为T及T的子类类型实参。
class Animal {}
class Cat extends Animal {}
class Haha<T>{
T data;
public void add(T param){}
}
//只使用不修改,就可以使用上界限定,producer-extends
public void printIt(List<? extends Animal>){} //这样既能打印List<Animal>也能打印List<Cat>对象
//泛型直接使用是不支持协变的
Haha<Animal> haha = new Haha<Cat>(); //报错:T声明为<Animal>类型,赋值的是<Cat>类型
Haha<? extends Animal> aa = new Haha<Cat>(); //T的声明使用上界限定解决了子类类型赋值给父类类型的限制
//需要的是? extends Animal的class对象,而aa是Haha对象
aa.add(aa); //引用无法调用包含类型参数的方法
aa.data = aa; //引用无法给包含类型参数的字段赋值
对于集合类,无法往实例中写入数据,除了null,因为子类型繁多,无法确保写入的数据类型单一性,就失去了泛型的意义。但是不管里面全是什么类型的数据,都可以当作T读取。
List<Integer> num = new ArrayList<>();
List<? extends Number> list = num; //Integer是Number的子类型因此可以赋值
Integer i = 3;
Double d = 3.14;
list.add(i); //报错,连Integer也无法写入
list.add(d); //报错,Double是Number子类无法写入
逆变 contravariance:下限定通配符 < ? super T >
- 用在变量:该类型的引用声明可赋值为T及T的父类类型对象。该引用对象不能调用返回值包含类型参数的方法,也不能获取包含类型参数的字段值。
- 用在方法:该类型的形参可赋值为T及T的父类类型实参。
class Animal {}
class Cat extends Animal {}
class Haha<T>{
T data;
public T get(){return null;}
}
//只修改不使用,就可以使用下界限定,consumer-super
public void addIn(List<? super Cat> list){ list.add(new Animal()); }
????待补充
对于集合类,只能往实例中写入T及其子类类型数据,因为会向上转型为T类型。不能写入父类类型数据是因为?匹配所有T的父类,但是具体类型未知是不安全的,而且也不能向下转型为T类型,也就无法读取,也违背了数据类型单一性原则失去了泛型的意义。
List<Number> num = new ArrayList<>();
List<? super Number> list = num;
list.add(3); //int向上转型为Number类型
list.add(3.14); //float向上转型为Number类型
list.add(new Object()); //Object报错,无法写入父类类型数据
List<Object> list1 = new ArrayList<>();
list1.add(new Object());
list1.add(new Object());
List<? super Number> list2 = list1;
list2.add(123); //int向上转型为Number类型
list2.add(3.14); //float向上转型为Number类型
list2.add(new Object()); //Object报错,无法写入父类数据
list2.forEach(System.out::println); //赋值前容器中的写入的两个Object对象会被保留
unbounded wildcard:无限通配符 < ? >
没有上限定也没有下限定,相当于<? extends Object>,可以传入任意类型,但只能当做Object使用。
//等价写法
public <T> void method(Demo<T> param){}
public void method(Demo<?> param){} //推荐写法,更简洁易读
区别
T extends E 类型参数约束 | 可以用在类/接口/方法的声明中,且大括号内可以直接把T当作类型使用 |
? extends T 通配符约束 | 只能用在方法的声明中,无法使用在类/接口的声明,无法单独拿来当作类型声明使用 |
类型参数 | 对应单一的数据类型,定义数据类型的代码复用(T param) |
类型参数约束 通配符约束 | 对应范围的数据类型,定义使用对象类型的代码复用(Demo<? extends Person> param) |
<?>、<? extends T> | 用于实现更为灵活的读取,可以用类型参数的等价形式替代<T>、<T extends Person> |
<? super T> | 用于实现更为灵活的写入和比较,没有对应的类型参数形式 <T super Cat> |