Java - 泛型 < >

泛型

用于限定实例持有的数据类型,在类/接口内部使用统一的数据类型,也保证了容器类写入的数据类型单一化,避免运行时的类型转换异常,传入其他数据类型在编译时期就会报错提示。

类型参数

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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值