Java 深入理解泛型

泛型大家都接触的不少,但是由于Java 历史的原因,Java 中的泛型一直被称为伪泛型,因此对Java中的泛型,有很多不注意就会遇到的“坑”,在这里详细讨论一下。对于基础而又常见的语法,这里就直接略过了。

泛型概念的提出(为什么需要泛型)?

首先,我们看下下面这段简短的代码:

 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4         List list = new ArrayList();
 5         list.add("qqyumidi");
 6         list.add("corn");
 7         list.add(100);
 8 
 9         for (int i = 0; i < list.size(); i++) {
10             String name = (String) list.get(i); // 1
11             System.out.println("name:" + name);
12         }
13     }
14 }

定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中也加入了Integer类型的值或其他编码原因,很容易出现类似于//1中的错误。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

 在如上的编码过程中,我们发现主要存在两个问题:

1.当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。

2.因此,//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。

那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型。

 

1 什么是泛型

自JDK 1.5 之后,Java 通过泛型解决了容器类型安全这一问题,而几乎所有人接触泛型也是通过Java的容器。那么泛型究竟是什么?
泛型的本质是参数化类型 也就是说,泛型就是将所操作的数据类型作为参数的一种语法。

public class Paly<T>{
    T play(){}
}

其中T就是作为一个类型参数在Play被实例化的时候所传递来的参数,比如:

Play<Integer> playInteger=new Play<>();

这里T就会被实例化为Integer

2 泛型的作用

- 使用泛型能写出更加灵活通用的代码

泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这便是泛型最初的设计宗旨。

- 泛型将代码安全性检查提前到编译期

泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入获取操作是否合法,从而将运行时ClassCastException转移到编译时比如:

List dogs =new ArrayList();
dogs.add(new Cat());

在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误。但是加入泛型后

List<Dog> dogs=new ArrayList<>();
dogs.add(new Cat());//Error Compile

会在编译的时候就检查出来。

- 泛型能够省去类型强制转换

在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。

Dog dog=(Dog)dogs.get(1);

加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅。

3 泛型的具体实现

我们可以定义泛型类,泛型方法,泛型接口等,那泛型的底层是怎么实现的呢?

3.1 从历史上看泛型

由于泛型是JDK1.5之后才出现的,在此之前需要使用泛型(模板代码)的地方都是通过Object向上转型以及强制类型转换实现的,这样虽然能满足大多数需求,但是有个最大的问题就在于类型安全。在获取“真正”的数据的时候,如果不小心强制转换成了错误类型,这种错误只能在真正运行的时候才能发现

因此Java 1.5推出了“泛型”,也就是在原本的基础上加上了编译时类型检查的语法糖。Java 的泛型推出来后,引起来很多人的吐槽,因为相对于C++等其他语言的泛型,Java的泛型代码的灵活性依然会受到很多限制。这是因为Java被规定必须保持二进制向后兼容性,也就是一个在Java 1.4版本中可以正常运行的Class文件,放在Java 1.5中必须是能够正常运行的:

在1.5之前,这种类型的代码是没有问题的。

public static void addRawList(List list){
   list.add("123");
   list.add(2);
}

1.5之后泛型大量应用后:

public static void addGenericList(List<String> list){
    list.add("1");//Only String
    list.add("2");
}

虽然我们认为addRawList()方法中的代码不是类型安全的,但是某些时候这种代码是有用的,在设计JDK1.5的时候,想要实现泛型有两种选择:

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

什么意思呢?也就是第一种办法是在原有的Java库的基础上,再添加一些库,这些库的功能和原本的一模一样,只是这些库是使用Java新语法泛型实现的,而第二种办法是保持和原本的库的高度一致性,不添加任何新的库。

在出现了泛型之后,原本没有使用泛型的代码就被称为raw type(原始类型)
Java 的二进制向后兼容性使得Java 需要实现前后兼容的泛型,也就是说以前使用原始类型的代码可以继续被泛型使用,现在的泛型也可以作为参数传递给原始类型的代码。
比如

 List<String> list=new ArrayList<>();
 List rawList=new ArrayList();
 addRawList(list);
 addGenericList(list);
 
 addRawList(rawList);
 addGenericList(rawList);

上面的代码能够正确的运行。

Java 设计者选择了第二种方案

C# 在1.1过渡到2.0中增加泛型时,使用了第一种方案。


