Java - 泛型详解

目录

泛型使用

泛型擦除

边界处的动作

擦除的代价

擦除的补偿

桥方法


泛型使用

泛型类

  1. 泛型的类型参数只能是引用类型(包括自定义类),不能是简单类型(基本数据类型)

  2. 泛型类不能 extends Throwable 类,不能抛出、捕获泛型类对象;在异常规范中可以使用类型变量,但catch 子句中不能使用类型变量

public class GenericTest{
    public static void main(String[] args){
        Generic<String> s=new Generic<String>("abc");
        System.out.println(s.getKey());
        Generic<Integer> i=new Generic<Integer>(123);
        System.out.println(i.getKey());
    }
}

class Generic<T> {
    private T key;

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

    public T getKey() {
        return key;
    }
}

 

泛型接口

public interface Generator<T> {
    public T next();
}
//实现泛型接口的类要么用类型实参替换参数类型,要么不传入实参,如下继续定义泛型类
class FruitGenerator<T> implements Generator<T>{   //实现接口的类未传入泛型实参时,与定义泛型类相同,在声明此类时需将泛型声明也加到类中
    @Override
    public T next() {
        return null;
    }
}

 

泛型方法

1、只有声明了类型参数的方法才是泛型方法,只是使用了泛型类声明的泛型作参数或返回值类型的方法不是泛型方法

2、泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,需要声明自己的类型参数

public class ErasedTest {

    public static void main(String[] args) {

        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();

        generateTest.show_1(apple);       //apple是Fruit的子类,所以这里可以
//        generateTest.show_1(person);    //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person

        generateTest.show_2(apple);       //使用这两个方法都可以成功,因为泛型方法的类型参数与泛型类的类型参数无关
        generateTest.show_2(person);      //使用这两个方法都可以成功

        generateTest.show_3(apple);       //使用这两个方法都可以成功
        generateTest.show_3(person);      //使用这两个方法都可以成功
    }

}    //GenericFruit

class GenerateTest<T>{                       //泛型类GenerateTest

    public void show_1(T t){                 //show_1不是泛型方法,只是使用了类GenerateTest声明的类型参数 T
        System.out.println(t.toString());
    }

    public <T> void show_2(T t){             //泛型方法 show_2 这里T是全新类型,与其所在类有无声明泛型、声明的何种泛型无关
        System.out.println(t.toString());
    }

    public <E> void show_3(E t){            //泛型方法 show_3 使用新泛型E,E可以为任意类型(与T无关)。
        System.out.println(t.toString());
    }
}   //GenerateTest<T>

class Fruit{
    @Override
    public String toString() {
        return "fruit";
    }
}

class Apple extends Fruit{
    @Override
    public String toString() {
        return "apple";
    }
}

class Person{
    @Override
    public String toString() {
        return "Person";
    }
}

 

通配符

类型通配符一般是使用 ?代替具体的类型实参此处’ ?’是类型实参,而不是类型形参

?:任意类型

上限:?extends E:任意 E 及其 子类类型

下限:?super E:任意 E 及其 超类类型

带有超类型限定的通配符可以向泛型对象写人(set、add),带有子类型限定的通配符可以从泛型对象读取(get)

使用场景:当类型参数 T 只使用一次,而且是被用作多态,即允许在不同的调用点可以使用多种实参类型时,就应该使用通配符

如果你有一个只使用类型参数T作为参数的API,它的使用应该利用下限通配符( ? super T )的好处。相反的,如果API只返回T,你应该使用上限通配符( ? extends T )来给你的客户端更大的灵活性(出处)—— 这句话的意思是所有形参 T 可以接受的实参, ( ? super T )也可以接受;所有能接受返回 T 类型赋值的引用也能接受 ( ? extends T ) 类型赋值(Java 1.5泛型指南原文:In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T). )

import java.util.*;

public class GenericTest {

