Java泛型最全总结

Java
350
0
0
2023-07-07

概述

什么是泛型?泛型能解决什么问题? 泛型即参数化类型,会让你的程序更易读,也更安全。 在Java增加泛型特性前【JDK5前】,泛型的程序的设计是用继承来实现的。如下程序可以正常编译和运行,但将get的结果强制类型转换会产生一个错误。为了解决这个问题,泛型应运而生。

  /**
     * 不同类型的元素add到ArrayList中
     */    @Test
    public void mixedValueTest(){
        ArrayList mixedValue = new ArrayList();
        mixedValue.add(L);
        mixedValue.add("string");
        mixedValue.forEach(item->{
            System.out.println((String)item);
        });
    }
  // java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String

在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

特性

泛型只在编译阶段有效,在编译时会采取去泛型化的措施:将泛型相关的信息擦除,同时在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行阶段。

  /**
     * 使用类型参数约束元素的类型
     */    
@Test
public void traitTest(){
    ArrayList<String> stringList = new ArrayList<String>();
    stringList.add("string a");
    stringList.add("string b");

    ArrayList<Integer> IntegerList = new ArrayList<Integer>();
    IntegerList.add();
    IntegerList.add();
    if(stringList.getClass()==IntegerList.getClass()){
        System.out.println("相同类型"); // 输出相同类型
    }
// 泛型类型在逻辑上可以看成是多个不同的类型,实际上都是相同的类型
}

泛型的定义和使用

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

泛型类

泛型类型用于类的定义中,被称为泛型类,泛型类实例化时需要传入泛型类型实参【不传默认为Object】。最典型的是各种容器类List、ArrayList、Set、Map。

. 泛型类的定义
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 变量名;
  .....
  }
}
常用的泛型标识【一般使用大写字母标识】: K/V/T/E/U/S/R....... 泛型类的使用
// 泛型相当于普通类的工厂
类目<具体的数据类型> 对象名 = new 类名<具体的数据类型[JDK后可省略]>();
. 从泛型类派生子类
// 子类也是泛型类,子类和父类的泛型类型要一致,只能在父类的基础上进行扩展【子类必须包含父类的泛型标识】
class 子类名<泛型标识:T> extends 父类名<泛型标识:T>

// 子类不是泛型类,父类要明确泛型的数据类型
class 子类名 extends 父类名<明确的数据类型>

注意事项:

  • 泛型的类型参数只能是Object类型,不能是基本数据类型
  • 如果没有指定具体的数据类型。默认操作的类型是Object
  • 泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同的类型

泛型接口

泛型接口与泛型类的定义及使用基本相同。

. 泛型接口的定义
interface 接口名称<泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 方法名();
  .....
  }. 泛型接口的使用
// 实现类是泛型类,实现类和接口的泛型类型保持一致
class 实现类<T> implements 泛型接口<T>
// 实现类不是泛型类,接口要明确数据类型
class 实现类 implements 泛型接口<String>

泛型方法

泛型方法:是在调用方法的时候指明泛型的具体类型,泛型方法中的泛型标识跟泛型类中的泛型标识无关,可以看成一个独立的体系。

. 泛型方法定义
// 关键在于修饰符后的菱形语法<E>
修饰符 <E> 返回值类型 方法名(E ele){
    return Objects.isNull(ele);
}
// 修饰符后的菱形语法<E>,可以理解声明此方法为泛型方法,泛型类的使用了泛型的成员方法并不是泛型方法。. 泛型方法和可变参数
public <E> void printElement(E... elements){
    for(E element : elements){
        System.out.println(element);
    }
}. 泛型方法的使用
printElement("string",,true);

小结:

  • 泛型方法能是方法独立于类而产生变化
  • 无论何时,如果能做到,就该尽量使用泛型方法,而不是将类泛型化
  • 如果static方法要使用泛型能力,就必须将其定义为泛型方法,原因是静态方法无法访问类上定义的泛型。

通配符

泛型标识本质上也是通配符,没啥区别,是编码时约定俗成的东西。 通常情况下,T,E,K,V,? 是这样约定的:

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

? 和 T 的区别

T是一个确定的类型,通常用于泛型类和泛型方法的定义,?是一个不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

  • 通过T可以确定泛型参数的一致性
 // ?本身就是不确定的元素,不能保证List的元素是一致的
 void t(List<? extends Number> list);
 // 确保泛型参数是一致的,List中存储相同的元素
 void t(List<T> list);
  • 类型参数T可以多重限定,而通配符?不行
  • 通配符可以使用超类限定而类型参数不行
  T extends Number  // 允许
 T susper Number  // 不允许
 
 ? extends Number  // 允许
 ? susper Number  // 允许