为了实现以上功能,Java 设计者将泛型完全作为了语法糖加入了新的语法中,什么意思呢?也就是说泛型对于JVM来说是透明的,有泛型的和没有泛型的代码,通过编译器编译后所生成的二进制代码是完全相同的。

泛型是一个语法糖,而这个语法糖的实现被称为擦除

3.2 擦除的过程

泛型是为了将具体的类型作为参数传递给方法,类,接口。
擦除是在代码编译过程中将具体的类型都抹除。

前面说过,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文件:
Node:

  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
}

Node

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文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。

 

既然说类型变量会在编译的时候擦除掉,那为什么我们往ArrayList<String> arrayList=new ArrayList<String>();所创建的数组列表arrayList中,不能使用add方法添加整形呢?不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

java编译器是通过先检查代码中泛型的具体类型,然后再进行类型擦除,在进行编译的。

 

那么,这么类型检查是针对谁的呢?我们先看看参数化类型与原始类型的兼容

以ArrayList举例子,以前的写法:

  1. ArrayList arrayList=new ArrayList();  

现在的写法:

  1. ArrayList<String>  arrayList=new ArrayList<String>();  


如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:

ArrayList<String> arrayList1=new ArrayList(); //第一种 情况  

ArrayList arrayList2=new ArrayList<String>();//第二种 情况  

这样是没有错误的,不过会有个编译时警告。

不过在第一种情况,可以实现与 完全使用泛型参数一样的效果,第二种则完全没效果。

因为,本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用arrayList1 来调用它的方法,比如说调用add()方法。所以arrayList1引用能完成泛型类型的检查。而引用arrayList2没有使用泛型,所以不行。

举例子:

public class Test10 {  

  1.     public static void main(String[] args) {  
  2.           
  3.         //  
  4.         ArrayList<String> arrayList1=new ArrayList();  
  5.         arrayList1.add("1");//编译通过  
  6.         arrayList1.add(1);//编译错误  
  7.         String str1=arrayList1.get(0);//返回类型就是String  
  8.           
  9.         ArrayList arrayList2=new ArrayList<String>();  
  10.         arrayList2.add("1");//编译通过  
  11.         arrayList2.add(1);//编译通过  
  12.         Object object=arrayList2.get(0);//返回类型就是Object  
  13.           
  14.         new ArrayList<String>().add("11");//编译通过  
  15.         new ArrayList<String>().add(22);//编译错误  
  16.         String string=new ArrayList<String>().get(0);//返回类型就是String  
  17.     }  
  18. }  

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下ArrayList和get方法:

  1. public E get(int index) {  
  2.     RangeCheck(index);  
  3.     return (E) elementData[index];  
  4.     }  

写了个简单的测试代码:

public class Test {  

  1. public static void main(String[] args) {  
  2. ArrayList<Date> list=new ArrayList<Date>();  
  3. list.add(new Date());  
  4. Date myDate=list.get(0);  //编译通过
  5. }  

Date myDate=list.get(0);编译期为什么能通过?

因为类型检查的时候,发现列表内存储的是Date类型的数据,所以这句话没有强转也是可以的。

运行期为什么能通过?

然后反编了下字节码,如下

public static void main(java.lang.String[]);  

  1. Code:  
  2. 0: new #16 // class java/util/ArrayList  
  3. 3: dup  
  4. 4: invokespecial #18 // Method java/util/ArrayList."<init  
  5. :()V  
  6. 7: astore_1  
  7. 8: aload_1  
  8. 9: new #19 // class java/util/Date  
  9. 12: dup  
  10. 13: invokespecial #21 // Method java/util/Date."<init>":()  
  11.   
  12. 16: invokevirtual #22 // Method java/util/ArrayList.add:(L  
  13. va/lang/Object;)Z  
  14. 19: pop  
  15. 20: aload_1  
  16. 21: iconst_0  
  17. 22: invokevirtual #26 // Method java/util/ArrayList.get:(I  
  18. java/lang/Object;  
  19. 25: checkcast #19 // class java/util/Date  
  20. 28: astore_2  
  21. 29: return  


看第22 ,它调用的是ArrayList.get()方法,方法返回值是Object,说明类型擦除了。然后第25,它做了一个checkcast操作,即检查类型#19, 在在上面找#19引用的类型,他是
9: new #19 // class java/util/Date
是一个Date类型,即做Date类型的强转。
所以不是在get方法里强转的,是在你调用的地方强转的。

 

3.3泛型语法

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());
    }
}

