java---泛型深入理解总结

最近在遇到下面这段代码时,触及到了知识盲区。因此产生了一些困惑,经过一番查找和研究发现以前对泛型的理解还是太浅显了。之前一直停留在怎么用的阶段,并没有真正理解泛型存在的意义和原理。

 Class<T> clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];

没错三个方法两个不认识。。。惭愧。总的来说这段代码的作用就是获取超类的泛型参数的实际类型。言归正传这篇主要用来记录和深入学习下泛型。

什么是泛型:

用很官方话的解释就是:泛型的本质是参数化类型。一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以应用多中类型的代码,这种刻板的限制对代码得束缚会就会很大。—《Thinking in Java》所以在1.5后java就引入了泛型来解决这种缺陷,很好的解决了类型安全问题同时可以将运行时错误提前到编译时错误并且省去之前的强制类型转换。通俗的说就是用一个变量来表示类型。

泛型使用场景:

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

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

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型通配符

class Info<T>{  
    private T var ;     // 定义泛型变量  
    public void setVar(T var){  
        this.var = var ;  
    }  
    public T getVar(){  
        return this.var ;  
    }  
    public String toString(){   // 直接打印  
        return this.var.toString() ;  
    }  
};  
public class GenericsDemo14{  
    public static void main(String args[]){  
        Info<String> i = new Info<String>() ;       // 使用String为泛型类型  
        i.setVar("it") ;                            // 设置内容  
        fun(i) ;  
    }  
    public static void fun(Info<?> temp){     // 可以接收任意的泛型对象  
        System.out.println("内容:" + temp) ;  
    }  
};  

没有泛型的时候,只有原始类型。此时,所有的原始类型都通过字节码文件类Class类进行抽象。Class类的一个具体对象就代表一个指定的原始类型,泛型出现之后,扩充了数据类型。从只有原始类型扩充了参数化类型、类型变量类型、限定符类型 、泛型数组类型。类型的父类为Type,它位于反射包java.lang.reflect内。由JDK1.5之后提供的,它的标准继承图谱如下:
在这里插入图片描述
Class(原始/基本类型,也叫raw type):不仅仅包含我们平常所指的类、枚举、数组、注解,还包括基本类型int、float等等

  • TypeVariable(类型变量): 比如List中的T等
  • WildcardType( 泛型表达式类型): 例如List< ? extends Number>这种
  • ParameterizedType(参数化类型): 就是我们平常所用到的泛型List、Map(注意和TypeVariable的区别)
  • GenericArrayType(数组类型): 并不是我们工作中所使用的数组String[] 、byte[](这种都属于Class),而是带有泛型的数组,即T[] 泛型数组

GenericArrayType有两个都是:List[] pTypeArray, T[] vTypeArray它哥俩都是泛型数组。但是这两String[] strings, Main[] test可不是,他俩属于Class普通类型

Type接口本身算是一个标记接口,不提供任何需要复写的方法:

在这里插入图片描述

