Java码农人生开启手册——泛型

       参考文章:Java 中的泛型(两万字超全详解)_java 泛型-优快云博客Java泛型中的类型擦除详解 - 知乎java泛型通配符详解 - 朱子威 - 博客园

        Java 泛型(generics)是 JDK 5 中引入的一个新特性,本质是参数化类型。泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型,一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。泛型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

一、泛型类

1、泛型类的定义

class 类名称 <泛型标识> {
  .....
}

泛型标识符设置规范:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 表示 第二、第三、第四个类型

注意:

  1. 泛型类可以接受多个参数类型。
  2. 泛型类所声明的参数类型不可以用来定义静态的方法和变量,因为他们在类加载是就已经完成了初始化,直接通过类名调用。 
  3. 静态泛型方法中可以使用自身的方法签名中定义的参数类型。

2、泛型类的使用

// 定义一个泛型类的引用,并实例化一个泛型对象
泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参);
// 如果 <> 中什么都不传入,则默认是 < Object >

        使用泛型的上述特性便可以在集合中限制添加对象的数据类型,若集合中添加的对象与指定的泛型数据类型不一致,则编译器会直接报错,这也是泛型的类型安全检测机制的实现原理。

        类型推导:当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写 。比如:MyArray<Integer> list = new MyArray<>();

3、裸类型

        裸类型是一个泛型类但没有带着类型实参。这样做编译完全正常,但我们不要去使用裸类型,因为这是为了兼容老版本的 API 保留的机制。

二、泛型接口

// 泛型接口的定义
public interface 接口名<类型参数> {
    ...
}

注意:与泛型类相同,静态成员同样也不可以使用泛型接口定义的数据类型。

三、泛型方法

1、泛型方法的定义

         当在一个方法签名中的返回类型前声明了一个 <T> 时,该方法就被声明为一个泛型方法,<T> 表明该方法声明了一个类型参数T。 

public <类型参数> 返回类型 方法名(类型参数 变量名) {
    ...
}

注意: 

  1. 泛型方法声明的类型参数只能在该方法内使用,泛型方法也可以使用泛型类中定义的类型参数。
  2. 只有在方法签名中声明了 <T> 的才是泛型方法,仅仅使用了泛型类定义的泛型参数的方法不是泛型方法。
  3. 泛型方法也可以声明多个类型参数。
  4. 当泛型类和泛型方法同时使用参数表示 T 实名了类型参数是,泛型方法会以自己声明的参数类型为准。为避免混淆,我们最好不要同名。
  5. 虽然静态成员不可以使用泛型类(或接口)声明的类型参数,但是我们可以将静态成员定义为一个泛型方法。

2、泛型方法的使用

        泛型方法在调用方法的时候再确定类型参数的具体类型。 编译器会根据调用泛型方法时传入的实际对象判断出类型参数所代表的实际数据类型。

class GenericMethod {
	// 泛型方法
	public <T> T fun(T t) {
    	return t;
  	} 
}

public class Demo {  
  public static void main(String args[]) {  
    GenericMethod g = new GenericMethod();
    String str = g.<String>fun("刘"); // 指定类型参数
    String str2 = g.fun("刘"); // 不指定类型参数
    int num = g.<Integer>fun(20);  // 自动装箱  
    int num2 = g.fun(20);
  }  
}

类型推断 :

  1. 在不指定类型参数的情况下,方法声明的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
  2. 在指定类型参数的情况下,出入泛型方法中的实参的数据类型必须为指定数据类型或其子类,否则编译报错。

四、继承与实现

  • 泛型类继承泛型类
    • 子类的类型参数与父类的类型参数一致。
    • 子类的类型参数包含父类的类型参数。
    • 子类拥有其他类型参数,父类需要确定参数类型。
      class Son<T> extends Father<String> {}
  • 普通类继承泛型类,父类必须确定类型参数。
  • 对于接口的继承与实现,规则与上述同理,不再赘述。

五、类型擦除

        Java的泛型是伪泛型,本质是数据类型参数化,它是通过擦除的方式来实现的,即编译器毁在编译期间擦除代码中所有泛型语法并相应的做出一些类型转换动作。换而言之,泛型的信息只存在于代码编译阶段, .class 文件中不会包含任何的泛型信息,即泛型信息不会进入到运行时阶段

        在代码中定义List<Object>和List<String>,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的Java编译器会在编译时尽可能的发现可能出错的地方。我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

public class Test {

    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());    //true
    }
}

        原始类型,就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

擦除机制引发的问题

1、泛型是抗变的

Dag 是 Animal 的子类:

协变(Covariance):一个List<Dog>类型对象可以赋值给 一个List<Animal>类型对象
逆变(Contravariance): 一个List<Animal>类型对象可以赋值给 一个List<Dog>类型对象
抗变(Invariant): List<Animal>:一个List<Animal>类型对象和一个List<Dog>类型对象相互无法赋值

2、自动类型转换

// ArrayList.get()方法:
public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
}

        因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。

3、类型擦除与多态冲突

// 有这样一个泛型类:
class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  

    public void setValue(T value) {  
        this.value = value;  
    }  
}