通过extend关键字,编译器会将最后类型都擦除为People类型,就好像最开始我们看见的原始代码一样。

3.4 泛型与向上转型的概念

先讲一讲几个概念:

  • 协变:子类能向父类转换 Animal a1=new Cat();
  • 逆变: 父类能向子类转换 Cat a2=(Cat)a1;
  • 不变: 两者均不能转变

对于协变,我们见得最多的就是多态,而逆变常见于强制类型转换。
这好像没什么奇怪的。但是看以下代码:

public static void error(){
   Object[] nums=new Integer[3];
   nums[0]=3.2;
   nums[1]="string"; //运行时报错,nums运行时类型是Integer[]
   nums[2]='2';
 }

因为数组是协变的,因此Integer[]可以转换为Object[],在编译阶段编译器只知道numsObject[]类型,而运行时nums则为Integer[]类型,因此上述代码能够编译,但是运行会报错。

这就是常见的人们所说的数组是协变的。这里带来一个问题,为什么数组要设计为协变的呢?既然不让运行,那么通过编译有什么用?

答案是在泛型还没出现之前,数组协变能够解决一些通用的问题:

public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }
/**
 * 摘自JDK 1.8 Arrays.equals()
 */
  public static boolean equals(Object[] a, Object[] a2) {
        //...
        for (int i=0; i<length; i++) {
            Object o1 = a[i];
            Object o2 = a2[i];
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        //..
        return true;
    }

可以看到,只操作数组本身,而关心数组中具体保存的原始,或则是不管什么元素,取出来就作为一个Object存储的时候,只用编写一个Object[]就能写出通用的数组参数方法。比如:

Arrays.sort(new Student[]{...})
Arrays.sort(new Apple[]{...})

等,但是这样的设计留下来的诟病就是偶尔会出现对数组元素有具体的操作的代码,比如上面的error()方法。

泛型的出现,是为了保证类型安全的问题,可以看到数组协变会引起类型安全问题,所以如果将泛型也设计为协变的话,那也就违背了泛型最初设计的初衷,因此在Java中,泛型是不变的,什么意思呢?

List<Number> 和 List<Integer> 是没有任何关系的,即使Integer 是 Number的子类

也就是对于

public static void test(List<Number> nums){...}

方法,是无法传递一个List<Integer>参数的

假设编译没有错误,那么当我们使用nums引用add 数据的时候,如果add 了一个long 类型的数据,编译可以通过,但是实际执行的 却无法转化为 Integer 类型的,所以为了避免类型转化异常,所以泛型类型不支持协变。

逆变一般常见于强制类型转换。

Object obj="test";
String str=(String)obj;

原理便是Java 反射机制能够记住变量obj的实际类型,在强制类型转换的时候发现obj实际上是一个String类型,于是就正常的通过了运行。

 

  1.           ArrayList<Object> arrayList1=new ArrayList<Object>();  
  2.           arrayList1.add(new Object());  
  3.           arrayList1.add(new Object());  
  4.           ArrayList<String> arrayList2=arrayList1;//编译错误

我们先假设它编译没错。那么当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的。),可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

 

泛型与向上转型的实现

前面说了这么多,应该关心的问题在于,如何解决既能使用数组协变带来的方便性,又能得到泛型不变带来的类型安全?

答案依然是extend,super关键字与通配符? 泛型重载了extendsuper关键字来解决通用泛型的表示。

注意:这句话可能比较熟悉,没错,前面说过extend还被用来指定擦除到的具体类型,比如<E extend Fruit>,表示在运行时将E替换为Fruit,注意E表示的是一个具体的类型,但是这里的extend和通配符连续使用<? extend Fruit>这里通配符?表示一个通用类型,它所表示的泛型在编译的时候,被指定的具体的类型必须是Fruit的子类。比如List<? extend Fruit> list= new ArrayList<Apple>ArrayList<>中指定的类型必须是Apple,Orange等。不要混淆。

概念麻烦,直接看代码:

协变泛型 List <? extend Fruit> fruit 既能使用数组协变带来的方便性,又能得到泛型不变带来的类型安全,

可以传递 List<Apple> 和List<Orange> 对象 ,但是fruit对象不可以对元素做增删操作


public static  void playFruit(List < ? extends Fruit> list){
    //do somthing
}

public static void main(String[] args) {
    List<Apple> apples=new ArrayList<>();
    List<Orange> oranges=new ArrayList<>();
    List<Food> foods =new ArrayList<>();
    playFruit(apples);
    playFruit(oranges);
    //playFruit(foods); 编译错误
}