Type的直接子类只有一个,也就是Class,代表着类型中的原始类型以及基本类型。

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {

什么是raw type?

在出现了泛型之后,原本没有使用泛型的代码就被称为raw type(原始类型)。

泛型设计

其实–在设计JDK1.5的时候,想要实现泛型有两种选择:

  • 需要泛型化的类型(主要是容器(Collections)类型),以前有的就保持不变,然后平行地加一套泛型化版本的新类型;
  • 直接把已有的类型泛型化,让所有需要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版。

也就是第一种办法是在原有的Java库的基础上,再添加一些库,这些库的功能和原本的一模一样,只是这些库是使用Java新语法泛型实现的,而第二种办法是保持和原本的库的高度一致性,不添加任何新的库。在当时Java 设计者选择了第二种方案,为什么不选择真正实现泛型?我在知乎看到了一些大神给我的答案:泛型的历史问题>>
在这里插入图片描述

Java 7中对于泛型的类型推导方面的改进

泛型的最大优点是提供了程序的类型安全同时可以向后兼容,但也有让开发者不爽的地方,就是每次定义时都要写明泛型的类型,这样显示指定不仅感觉有些冗长,最主要是很多程序员不熟悉泛型,因此很多时候不能够给出正确的类型参数,现在通过编译器自动推断泛型的参数类型,能够减少这样的情况,并提高代码可读性。在Java 7以前的版本中使用泛型类型,需要在声明并赋值的时候,两侧都加上泛型类型。比方说这样:

Map<String,Integer> map = new HashMap<String,Integer>(); 

为什么在对象初始化的时候还要显示的写出来?这也是泛型在一开始出现的时候受到很多人吐槽的地方。不过,让人欣慰的是,java在进步的同时,那些设计者们也在不断的改进java的编译器,让它变的更加智能与人性化。所以就有了后面人们口中的钻石语法,type inference即,类型推导。

Map<String,Integer> map = new HashMap<>();

在这条语句中,编译器会根据变量声明时的泛型类型自动推断出实例化HashMap时的泛型类型。再次提醒一定要注意new HashMap后面的“<>”,只有加上这个“<>”才表示是自动类型推断,否则就是非泛型类型的HashMap,并且在使用编译器编译源代码时会给出一个警告提示(unchecked conversion warning)。这一对尖括号"<>“官方文档中叫做"diamond”。但是,这时候的类型推导做的并不完全(甚至算是一个半成品),因为在Java SE 7中创建泛型实例时的类型推断是有限制的:只有构造器的参数化类型在上下文中被显著的声明了,才可以使用类型推断,否则不行。例如:下面的例子在java 7无法正确编译(但现在在java8里面可以编译,因为根据方法参数来自动推断泛型的类型):

List<String> list = new ArrayList<>(); 
list.add("A");// 由于addAll期望获得Collection<? extends String>类型的参数,因此下面的语句无法通过 
list.addAll(new ArrayList<>());

并不是在方法调用时动态推导出来的,而是在编译时期就已经确定了。所谓的泛型类型推导是当我们程序中访问泛型集合中的元素时,编译器会根据程序上下文推断出元素的类型,拿出元素之后用类型转换直接强转成我们需要的类型而已,这个操作在编译成class文件的时候就已经完成了,JVM执行class文件时就只是拿出集合里面的元素做一个类型强转操作,仅此而已。所以java的泛型会被称为伪泛型。
下面用一段简单的程序来验证:

public class GenericTypeTest {

    public static void main(String[] args){

        List<Integer> integerList = new ArrayList<>();
        List<String> strList = new ArrayList<>();
        integerList.add(1);
        strList.add("1");
        Integer i = integerList.get(0);
        strList.get(0);
        
    }
}

这段程序很简单,声明两个List分叫integerList、strList。前者用于存放Integer元素,后者用于存放String字符串。往两个List里面塞元素,然后访问,不同的是integerList把访问值赋给一个变量i,strList只是单纯的访问。我们来看一下这些代码逻辑在JVM层面看来到底都是一些什么样的操作。把代码编译成class文件,然后用javap -p -v 命令查看main方法的字节码部分

avap -p -v GenericTypeTest

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: new           #2                  // class java/util/ArrayList
        11: dup
        12: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
        15: astore_2
        16: aload_1
        17: iconst_1
        18: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        21: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        26: pop
        27: aload_2
        28: ldc           #6                  // String 1
        30: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        35: pop
        36: aload_1
        37: iconst_0
        38: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        43: checkcast     #8                  // class java/lang/Integer
        46: astore_3
        47: aload_2
        48: iconst_0
        49: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        54: pop
        55: return

在stack = 2 正下方的

18: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

对应integerList.add(1);

28: ldc           #6                  // String 1
30: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

对应strList.add(“1”);
然后这两个添加操作add进去的元素都被看作是Object类型,看方法参数类型的描述符Ljava/lang/Object,也就是说我们存进去的时候元素的具体类型已经被擦除了,类型已经丢失了。所以在集合里面是找不回我们当初的类型了。所以当我们访问里面的集合元素时返回的对象类型就是Object

49: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;

这段字节码对应代码strList.get(0);
但是如果我们要把返回的集合元素赋值给一个变量时我们的java编译器就会根据程序上下文提供的信息推导出元素的具体类型,然后对赋值的变量做类型检查和强制转换。编译器把元素类型的信息存放在哪里呢?我的猜想是class文件里面的LocalVariableTable

LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      56     0  args   [Ljava/lang/String;
            8      48     1 integerList   Ljava/util/List;
           16      40     2 strList   Ljava/util/List;
           47       9     3 integer   Ljava/lang/Integer;

所以我们看到 Integer i = integerList.get(0); 的真实操作是:取出元素再强转

38: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
43: checkcast     #8                  // class java/lang/Integer

什么是泛型擦除:

众所周知Java的泛型是伪泛型。为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。下面两个例子就很好的证明了泛型擦除的真实性。

 @Test
    public void test13(){
        List<String> stringList = new ArrayList<>();
        stringList.add("chihai");
        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        System.out.println(stringList.getClass() == integerList.getClass());  // 输出结果true
    }
 @Test
    public void test14() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ArrayList<Integer> arrayList3= new ArrayList<>();
        arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer  
        arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "chihai");
        for (int i=0;i<arrayList3.size();i++) {
            System.out.println(arrayList3.get(i));
        }
    }