    public static void main(String[] args){

        GenericTest generic=new GenericTest();

        List<String> names=new ArrayList<>();
        names.add("Bob");
        List<Integer> integers=new ArrayList<>();
        integers.add(20);
        List<Number>  numbers=new ArrayList<>();
        numbers.add(1.0);
        List<Object> objects=new ArrayList<>();
        objects.add(new Object());

        generic.genericData(names);
        generic.genericData(integers);
        generic.genericData(numbers);
        generic.genericData(objects);

//        generic.extendsNumber(names);     // 编译不通过。extendsNumber 方法只接受 Number 及其子类类型的集合
        generic.extendsNumber(integers);
        generic.extendsNumber(numbers);
//        generic.extendsNumber(objects);   // 编译不通过。理由同上

//        generic.superNumber(names);       // 编译不通过。superNumber 方法只接受 Number 及其超类类型的集合
//        generic.superNumber(integers);    // 编译不通过。理由同上
        generic.superNumber(numbers);
        generic.superNumber(objects);

    }

    public void genericData(List<?> data){  // data 表示任意类型的 List集合

//        data.add(new Integer(1));  // 编译不通过。因为不确定 data 的类型,所以可能出现将超类对象赋给子类引用的情况
//        data.add(new Object());    //编译不通过。

        Object object=data.get(0);
//        String string=data.get(0);    //编译不通过。很明显不确定类型的对象无法赋给指定类型的引用
//        Integer integer=data.get(0);  //编译不通过。理由同上

        for(int i=0;i<data.size();i++){
            System.out.println(data.get(i));
        }
    }

    public void extendsNumber(List<? extends Number> data){  // data 表示 Number 及其子类类型引用的集合

//        data.add(new Object());        //编译不通过。因为不知道 data 是 Number 类型还是其某子类
//        data.add(new Integer(2));      //编译不通过。data 可能是 Number 某子类

        Object object=data.get(0);
        Number number=data.get(0);
//        Integer integer=data.get(0);   //编译不通过。Number及其子类对象只能赋给Number及其超类引用

        for(Object d:data){
            System.out.println(d.toString());
        }
    }

    public void superNumber(List<? super Number> data){   // data 表示 Number 及其超类类型引用的集合(此集合可添加 Number 的子类对象)

//        data.add(new Object());        //编译不通过。理由同上
        data.add(new Integer(3));
        data.add(new Double(3.0));

        Object object=data.get(0);
//        Number number=data.get(0);      //编译不通过。Number及其超类对象不能赋给Number及其子类引用
//        Integer integer=data.get(0);    //编译不通过。理由同上

        Iterator iter=data.iterator();
        while(iter.hasNext()){
            System.out.println(iter.next());
        }
    }

}

通配符还有一个优势式他们可以在方法签名之外被使用,比如field的类型,局部变量和数组

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
  history.addLast(shapes);
  for (Shape s: shapes) {
  	s.draw(this);
  }
}

 

泛型擦除

Java中的泛型,只在编译阶段有效,在进入虚拟机之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除

在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法,也就是说,泛型信息不会进入到运行时阶段。

  • 虚拟机中没有泛型,只有普通类,所有类型变量都被替换为其第一个限定(bound)类型(没有限定类型就替换为Object)。(通俗地讲,泛型类和普通类在JVM内没有特别之处)

  • 擦除的核心动机是使得泛化的客户端可以通过非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”(设计 Java 泛型类型的主要目标是允许泛型代码和遗留代码之间能够互操作)

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass(); 
System.out.println(c1 == c2);     //true。ArrayList<Integer>和ArrayList<String> 都在编译期被编译器擦除成了ArrayList

 

 

边界处的动作

即使擦除在方法或类的内部移除类有关实际类型的信息,编译器仍旧可以确保在方法和类中使用的类型的内部一致性(内部一致性即放进去的是什么类型的对象,取出来还是相同类型的对象)

擦除是在方法体中移除类型信息,所以运行时的问题就是边界(即对象进入和离开方法的地点),泛型所有的动作都发生在边界处:对传递进来的值进行额外的编译期检查,对传递出去的值插入转型

编译器的类型检查是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象,如下例的list1

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  //和ArrayList<String> list1 = new ArrayList<String>();效果相同(因为new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,真正涉及类型检查的是它的引用list1)
        list1.add("1");             //编译通过 ———— 编译器对进入方法的参数类型进行检查(边界动作)
        list1.add(1);               //编译错误
        String str1 = list1.get(0); //返回类型就是String ———— 编译器对传递出去对值插入转型(边界动作)

        ArrayList list2 = new ArrayList<String>();   //这里没有使用泛型
        list2.add("1");    //编译通过  
        list2.add(1);      //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList<String>().add("11"); //编译通过  
        new ArrayList<String>().add(22);   //编译错误  
    }  
} 

 

 