可以看到,参数List < ? extend Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是继承自Fruit的。

这样便解决了泛型无法向上转型的问题,前面说过,数组也能向上转型,但是存取元素有问题啊,这里继续深入,看看泛型是怎么解决这一问题的。

  public static  void playFruit(List < ? extends  Fruit> list){
         list.add(new Apple());
    }

向传入的list添加元素,你会发现编译器直接会报错

逆变泛型

public  static  void playFruitBase(List < ? super  Fruit> list){
     //..
}

public static void main(String[] args) {
    List<Apple> apples=new ArrayList<>();
    List<Food> foods =new ArrayList<>();
    List<Object> objects=new ArrayList<>();
    playFruitBase(foods);
    playFruitBase(objects);
    //playFruitBase(apples); 编译错误
}
    

同理,参数List < ? super Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是Fruit的父类类型。

思考: 为什么要这么麻烦要区分开到底是xxx的父类还是子类,不能直接使用一个关键字表示么?

前面说过,数组的协变之所以会有问题是因为在对数组中的元素进行存取的时候出现的问题,只要不对数组元素进行操作,就不会有什么问题,因此可以使用通配符?达到此效果:

public static void playEveryList(List < ?> list){
    //..
}

对于playEveryList方法,传递任何类型的List都没有问题,但是你会发现对于list参数,你无法对里面的元素存和取。这样便达到了上面所说的安全类型的协变数组的效果。

但是觉得多数时候,我们还是希望对元素进行操作的,这就是extendsuper的功能。

<? extend Fruit>表示传入的泛型具体类型必须是继承自Fruit,那么我们可以里面的元素一定能向上转型为Fruit。但是也仅仅能确定里面的元素一定能向上转型为Fruit

public static  void playFruit(List < ? extends  Fruit> list){
     Fruit fruit=list.get(0);
     //list.add(new Apple());
}

比如上面这段代码,可以正确的取出元素,因为我们知道所传入的参数一定是继承自Fruit的,比如

List<Apple> apples=new ArrayList<>();
List<Orange> oranges=new ArrayList<>();

都能正确的转换为Fruit
但是我们并不知道里面的元素具体是什么,有可能是Orange,也有可能是Apple,因此,在list.add()的时候,就会出现问题,有可能将Apple放入了Orange里面,因此,为了不出错,编译器会禁止向里面加入任何元素。这也就解释了协变中使用add会出错的原因。


同理:

<? super Fruit>表示传入的泛型具体类型必须是Fruit父类,那么我们可以确定只要元素是Fruit以及能转型为Fruit的,一定能向上转型为对应的此类型,比如:

    public  static  void playFruitBase(List < ? super  Fruit> list){
        list.add(new Apple());
    }

因为Apple继承自Fruit,而参数list最终被指定的类型一定是Fruit的父类,那么Apple一定能向上转型为对应的父类,因此可以向里面存元素。

但是我们只能确定他是Furit的父类,并不知道具体的“上限”。因此无法将取出来的元素统一的类型(当然可以用Object)。比如

List<Eatables> eatables=new ArrayList<>();
List<Food> foods=new ArrayList<>();

除了

Object obj;

obj=eatables.get(0);
obj=foods.get(0);

之外,没有确定类型可以修饰obj以达到类似的效果。

针对上述情况。我们可以总结为:PECS原则,Producer-Extend,Customer-Super,也就是泛型代码是生产者,使用Extend,泛型代码作为消费者Super

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

泛型的阴暗角落

通过擦除而实现的泛型,有些时候会有很多让人难以理解的规则,但是了解了泛型的真正实现又会觉得这样做还是比较合情合理。下面分析一下关于泛型在应用中有哪些奇怪的现象:

1 擦除的地点---边界

    static <T> T[] toArray(T... args) {

        return args;
    }

    static <T> T[] pickTwo(T a, T b, T c) {
        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return toArray(a, b);
            case 1: return toArray(a, c);
            case 2: return toArray(b, c);
        }
        throw new AssertionError(); // Can't get here
    }

    public static void main(String[] args) {

        String[] attributes = pickTwo("Good", "Fast", "Cheap");
    }

这是在《Effective Java》中看到的例子,编译此代码没有问题,但是运行的时候却会类型转换错误:Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;