擦除的过程:

泛型是为了将具体的类型作为参数传递给方法,类,接口。擦除是在代码运行过程中将具体的类型都抹除。
前面说过,Java 1.5 之前需要编写模板代码的地方都是通过Object来保存具体的值。比如:

public class Node{
   private Object obj;

   public Object get(){
       return obj;
   }
   
   public void set(Object obj){
       this.obj=obj;
   }
   
   public static void main(String[] argv){
    
    Student stu=new Student();
    Node  node=new Node();
    node.set(stu);
    Student stu2=(Student)node.get();
   }
}

这样的实现能满足绝大多数需求,但是泛型还是有更多方便的地方,最大的一点就是编译期类型检查,于是Java 1.5之后加入了泛型,但是这个泛型仅仅是在编译的时候帮你做了编译时类型检查,成功编译后所生成的.class文件还是一模一样的,这便是擦除

1.5后泛型实现

public class Node<T>{

    private T obj;
    
    public T get(){
        
        return obj;
    }
    
    public void set(T obj){
        this.obj=obj;
    }
    
    public static void main(String[] argv){
    
    Student stu=new Student();
    Node<Student>  node=new Node<>();
    node.set(stu);
    Student stu2=node.get();
  }
}

两个版本生成的.class文件:

 public Node();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public java.lang.Object get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn
  public void set(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return
}
public class Node<T> {
  public Node();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return
}

可以看到泛型就是在使用泛型代码的时候,将类型信息传递给具体的泛型代码。而经过编译后,生成的.class文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。

泛型语法:

Java 的泛型就是一个语法糖,而语法糖最大的好处就是让人方便使用,但是它的缺点也在于如果不剥开这颗语法糖,有很多奇怪的语法就很难理解。

  • 类型边界
    泛型在最终会擦除为Object类型。这样导致的是在编写泛型代码的时候,对泛型元素的操作只能使用Object自带的一些方法,但是有时候我们想使用其他类型的方法呢?比如:
public class Node{
    private People obj;
    public People get(){
        
        return obj;
    }
    
    public void set(People obj){
        this.obj=obj;
    }
    
    public void playName(){
        System.out.println(obj.getName());
    }
}