// 子类继承
class DateInter extends Pair<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  

    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

        在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。所以,我们在子类中重写这两个方法一点问题也没有,从他们的 @Override 标签中也可以看到,好像一点问题也没有。可是实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  
    private Object value;  

    public Object getValue() {  
        return value;  
    }  

    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

        不难发现,父类和子类中的参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。我们在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException {  
        DateInter dateInter = new DateInter();  
        dateInter.setValue(new Date());                  
        dateInter.setValue(new Object()); //编译错误  
}

        如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,是重写,而不是重载。 

        这是因为JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  
       4: return  

  public void setValue(java.util.Date);  //我们重写的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  
       5: return  

  public java.util.Date getValue();    //我们重写的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  

  public java.lang.Object getValue();     //编译时由编译器生成的巧方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;  
       4: areturn  

  public void setValue(java.lang.Object);   //编译时由编译器生成的巧方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V  
       8: return  
}

        从编译的结果来看,我们本意重写 setValue 和 getValue 方法的子类,但是却出现了4个方法。而最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是 Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue 和 getValue 方法上面的 @Oveerride 只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

4、泛型类型变量不能是基本数据类型

        因为类型擦除后的原始类型只能存储引用类型的值,不能存储基本类型的值。

5、使用instanceof

class Example<T> {
    public boolean isTypeAString(String s) {
        return s instanceof T; 
    }
}

        这段代码产生编译错误,因为只要编译器将 Java 源代码编译为 Java 字节码,类型擦除之后,JVM 无法在运行时区分 T 类型。

// Example<String> 和 Example<Number>被类型擦除后
class Example { 
    public boolean isTypeAString(String s) {
        return s instanceof Object;
    }
}

        但是我们可以使用无界通配符 (?) 在 instanceof 中指定类型,用于评估实例 obj 是否为 List:

public boolean isAList(Object obj) {
    return obj instanceof List<?>;
}

System.out.println(isAList("foo")); //  false
System.out.println(isAList(new ArrayList<String>()); //  true
System.out.println(isAList(new ArrayList<Float>()); //  true

        使用 T 与 instanceof 的实例 t 也是可以的,因为,即使类型擦除仍然发生,现在 JVM 可以区分内存中的不同类型,即使它们使用相同的引用类型(Object)。

class Example<T> {
    public boolean isTypeAString(T t) {
        return t instanceof String;
    }
}
// 类型擦除后
class Example {
    public boolean isTypeAString(Object t) {
        return t instanceof String;
    }
}

// -----------------

Object obj1 = new String("foo");
Object obj2 = new Integer(11);
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String

6、泛型在静态类和静态方法中的使用

        泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

        但是,因为在静态泛型方法中使用的 T 是自己在方法中定义的 T ,所以这种情况是正确的。

六、泛型通配符

        在现实编码中,有时候我们希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符这个概念。

1、无界通配符 <?>

        ? 代表了任何一种数据类型,能代表任何一种数据类型的只有 null。需要注意的是: <?> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。

注意:Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList<?> 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?> 是 ArrayList< Object > 逻辑上的父类。

2、 上界通配符 < ? extends E>

        T 代表了类型参数的上界,<? extends T>表示类型参数的范围是 T 和 T 的子类。上界通配符主要用于读数据。需要注意的是: <? extends T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型,这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功

  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

        类型参数列表中如果有多个类型参数上限,用逗号分开,如:

private <K extends A, E extends B> E test(K arg1, E arg2){
    E result = arg2;
    arg2.compareTo(arg1);
    //.....
    return result;
}

3、下界通配符 < ? super E>

       T 代表了类型参数的下界,<? super T>表示类型参数的范围是 T 和 T 的超类,直至 Object。下界通配符主要用于写数据。需要注意的是: <? super T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。

private <T> void test(List<? super T> dst, List<T> src){
    for (T t : src) {
        dst.add(t);
    }
}

public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = new ArrayList<>();
    new Test3().test(animals,dogs);
}
// Dog 是 Animal 的子类
class Dog extends Animal {

}

// dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,
// 因此装得下 dst 的容器也就能装 src 。

4、? 和 T 的区别

        ? 和 T 都用来表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行,比如:

// 可以
T t = operate();

// 不可以
? car = operate();

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

区别: 

  1. 通过 T 来 确保 泛型参数的一致性
    // 通过 T 来 确保 泛型参数的一致性
    public <T extends Number> void test(List<T> dest, List<T> src)
    
    // 通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
    public void test(List<? extends Number> dest, List<? extends Number> src)
    
    // 不能保证两个 List 具有相同的元素类型的情况
    GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
    List<String> dest = new ArrayList<>();
    List<Number> src = new ArrayList<>();
    glmapperGeneric.testNon(dest,src);
    // 上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),
    // 对于 dest 和 src 而言,就还是需要进行类型转换。

  2. 类型参数可以多重限定而通配符不行
    public class MultiLimit implements MultiLimitInterfaceA, MultiLimitInterfaceB {
        public static<T extends MultiLimitInterfaceA & MultiLimitInterfaceB> void test(T t) {
        
        }
    }
    // 使用 & 符号设定多重边界(Multi Bounds),
    // 指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,
    // 此时变量 t 就具有了所有限定的方法和属性。
    // 对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。

  3. 通配符可以使用超类限定而类型参数不行
    // 类型参数 T 只具有 一种 类型限定方式:
    T extends A
    
    // 但是通配符 ? 可以进行 两种限定:
    ? extends A
    ? super A

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值