当时对泛型并没有一个很好的认识,一直不明白为什么会有Object[]转换到String[]的错误。现在我们来分析一下:

  • 首先看toArray方法,由本章最开始所说泛型使用擦除实现的原因是为了保持有泛型和没有泛型所产生的代码一致,那么:
    static <T> T[] toArray(T... args) {
        return args;
    }

static Object[] toArray(Object... args){
    return args;
}

生成的二进制文件是一致的。

进而剥开可变数组的语法糖:

static Object[] toArray(Object[] args){
    return args;
}
    static <T> T[] pickTwo(T a, T b, T c) {

        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return toArray(a, b);
            case 1: return toArray(a, c);
            case 2: return toArray(b, c);
        }

        throw new AssertionError(); // Can't get here
    }

    static  Object[] pickTwo(Object a, Object b, Object c) {
        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return toArray(new Object[]{a,b});//可变参数会根据调用类型转换为对应的数组,这里a,b,c都是Object
            case 1: return toArray(new Object[]{a,b});
            case 2: return toArray(new Object[]{a,b});
        }

        throw new AssertionError(); // Can't get here
    }

是一致的。
那么调用pickTwo方法实际编译器会帮我进行类型转换

    public static void main(String[] args) {
        String[] attributes =(String[])pickTwo("Good", "Fast", "Cheap");
    }

可以看到,问题就在于可变参数那里,使用可变参数编译器会自动把我们的参数包装为一个数组传递给对应的方法,而这个数组的包装在泛型中,会最终翻译为new Object,那么toArray接受的实际类型是一个Object[],当然不能强制转换为String[]

上面代码出错的关键点就在于泛型经过擦除后,类型变为了Object导致可变参数直接包装出了一个Object数组产生的类型转换失败。

2 基类劫持

public interface Playable<T>  {
    T play();
}

public class Base implements  Playable<Integer> {
    @Override
    public Integer play() {
        return 4;
    }
}

public class Derived extend Base implements Playable<String>{
    ...
}

可以发现在定义Derived类的时候编译器会报错。
观察Derived的定义可以看到,它继承自Base
那么它就拥有一个Integer play()和方法,继而实现了Playable<String>接口,也就是它必须实现一个String play()方法。对于Integer play()String play()两个方法的函数签名相同,但是返回类型不同,这样的方法在Java 中是不允许共存的:

public static void main(String[] args){
    new Derived().play();
}

编译器并不知道应该调用哪一个play()方法。

3 自限定类型

自限定类型简单点说就是将泛型的类型限制为自己以及自己的子类。最常见的在于实现Compareable接口的时候:

public class Student implements Comparable<Student>{
    
}

这样就成功的限制了能与Student相比较的类型只能是Student,这很好理解。

但是正如Java 中返回类型是协变的:

public class father{
    public Number test(){
        return nll;
    }
}


public class Son extend father{
    @Override
    public Interger test(){
        return null;
    }
}

有些时候对于一些专门用来被继承的类需要参数也是协变的。比如实现一个Enum:

public abstract class Enum implements Comparable<Enum>,Serializable{
    @Override
    public int compareTo(Enum o) {
        return 0;
    }
}

这样是没有问题的,但是正如常规所说,假如PenCup都继承于Enum,但是按道理来说笔和杯子之间相互比较是没有意义的,也就是说在EnumcompareTo(Enum o)方法中的Enum这个限定词太宽泛,这个时候有两种思路:

  1. 子类分别自己实现Comparable接口,这样就可以规定更详细的参数类型,但是由于前面所说,会出现基类劫持的问题
  2. 修改父类的代码,让父类不实现Comparable接口,让每个子类自己实现即可,但是这样会有大量一模一样的代码,只是传入的参数类型不同而已。

而更好的解决方案便是使用泛型的自限定类型:

public abstract class Enum<E extend Enum<E>> implements Comparable<E>,Serializable{
    @Override
    public int compareTo(E o) {
        return 0;
    }
    
}

泛型的自限定类型比起传统的自限定类型有个更大的优点就是它能使泛型的参数也变成协变的。

这样每个子类只用在集成的时候指定类型

public class Pen extends Enum<Pen>{}
public class Cup extends Cup<Cup>{}

便能够在定义的时候指定想要与那种类型进行比较,这样达到的效果便相当于每个子类都分别自己实现了一个自定义的Comparable接口。

自限定类型一般用在继承体系中,需要参数协变的时候。

类型擦除与多态的冲突和解决方法

现在有这样一个泛型类:

