Java-泛型和通配符

leard 发布于 2024-05-16 2 次阅读


泛型

泛型的作用

Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Persion> persons = new ArrayList<Persion>();这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。

泛型的使用方式

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

泛型类

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
//实例化泛型类
Generic<Integer> genericInteger = new Generic<Integer>(123);

泛型接口

public interface Generator<T> {
    public T method();
}

//实现泛型接口,不指定类型
class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method(){
        return null;
    }
}
//实现泛型接口,指定类型
class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method(){
        return "";
    }
}

泛型方法

public static < E > void printArray(E[] inputArray){

    for (E element : inputArray){
        System.out.printf("%s ", element);
    }

    System.out.println();
}

//使用
//创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = {1, 2, 3};
String[] stringArray = {"Hello", "World"};
printArray(intArray);
printArray(stringArray);

泛型擦除机制

Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除。

编译器会在编译期间会动态地将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx 。

因此,泛型本质上其实还是编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过擦除将泛型类转化为一般类。

List<Integer> list = new ArrayList<>();
list.add(12);
//1.编译期间直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//2.运行期间通过反射添加,是可以的
add.invoke(list, "kl");
System.out.println(list);

为什么不能用 Object 代替

  • 使用泛型可在编译期间进行类型检测。
  • 使用 Object 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。
  • 泛型可以使用自限定类型如 T extends Comparable。

桥方法(Bridge Method)

桥方法(Bridge Method) 用于继承泛型类时保证多态。桥方法为编译器自动生成,非手写。

class Node<T> {
    public T data;

    public Node(T data) {
        this.data = data; 
    }

    public void setData(T data) {

        System.out.println("Node.setData");
        this.data \= data;
    }
}

class MyNode extends Node<Integer> {

    public MyNode(Integer data) {
        super(data);
    }

    //Node<T> 泛型擦除后为 setData(Object data),
    //而子类 MyNode 中并没有重写该方法,所以编译器会加入该桥方法保证多态
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
   }
}

泛型的限制

泛型的限制一般是由泛型擦除机制导致的。擦除为 Object 后无法进行类型判断

  • 只能声明不能实例化 T 类型变量。
  • 泛型参数不能是基本类型。因为基本类型不是 Object 子类,应该用基本类型对应的引用类型代替。
  • 不能实例化泛型参数的数组。擦除后为 Object 后无法进行类型判断。
  • 不能实例化泛型数组。
  • 泛型无法使用 Instance of 和 getClass() 进行类型判断。
  • 不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突
  • 不能使用 static 修饰泛型变量

通配符 ?

通配符?的作用

泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了!通配符可以允许类型参数变化,用来解决泛型无法协变的问题。

//限制类型为 Person 的子类
<? extends Person>
//限制类型为 Manager 的父类
<? super Manager>

通配符 ?和常用的泛型 T 之间的区别

  • T 可以用于声明变量或常量而 ? 不行。
  • T 一般用于声明泛型类或方法,通配符 ? 一般用于泛型方法的调用代码和形参。
  • T 在编译期会被擦除为限定类型或 Object,通配符用于捕获具体类型。

无界通配符

无界通配符可以接收任何泛型类型数据,用于实现不依赖于具体类型参数的简单方法,可以捕获参数类型并交由泛型方法进行处理。

void testMethod(Person<?> p) {
    //泛型方法自行处理
}

List<?> 和 List 有区别

  • List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。因此,我们添加元素进去的时候会报错。
  • List list 表示 list 是持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。

上边界通配符和下边界通配符

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

上边界通配符 extends 可以实现泛型的向上转型即传入的类型实参必须是指定类型的子类型。

//限制必须是 Person 类的子类
<? extends Person>

下边界通配符 super 与上边界通配符 extends刚好相反,它可以实现泛型的向下转型即传入的类型实参必须是指定类型的父类型。

//限制必须是 Employee 类的父类
List<? super Employee>

? extends xxx 和 ? super xxx 的区别

两者接收参数的范围不同。并且,使用 ? extends xxx 声明的泛型参数只能调用 get() 方法返回 xxx 类型,调用 set() 报错。使用 ? super xxx 声明的泛型参数只能调用 set() 方法接收 xxx 类型,调用 get() 报错。

T extends xxx 和 ? extends xxx 的区别?

T extends xxx 用于定义泛型类和方法,擦除后为 xxx 类型, ? extends xxx 用于声明方法形参,接收 xxx 和其子类型。

Class<?> 和 Class 的区别

直接使用 Class 的话会有一个类型警告,使用 Class<?> 则没有,因为 Class 是一个泛型类,接收原生类型会产生警告。