如上,代码中需要使用obj.getName()方法,因此比如规定传入的元素必须是People及其子类,那么这样的方法怎么通过泛型体现出来呢?答案是extend,泛型重载了extend关键字,可以通过extend关键字指定最终擦除所替代的类型。

public class Node<T extend People>{

    private T obj;
    
    public T get(){
        
        return obj;
    }
    
    public void set(T obj){
        this.obj=obj;
    }
    
    public void playName(){
        System.out.println(obj.getName());
    }
}

泛型限定(上限和下限)的表达式:

因为在泛型代码内部,无法获取任何有关泛型参数类型的任何信息!,Java的泛型就是使用擦除来实现的,当你在使用泛型的时候,任何信息都被擦除,你所知道的就是你在使用一个对象。所以List< Integer>和List< String>在运行时,会被擦除成他们的原生类型List。再通过这个例子加深一下擦除的理解:

class Hello{
    public void hello(){
        System.out.println("hello");
    }
}

class TestHello<T>{
    private T t;
    public TestHello(T t){
        this.t = t;
    }
    public void callHello(){
        t.hello();//这里不能编译
    }
}

public class Abrasion {
    public static void main(String[] args) {
        Hello hello = new Hello();
        TestHello<Hello> testHello = new TestHello<>(hello);
        testHello.callHello();
    }
}

我们可以看到,t.hello()这里是不能够编译的。因为擦除的存在,main中传入的泛型,在TestHello中编程了Object,因此并不能跟Hello这个类绑定,也就调用不了hello方法。那能够怎么让object调用hello的方法?
可以给定泛型的边界,把类TestHello< T>改为TestHello< T extends Hello>,这个边界声明了T必须具有类型Hello或者从Hello导出的类型。

上限:?extends E:可以接收E类型或者E的子类型对象。

下限:?super E:可以接收E类型或者E的父类型对象。

上限什么时候用: 往集合中添加元素时,既可以添加E类型对象,又可以添加E的子类型对象。为什么?因为取的时候,E类型既可以接收E类对象,又可以接收E的子类型对象。
下限什么时候用: 当从集合中获取元素进行操作的时候,可以用当前元素的类型接收,也可以用当前元素的父类型接收。

擦除带来的问题:

泛型不能用于显性地引用运行时类型的操作之中,例如转型,instanceof和new操作(包括new一个对象,new一个数组),因为所有关于参数的类型信息都在运行时丢失了,所以任何在运行时需要获取类型信息的操作都无法进行工作。
如下:

if(obj instanceof T);
T t = new T();
T[] ts = new T[10];

使用instanceof会失败,是因为类型信息已经被擦除,因此我们可以引入类型标签Class< T>,就可以转用动态的isInstance()。

class A{}
class B extends A{}

public class TestInstance<T> {
    private Class<T> t;
    public TestInstance(Class<T> t){
        this.t = t;
    }

    public boolean compare(Object obj){
        return t.isInstance(obj);
    }

    public static void main(String[] args) {
        TestInstance<A> ti = new TestInstance<A>(A.class);
        System.out.println(ti.compare(new A()));  // true
        System.out.println(ti.compare(new B())); // true

    }
}

解决创建类型实例:

解决办法是使用工厂

interface Factory<T>{
    T create();
}

class Product<T>{
    public <F extends Factory<T>> Product(F factory){
        factory.create();
    }
}

class ProductFactory implements Factory<Integer>{
    public Integer create(){
        Integer integer = new Integer(10);
        System.out.println(integer);
        return integer;
    }
}
public class TestNew {
    public static void main(String[] args) {
        new Product(new ProductFactory());
    }
}

这篇介绍比较详细》》》

解决创建泛型数组:

Java中的泛型会被类型擦除,那为什么在运行期仍然可以使用反射获取到具体的泛型类型?

PECS原则:

如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。

发布了29 篇原创文章 · 获赞 11 · 访问量 1853

猜你喜欢

转载自blog.csdn.net/chihaihai/article/details/104688154