class Pair<T> {  

  1.     private T value;  
  2.     public T getValue() {  
  3.         return value;  
  4.     }  
  5.     public void setValue(T value) {  
  6.         this.value = value;  
  7.     }  
  8. }  

然后我们想要一个子类继承它

class DateInter extends Pair<Date> {  

  1.     @Override  
  2.     public void setValue(Date value) {  
  3.         super.setValue(value);  
  4.     }  
  5.     @Override  
  6.     public Date getValue() {  
  7.         return super.getValue();  
  8.     }  
  9. }  

以上代码中 @Override   注解标识的方法和 泛型擦除后的方法是否冲突?

实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

          class Pair {  

  1.     private Object value;  
  2.     public Object getValue() {  
  3.         return value;  
  4.     }  
  5.     public void setValue(Object  value) {  
  6.         this.value = value;  
  7.     }  
  8. }  

再看子类的两个重写的方法的类型:

        @Override  

  1. public void setValue(Date value) {  
  2.     super.setValue(value);  
  3. }  
  4. @Override  
  5. public Date getValue() {  
  6.     return super.getValue();  
  7. }  

先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果是在普通的继承关系中,根本就不会是重写,而是重载。

我们在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException {  

  1.         DateInter dateInter=new DateInter();  
  2.         dateInter.setValue(new Date());                  
  3.         dateInter.setValue(new Object());//编译错误  
  4.  }  

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

为什么会这样呢?

原因是这样的,我们传入父类的泛型类型是Date,Pair<Date>,我们的本意是将泛型类变为如下:

          class Pair {  

  1.     private Date value;  
  2.     public Date getValue() {  
  3.         return value;  
  4.     }  
  5.     public void setValue(Date value) {  
  6.         this.value = value;  
  7.     }  
  8. }  

然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。

可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

      class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  

  1.   com.tao.test.DateInter();  
  2.     Code:  
  3.        0: aload_0  
  4.        1: invokespecial #8                  // Method com/tao/test/Pair."<init>"  
  5. :()V  
  6.        4: return  
  7.   
  8.   public void setValue(java.util.Date);  //我们重写的setValue方法  
  9.     Code:  
  10.        0: aload_0  
  11.        1: aload_1  
  12.        2: invokespecial #16                 // Method com/tao/test/Pair.setValue  
  13. :(Ljava/lang/Object;)V  
  14.        5: return  
  15.   
  16.   public java.util.Date getValue();    //我们重写的getValue方法  
  17.     Code:  
  18.        0: aload_0  
  19.        1: invokespecial #23                 // Method com/tao/test/Pair.getValue  
  20. :()Ljava/lang/Object;  
  21.        4: checkcast     #26                 // class java/util/Date  
  22.        7: areturn  
  23.   
  24.   public java.lang.Object getValue();     //编译时由编译器生成的桥方法  
  25.     Code:  
  26.        0: aload_0  
  27.        1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法  
  28. ;  
  29.        4: areturn  
  30.   
  31.   public void setValue(java.lang.Object);   //编译时由编译器生成的桥方法  
  32.     Code:  
  33.        0: aload_0  
  34.        1: aload_1  
  35.        2: checkcast     #26                 // class java/util/Date  
  36.        5: invokevirtual #30                 // Method setValue:(Ljava/util/Date;   去调用我们重写的setValue方法  
  37. )V  
  38.        8: return  
  39. }  

从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。

setValue方法是为了解决类型擦除与多态之间的冲突。

而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

那么父类的setValue方法如下:

  1.     public Object getValue() {  
  2.         return super.getValue();  
  3.     }  

而子类重写的方法是:

  1. public Date getValue() {  
  2.         return super.getValue();  
  3.     }  

其实这在普通的类继承中也是普遍存在的重写,这就是协变

 

泛型使用:

1 在调用泛型方法的时候,可以指定泛型类型,也可以不指定泛型类型。

在不指定泛型类型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。

在指定泛型类型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。

public class Test2{  

  1.     public static void main(String[] args) {  
  2.         /**不指定泛型的时候*/  
  3.         int i=Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
  4.         Number f=Test2.add(1, 1.2);//这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number  
  5.         Object o=Test2.add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object  
  6.   
  7.                 /**指定泛型的时候*/  
  8.         int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类  
  9.         int b=Test2.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float  
  10.         Number c=Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
  11.     }  
  12.       
  13.     //这是一个简单的泛型方法  
  14.     public static <T> T add(T x,T y){  
  15.         return y;  
  16.     }  
  17. }  


