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>
Java 中, `<T>` 是一种强大的特性,它允许在类、接口和方法中使用类参数,以实现代码的复用和类安全。以下是 `<T>` 的几种常见使用方法: #### 类是指在类的定义中使用参数。通过在类名后面添加 `<T>`,可以在类的内部使用这个类参数来定义成员变量、方法参数和返回值。 ```java class GenericClass<T> { private T value; public GenericClass(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } } ``` 使用类时,需要在创建对象时指定具体的类参数: ```java GenericClass<Integer> intObj = new GenericClass<>(10); Integer intValue = intObj.getValue(); GenericClass<String> strObj = new GenericClass<>("Hello"); String strValue = strObj.getValue(); ``` #### 方法 方法是指在方法的定义中使用参数。在方法的返回类之前添加 `<T>` 来声明这是一个方法,然后可以在方法的参数和返回值中使用这个类参数。 ```java public class GenericMethodExample { public <T> List<T> show(T[] arr) { ArrayList<T> list = new ArrayList<>(); for (T t : arr) { list.add(t); } return list; } } ``` 调用方法时,可以显式地指定类参数,也可以让编译器根据传入的参数自动推断类参数: ```java GenericMethodExample example = new GenericMethodExample(); Integer[] intArray = {1, 2, 3}; List<Integer> intList = example.show(intArray); String[] strArray = {"a", "b", "c"}; List<String> strList = example.show(strArray); ``` #### 接口 接口的定义与类类似,在接口名后面添加 `<T>` 来声明类参数。实现接口的类可以选择指定具体的类参数,也可以继续使用。 ```java interface GenericInterface<T> { T getValue(); } class GenericInterfaceImpl<T> implements GenericInterface<T> { private T value; public GenericInterfaceImpl(T value) { this.value = value; } @Override public T getValue() { return value; } } ``` 使用接口的实现类时,需要指定具体的类参数: ```java GenericInterface<String> strInterface = new GenericInterfaceImpl<>("Hello"); String strValue = strInterface.getValue(); ``` #### 通配符 通配符用于在中表示未知类。常见的通配符有 `?`、`? extends T` 和 `? super T`。 - `?`:表示未知类。 ```java public void printList(List<?> list) { for (Object obj : list) { System.out.println(obj); } } ``` - `? extends T`:表示类参数必须是 `T` 或 `T` 的子类。 ```java public void processList(List<? extends Number> list) { for (Number num : list) { System.out.println(num); } } ``` - `? super T`:表示类参数必须是 `T` 或 `T` 的父类。 ```java public void addToList(List<? super Integer> list) { list.add(10); } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值