擦除的代价

擦除使得泛型不能使用任何在运行时需要知道确切类型信息的操作,如转型、instanceof、new,不能创建具体类型的泛型数组(程序运行过程中无法分辨一个数组中的元素类型)

数组对象的元素类型不能是一个类型变量或者类型参数,除非它是无上限的通配符类型(<? super XXX>),可以声明元素类型是一个类型参数或者参数化类型的数组引用,但不能实例化泛型数组对象(译注:得不到对象,只能声明变量)。

(原文:The component type of an array object may not be a type variable or a parameterized type, unless it is an (unbounded) wildcard type.You can declare array types whose element type is a type variable or a parameterized type, but not array objects.)

import java.util.ArrayList;
import java.util.List;

public class ErasedTest{

    private static final int SIZE = 100;

    public static void main(String[] args){
        func();
    }

    public static <T> void func() {
				// instanceof
			  Collection cs = new ArrayList<String>();
//        System.out.println((cs instanceof Collection<String>));  //编译不通过。提示:illegal generic type for instanceof

        // 转型
        Collection<String> cstr = (Collection<String>) cs;	//warning:Unchecked cast: 'java.util.Collection' to 'java.util.Collection'   (使用快捷键看到的warning:Inspect current file with current profile)
        T[] array2 = (T[]) new Object[SIZE];                //warning:Unchecked cast: 'java.lang.Object[]' to 'T[]'
        T t=(T) new Object();                               //warning:Unchecked cast: 'java.lang.Object' to 'T'
        
        // 实例化对象
//        T var = new T();                               //编译不通过。类型参数 T 不能被直接实例化
//        T[] array1 = new T[SIZE];                      //编译不通过。类型参数 T 不能被直接实例化

      	//创建泛型数组  (这部分有必要参考:https://blog.csdn.net/explorers/article/details/454837#_Toc111865971)
//        List<T>[] ls=new ArrayList<T>[SIZE];                //编译不通过。不能创建泛型数组
//        List<String>[] ls2 = new ArrayList<String>[SIZE];   //编译不通过。不能创建泛型数组
      	
      	List<T>[] ls1=new ArrayList[SIZE];              //等效于 List[] lll=new ArrayList[SIZE];
        List<String>[] ls3 = new ArrayList[SIZE];       //等效于 List[] lll=new ArrayList[SIZE];
        List<?>[] ls4 = new ArrayList<?>[SIZE];         //等效于 List[] lll=new ArrayList[SIZE];
         
        //以下都是创建泛型集合对象,没问题
        List<?> ls5 = new ArrayList<>();             
        List<String> ls6 = new ArrayList<>();
        List<String> ls7 = new ArrayList<String>();

        System.out.println(array2.getClass());  //class [Ljava.lang.Object;
        System.out.println(ls1.getClass());     //class [Ljava.util.ArrayList;
        System.out.println(ls3.getClass());     //class [Ljava.util.ArrayList;
        System.out.println(ls4.getClass());     //class [Ljava.util.ArrayList;
        System.out.println(ls5.getClass());     //class java.util.ArrayList
        System.out.println(ls6.getClass());     //class java.util.ArrayList
        System.out.println(ls7.getClass());     //class java.util.ArrayList

    }
}

 

 

擦除的补偿

擦除丢失了在泛型代码中执行某些操作的能力,可以通过显示地引入类型标签来对擦除进行补偿(给构造器传递工厂对象、类的域包含工厂对象)

例1:Class对象是最便利的工厂对象

class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
  Class<T> kind;                           //ClassTypeCapture类的域为工厂对象
  public ClassTypeCapture(Class<T> kind)   //给构造器传递工厂对象
  	{ this.kind = kind; }     
  public boolean f(Object arg) { return kind.isInstance(arg); }   //函数 f() 通过工厂对象kind使用isInstance()
  
  public static void main(String[] args) {
    ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);  //通过给构造器传递工厂对象使用 new,创建Building类型的ctt1
    System.out.println(ctt1.f(new Building()));    //true
    System.out.println(ctt1.f(new House()));       //true
    ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);           //给构造器传递工厂对象,创建House类型的ctt2
    System.out.println(ctt2.f(new Building()));    //false
    System.out.println(ctt2.f(new House()));       //true
  }
}

 