2 在泛型类中,不指定泛型的时候,泛型类型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList中可以放任意类型的对象。

举例:

  1. public static void main(String[] args) {  
  2.         ArrayList arrayList=new ArrayList();  
  3.         arrayList.add(1);  
  4.         arrayList.add("121");  
  5.         arrayList.add(new Date());  
  6.     }  

 

3 不能抛出也不能捕获泛型类的具体类。事实上,泛型类扩展Throwable都不合法。例如:下面的定义将不会通过编译:

             public class Problem<T> extends Exception{......}  

为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义:

               try{  

  1. }catch(Problem<Integer> e1){  
  2. 。。  
  3. }catch(Problem<Number> e2){  
  4. ...  
  5. }   

类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就相当于下面的这样

try{  

  1. }catch(Problem<Object> e1){  
  2. 。。  
  3. }catch(Problem<Object> e2){  
  4. ...  

这个当然就是不行的。就好比,catch两个一模一样的普通异常,不能通过编译一样:

         try{  

  1. }catch(Exception e1){  
  2. 。。  
  3. }catch(Exception  e2){//编译错误  
  4. ...  

不能在catch子句中使用泛型变量

                   public static <T extends Throwable> void doWork(Class<T> t){  

  1.         try{  
  2.             ...  
  3.         }catch(T e){ //编译错误  
  4.             ...  
  5.         }  
  6.    }  

因为泛型信息在编译的时候已经变为原始类型,也就是说上面的T会变为原始类型Throwable,那么如果可以再catch子句中使用泛型变量,那么,下面的定义呢:

         public static <T extends Throwable> void doWork(Class<T> t){  

  1.         try{  
  2.             ...  
  3.         }catch(T e){ //编译错误  
  4.             ...  
  5.         }catch(IndexOutOfBounds e){  
  6.         }                           
  7.  }  

根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。即使你在使用该静态方法的使用T是ArrayIndexOutofBounds,在编译之后还是会变成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子类,违背了异常捕获的原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。

 

但是在异常声明中可以使用类型变量。下面方法是合法的。

public static<T extends Throwable> void doWork(T t) throws T{  

  1.     try{  
  2.         ...  
  3.     }catch(Throwable realCause){  
  4.         t.initCause(realCause);  
  5.         throw t;   
  6.     }  

上面的这样使用是没问题的。

4泛型类型的实例化 

    不能new泛型类型。如,first = new T(); //ERROR  
    类型擦除会使这个操作做成new Object()。
    不能用new操作建立一个泛型数组。

  1. public<T> T[] minMax(T[] a){  
  2.      T[] mm = new T[2]; //ERROR  
  3. }  

   类似的,擦除会使这个方法总是构造一个Object[2]数组。

  但是,可以用反射构造泛型对象和数组。
  利用反射,调用Array.newInstance:

publicstatic <T extends Comparable> T[] sort(T[] a)  

  1.    {  
  2.       T[] mm == (T[])Array.newInstance(a.getClass().getComponentType(),2);    
  3.       // 以替换掉以下代码  
  4.       // Obeject[] mm = new Object[2];  
  5.       // return (T[]) mm; 
  6.    }  

数组(这个不属于类型擦除引起的问题)

不能声明参数化类型的数组。如:

  1. Pair<String>[] table = new Pair<String>(10); //ERROR  

如果需要收集参数化类型对象,直接使用ArrayList:ArrayList<Pair<String>>最安全且有效。

6  类型擦除后的冲突

当泛型类型被擦除后,创建条件不能产生冲突。如果在Pair类中添加下面的equals方法:

class Pair<T>   {  

  1.     public boolean equals(T value) {  
  2.         return null;  
  3.     }   
  4. }  

考虑一个Pair<String>。从概念上,它有两个equals方法:

boolean equals(String); //在Pair<T>中定义

boolean equals(Object);  //从object中继承

但是,这只是一种错觉。实际上,擦除后方法

boolean equals(T)

变成了方法 boolean equals(Object)

这与Object.equals方法是冲突的!当然,补救的办法是重新命名引发错误的方法。

 

7 、泛型在静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

举例说明:

  1. public class Test2<T> {    
  2.     public static T one;   //编译错误    
  3.     public static  T show(T one){ //编译错误    
  4.         return null;    
  5.     }    
  6. }    

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

但是要注意区分下面的一种情况:

  1. public class Test2<T> {    
  2.     public static <T >T show(T one){//这是正确的    
  3.         return null;    
  4.     }    
  5. }    

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。

8 泛型方法的规则和使用: 

1)所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。

2)类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。

3)泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