通配符

<?>无限定的通配符

无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关,只能操作与类型无关的方法:

 ArrayList<? super Number> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(1.2);

ArrayList<?> arrayList = new ArrayList<>();
// 将任意ArrayList加入ArrayList<?>不会出错
arrayList = arrayList3;
// 操作arrayList.add不能正常编译
// arrayList.add(1);  
for (Object o : arrayList) {
   // do something
}

上限通配符 < ? extends E>

<? extends E>限定参数类型的上限:参数类型必须是E或E的子类型:

 // 限定类型实参实现了Comparable接口,其中T表示限定类型的子类型,Comparable为限定类型的上界
public static <T extends Comparable> T min(T... comparbleElements)
// 一个类型变量或通配符可以有多个限定
public static <T extends Comparable & Serializable> T min(T... comparableElements)

小结:

  • 如果传入的类型不是限定类型及其子类,编译不成功
  • 只能取,且只能取E和E的父类型

下限通配符 <? super E>

<? super E>限定参数类型的下界:参数类型必须是E或E 的超类型。 susper关键字和extends关键字功能相反,使用<? super Integer>通配符表示:

  • 允许调用set(? super Integer)方法传入Integer的引用;
  • 不允许调用get()方法获得Integer的引用。

对比extends和super通配符区别:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外)
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

类型擦除

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。 在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。

无限制类型擦除

泛型标识未限定类型,编译器将未绑定的类型T替换为实际类型的Object:

  public class Erasure<T>{
        private T key;

        public T getKey() {
            return key;
        }

        public void setKey(T key) {
            this.key = key;
        }
    }
 // 类型擦除后
 public class Erasure{
        private Object key;

        public Object getKey() {
            return key;
        }

        public void setKey(Object key) {
            this.key = key;
        }
    }

有限制类型擦除

泛型标识有限定类型,编译器会将绑定类型参数T替换为第一个绑定类【存在多重限定的情况】:

  public class Erasure<T extends Number & Comparable>{
        private T key;

        public T getKey() {
            return key;
        }

        public void setKey(T key) {
            this.key = key;
        }
    }
// 类型擦除后
 public class Erasure{
        private Number key;

        public Number getKey() {
            return key;
        }

        public void setKey(Number key) {
            this.key = key;
        }
    }

桥接方法

一种允许扩展泛型类或实现泛型接口(带有具体类型参数)的类仍用作原始类型的方法。是编译器行为,在编译时自动在子类添加桥接方法以维持多态性。 编译器保护对桥接方法的访问,强制直接对其进行显式调用会导致编译时错误。

  // 接口
 interface IBridgeMethod<T> {
        public T info(T key);
    }

    // 泛型信息擦除后编译的字节码
    interface IBridgeMethod {
        public Object info(Object key);
    }

 // 实现类
 // 类型擦除后,方法签名不匹配,父类的info(Object key)方法不会被重写,为了解决这个问题并在类型擦除之后保留泛型类型的多态性,Java 编译器生成一个桥接方法来确保子类型按预期工作
 public class BridgeMethodImpl implements IBridgeMethod<Integer>{
        @Override
        public Integer info(Integer key) {
            return null;
        }
    }
    // 泛型信息擦除后编译的字节码:子类自动生成一个与父类的方法签名一致的桥接方法,可以通过反射或反编译看到
    public class BridgeMethodImpl implements IBridgeMethod<Integer>{
        public Integer info(Integer key) {
            return null;
        }
        // 编译器为了让子类有一个与父类的方法签名一致的方法,就在子类自动生成一个与父类的方法签名一致的桥接方法
        @Override
        public Object info(Object key) {
            return info((Integer)key);
        }
    }

类型擦除带来的局限性

类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如:

正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于以上对类型擦除的了解,利用反射,我们可以绕过这个限制:

 @Test
public void t() throws Exception {
    ArrayList<Integer> arrayList = new ArrayList<>();
    arrayList.add(123);
    Method method = arrayList.getClass().getDeclaredMethod("add",Object.class);
    method.invoke(arrayList,"test");
    System.out.println(arrayList);  //[123, test]
}
// 可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制

泛型的PECS原则

  • Producer extends原则:当只想从容器中获取元素,请把这个容器看成生产者,使用<? extends T>
  • Consumer super 原则:当只想操作容器中的元素,请把这个容器看成消费者,使用<? super T>