例2:创建显式的工厂对象,用于限制要替换类型参数的实际类的类型(限制其必须具备的功能等)

interface FactoryI<T> {     //工厂接口FactoryI<T>
	T create();
}

class Foo2<T> {            //泛型类Foo2
  private T x;
  public <F extends FactoryI<T>> Foo2(F factory) {    // Foo2 的构造器接受实现了泛型工厂接口FactoryI的类的对象
  	x = factory.create();   //通过工厂子类对象调用工厂的create方法创建泛型对象 x
	}
	// ...
}

class IntegerFactory implements FactoryI<Integer> {    //IntegerFactory类用Integer类型扩展了FactoryI工厂 ———— IntegerFactory是工厂接口的子类
  public Integer create() {
  	return new Integer(0);
	}
}

class Widget {       //Widget类通过内部类Factory扩展类工厂FactoryI ———— Widget是工厂接口的子类
  public static class Factory implements FactoryI<Widget> {    //Widget的内部类Factory,创建Widget类对象
  public Widget create() {
  	return new Widget();
  }
  }
}
public class FactoryConstraint {
  public static void main(String[] args) {
    new Foo2<Integer>(new IntegerFactory());  //给Foo2的构造器传递IntegerFactory类的对象,此对象在Foo2的构造器中调用自己重写的create方法,返回Integer对象
    new Foo2<Widget>(new Widget.Factory());   //给Foo2的构造器传递Widget类的工厂对象,此对象调用create返回Widget对象(Widget.Factory()是“外部类.内部类”的调用形式)
  }
} 

 

 

桥方法

当擦除与多态产生冲突时,编译器通过在子类生成桥方法来保持多态参考

public abstract class Person<E> {            //泛型父类
    public abstract void setName(E name);
    public abstract E getName();
}
public class Actor extends Person<String> {    //子类
    @Override
    public void setName(String name) {  }
  
    @Override
    public String getName() {
        return "LI_HUA";
    }
}

这里在运行时候父类泛型参数会变成Object,即Person<Object>,那么所谓的继承在运行期间岂不是建立在Object为参数的继承;而子类的泛型参数还是String,那么所谓的重写岂不是成为重载?

查看Actor的反编译代码可知编译器生成了桥方法 来解决冲突,保持多态:

通过桥方法 public java.lang.Object getName(); 调用子类方法 getName:()Ljava/lang/String; 方法

通过桥方法 public void setName(java.lang.Object); 调用子类方法 setName:(Ljava/lang/String;)V

(注:下面代码是直接复制的,注释是反编译生成的,其中invokevirtual意为调用实例方法)

~$ javap -c Actor.class
Compiled from "Actor.java"
public class Actor extends Person<java.lang.String> {
  public Actor();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Person."<init>":()V
       4: return

  public void setName(java.lang.String);
    Code:
       0: return

  public java.lang.String getName();
    Code:
       0: ldc           #2                  // String LI_HUA
       2: areturn

  public java.lang.Object getName();
    Code:
       0: aload_0
       1: invokevirtual #3                  // Method getName:()Ljava/lang/String; 
       4: areturn

  public void setName(java.lang.Object); 
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #4                  // class java/lang/String
       5: invokeinvokevirtualvirtual #5                  // Method setName:(Ljava/lang/String;)V
       8: return
}

子类中的桥方法Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

 

擦除的利用

例:通过反射绕过编译器检查绕过泛型本身的一些限制

public class Test {

    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<Integer>();

        list.getClass().getMethod("add", Object.class).invoke(list, "asd");   //利用反射调用add方法,绕开编译器对泛型的类型检查

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));       //Output:asd
        }
    }
}

 

 

 

参考:

https://blog.csdn.net/explorers/article/details/454837

https://blog.csdn.net/caihuangshi/article/details/51278793

https://blog.csdn.net/LonelyRoamer/article/details/7868820

https://blog.csdn.net/briblue/article/details/76736356

https://juejin.im/entry/5b18e19d5188257d887d4276

https://blog.csdn.net/s10461/article/details/53941091#commentBox

https://www.cnblogs.com/wuqinglong/p/9456193.html

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值