下面举一些泛型方法的示例:

/**
 * public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 * 只有声明了<T>等的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 * <T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 * 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public class GenericMethods {
    public <T> void genericMethod1(T t){
        System.out.println(t.getClass().getName());
    }
    public <T> T genericMethod2( Class<T> tClass ) throws InstantiationException ,
            IllegalAccessException {
        T t = tClass.newInstance();
        return t;
    }
    public static <T> void genericMethod3( T[] inputArray ) {
        // 输出数组元素            
        for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
        }
    }
}

 

泛型类是在实例化类的时候指明泛型的具体类型,而泛型方法是在调用方法的时候指明泛型的具体类型 。

泛型方法可以在任何地方和任何场景中使用,但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下:

/**
 * 这是一个泛型类
 */
class GenericClassDemo<T> {
    /**
     * 这个不是泛型方法,只是使用了泛型类中已声明的T
     */
    public void show1(T t){
        System.out.println(t.toString());
    }
    /**
     * 泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
     * 由于下面的泛型方法在声明的时候声明了泛型<E>,因此即使在泛型类中并未声明泛型,
     * 编译器也能够正确识别泛型方法中识别的泛型。
     */
    public <E> void show2(E e){
        System.out.println(e.toString());
    }
    /**
     * 在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型;
     * 可以与泛型类中声明的T不是同一种类型。
     */
    public <T> void show3(T t){
        System.out.println(t.toString());
    }
}
 

泛型方法和可变参数

泛型方法与可变参数列表能很好地共存:

public class GenericMethodTest {
    public <T> void printArgs( T... args ){
        for(T t : args){
            System.out.print(t + " ");
        }
    }
    public static <T> List<T> toList(T... args){
        List<T> result = new ArrayList<T>();
        for(T item:args)
            result.add(item);
        return result;    
    }     
    public static void main(String[] args) {
        GenericMethodTest gmt = new GenericMethodTest();
        gmt.printArgs("A","B"); // A B
        List ls = GenericMethodTest.toList("A");
        System.out.println(ls); // [A]
        ls = GenericMethodTest.toList("A","B","C"); 
        System.out.println(ls); // [A,B,C]
    }
 
}
 

静态方法使用泛型

静态方法无法访问类上定义的泛型,如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

public class GenericTest<T> {
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明
     * 即使静态方法不可以使用泛型类中已经声明过的泛型(需将这个方法定义成泛型方法)
     * 如:public static void genericMethod(T t){..},此时编译器会提示错误信息:
     *    "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void  genericMethod(T t) {
        // ...
    }
}

 

“<T>"和"<?>",首先要区分开两种不同的场景:

  1. 类型参数“<T>”主要用于声明泛型类或泛型方法。
  2. 无界通配符“<?>”主要用于使用泛型类或泛型方法

 

T 代表一种类型 ,用法如下:

加在类上:    class SuperClass<T extends Object>{}

加在方法上: public  <T extends Object>   void fromArrayToCollection(T[] a, Collection<T> c);

 

?是通配符,泛指所有类型 使用方法如下

1 一般用于定义一个引用变量,这么做的好处是,如下所示,定义一个sup的引用变量,就可以指向多个对象。

SuperClass<?> sup = new SuperClass<String>("lisi");

sup = new SuperClass<People>(new People());

sup = new SuperClass<Animal>(new Animal());

若不用?,用固定的类型的话,则:

SuperClass<String> sup1 = new SuperClass<String>("lisi");

SuperClass<People> sup2 = new SuperClass<People>("lisi");

SuperClass<Animal> sup3 = new SuperClass<Animal>("lisi");

这就是?通配符的好处。

2 加在方法上

public static void printList5(List<? extends Object> list);

public static List<?> printList6(List<? extends Object> list);

9 泛型通配符T,E,K,V区别

这些全都属于java泛型的通配符,刚开始我看到这么多通配符,一下晕了,这几个其实没什么区别,只不过是一个约定好的代码,也就是说

使用大写字母A,B,C,D......X,Y,Z定义的,就都是泛型,把T换成A也一样,这里T只是名字上的意义而已

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


参考   https://www.cnblogs.com/dengchengchao/p/9717097.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值