35.Java 中的泛型是什么

泛型就是将类型参数化,在编译时再确定类型。可以实现在类,接口,方法中。

  • 泛型类

    • public class Box<T> {
          private T value;
      
          public void set(T value) {
              this.value = value;
          }
      
          public T get() {
              return value;
          }
      }
      
    • Box<String> stringBox = new Box<>();
      stringBox.set("Hello");
      String str = stringBox.get(); // 直接获取String类型,无需强制转换
      
      Box<Integer> intBox = new Box<>();
      intBox.set(123);
      int num = intBox.get(); // 直接获取Integer类型
      
  • 泛型接口

    • public interface Repository<T, ID> {
          T findById(ID id);
          void save(T entity);
      }
      
    • public class UserRepository implements Repository<User, Long> {
          @Override
          public User findById(Long id) {
              // 根据ID查找User
          }
      
          @Override
          public void save(User user) {
              // 保存User
          }
      }
      
  • 泛型方法

    • public class ArrayUtils {
          public static <T> void printArray(T[] array) {
              for (T element : array) {
                  System.out.println(element);
              }
          }
      }
      
    • String[] names = {"Tom", "Jerry"};
      ArrayUtils.printArray(names); // 处理String数组
      
      Integer[] numbers = {1, 2, 3};
      ArrayUtils.printArray(numbers); // 处理Integer数组
      

使用泛型的好处是什么

在java1.4版本,是没有泛型概念的;只能用Object来实现。Object有两个缺点

  1. 每次使用的时候必须要进行强制转换
  2. 在编译时编译器不知道类型转换是否正常,在运行时就会报出异常

sun公司为了Java更加安全,引入了泛型

  1. 类型安全
    • 在编译的时候就可以检查出类型转换的异常
  2. 消除强制类型转换
    • 使用的时候可以直接得到想要的数据类型
  3. 提升性能
    • 在编译时直接完成

泛型是一种语法糖,基本原理就是进行类型擦除;在编译之后,就会擦去类型参数。

public class Caculate<T> {
    private T num;
}

我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。反编译一下这个 Caculate 类:

public class Caculate{
    public Caculate(){}
    private Object num;
}

发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。

那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:

public class Caculate<T extends String> {
    private T num;
}

这种情况的泛型类型,num 会被替换为 String 而不再是 Object。这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用String 进行类型擦除。

实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。

什么是泛型中的限定通配符和非限定通配符

  • 限定通配符
    • <? extends T> 可以是T或者是T的子类;上界通配符
    • <? super T> 可以是T或者是T的父类;下界通配符
  • 非限定通配符
    • <?> 可以匹配任意类型,

List<? extends T>和List <? super T>之间有什么区别?

  • List<? extends T> 上界通配符,只可以读取,不能写入。

  • List<? extends Number> list = new ArrayList<Integer>();
    Number number = list.get(0); // ✅ 可读
    list.add(10);                // ❌ 编译报错:无法添加
    
  • List <? super T> 下界通配符,只可以写入,不能读取

    • List<? super Integer> list = new ArrayList<Number>();
      list.add(10);        // ✅ 可写入 Integer
      Number num = (Number) list.get(0); // ✅ 可读,但需要强制转换
      

判断ArrayList与ArrayList是否相等?

ArrayList<String> a = new ArrayList<String>();
ArrayList<Integer> b = new ArrayList<Integer>();
Class c1 = a.getClass();
Class c2 = b.getClass();
System.out.println(c1 == c2);

输出的结果是 true。因为无论对于 ArrayList 还是 ArrayList,它们的 Class 类型都是一直的,都是ArrayList.class。

原因

  • Java 的泛型在运行时会进行 类型擦除(Type Erasure)。无论泛型参数是什么,最终的类对象都是 ArrayList.class
  • 因此,ArrayList<String>ArrayList<Integer> 在运行时都被视为 ArrayList,它们的类对象是相同的。

那它们声明时指定的 String 和 Integer 到底体现在哪里呢?答案是体现在类编译的时候。当 JVM 进行类编译时,会进行泛型检查,如果一个集合被声明为 String类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。

Array中可以用泛型吗?

不可以,因为 List可以提供编译期的类型安全保证,而 Array 却不能。

更多推荐