谈谈Java里头的泛型


前言

当我们无法确定一个方法的参数、或者返回型是啥的时候,我们可以尝试通过泛型来解决。而泛型的种类有:泛型接口、泛型类、泛型方法、泛型变量这四种。

尽管Java里头提供了这么些泛型方式,但它实际上却是伪泛型。在经过编译后,通过对字节码的查看,我们可以发现原先定义的泛型类型都被擦除了。


分析

在如下的例子中,申明了一个Test泛型类,其中还申明两个集合,而List是一个泛型接口。它的定义如下,这里我们分别申明了一个String类型的集合和一个Integer类型的集合。

public interface List<E> 
					extends Collection<E>
import java.util.ArrayList;
import java.util.List;

/***
 *
 * @Author:fsn
 * @Date: 2020/5/17 0:43
 * @Description
 */


public class Test<V> {
    
    
    List<String> list1 = new ArrayList<>();
    List<Integer> list2 = new ArrayList<>();

    V v;

    V get() {
    
    
        return null;
    }
}

接着,我们通过命令javap -s Test.class查查它的字节码的描述信息。

Compiled from "Test.java"
public class top.fsn.common.Test<V> {
    
    
  java.util.List<java.lang.String> list1;
    descriptor: Ljava/util/List;
  java.util.List<java.lang.Integer> list2;
    descriptor: Ljava/util/List;
  V v;
    descriptor: Ljava/lang/Object;
  public top.fsn.common.Test();
    descriptor: ()V

  V get();
    descriptor: ()Ljava/lang/Object;
}

或者,我们也可以通过javap -c Test命令进行查看字节码

Compiled from "Test.java"
public class top.fsn.common.Test<V> {
    
    
  java.util.List<java.lang.String> list1;

  java.util.List<java.lang.Integer> list2;

  V v;

  public top.fsn.common.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: new           #2                  // class java/util/ArrayList
       8: dup
       9: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      12: putfield      #4                  // Field list1:Ljava/util/List;
      15: aload_0
      16: new           #2                  // class java/util/ArrayList
      19: dup
      20: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      23: putfield      #5                  // Field list2:Ljava/util/List;
      26: return

  V get();
    Code:
       0: aconst_null
       1: areturn
}

从字节码的描述信息中,我们可以看到,我们定义的String集合和Integer集合,最终都是List集合。这也是我们对只用泛型类型集合的方法进行重载时,编译会报错的原因。
在这里插入图片描述
而我们定义的泛型边临V v则被擦除为Obect,而V get()方法的返回类型也被擦除为Object。


引申问题

既然泛型会擦除,那么如下代码又为何报错呢?我们申明了一个String集合,然后添加一个字符串1,再添加一个整型1,可以发发现编译报错了。
在这里插入图片描述
原来,Java编译器真正执行编译工作前还有一项任务,那就是检查代码中泛型的类型,擦除工作是在检查之后发生的。

如果,我们一开始具不申明具体类型,你又会发现上面的代码又可以编译通过。但此时,你会发现它多出了警告,如果通过命令进行编译,它还会提示需要我们添加-Xlint:unchecked参数。
在这里插入图片描述
上面例子之所以可以编译通过,但在使用的时候,比如String s = list.get(0)此时编译就会错误,需要进行强转操作。而指定泛型类型的集合,get操作时却不用?

E elementData(int index) {
    
    
        return (E) elementData[index];
    }

原来集合里头帮我们做了强转。而对于没有指定泛型的集合,这个E相当于Object类型,所以你取出来后,转换具体类型的时候还得需要进行强转。

扫描二维码关注公众号,回复: 12665131 查看本文章

关于通配符的泛型

当我们允许传递的对象类型是某个类A的子类的时候,我们可以尝试这种写法: List<? extends A>

其中,A为该泛型的上限。在传递的时候只要是A类的子类都可以正常传递。如下图所示,只要是Number类的子列,可以正常调用test()方法。
在这里插入图片描述

上限类型的泛型,只能从中取东西,不能往里面放东西。(不考虑反射这种骚操作)

为啥?首先,Java是一门强类型的语言,意味着在具体道使用时,必须明确类型。而List<? extends A>中,作为“人”来说,我们可以知道往里面添加A的哪些子类或自身,但从编译器的角度来说,就没这么智能了,它只知道可能是A的子类,但具体是哪路神仙就不知道了。

void test(List<? extends Number> list) {
    
    
        list.add(new Integer(1));

    }

如下所示,强行编译如上代码,会得到如下代码所示,可以知道它通过一个占位符CAP#1来捕获A或者其子类,并不能明白具体的类型。

F:\pratice\pratice-lock\src\main\java\top\fsn\common>javac Test.java
Test.java:23: 错误: 对于add(Integer), 找不到合适的方法
        list.add(new Integer(1));
            ^
    方法 Collection.add(CAP#1)不适用
      (参数不匹配; Integer无法转换为CAP#1)
    方法 List.add(CAP#1)不适用
      (参数不匹配; Integer无法转换为CAP#1)
  其中, CAP#1是新类型变量:
    CAP#1? extends Number的捕获扩展Number
注: 某些消息已经过简化; 请使用 -Xdiags:verbose 重新编译以获得完整输出
1 个错误

与泛型上限相对应的是就是泛型的下限了, List<? super A>。这里需要注意,它表示的意思不是存放A的父类对象。

List<? super A>跟List<? extend A>相似之处,里面的东西都是A的子类。但List<? super A>是可以往里面添加东西的。

如图所示,我申明了一个List<? super B>,B的子类有C,父类为A,添加A对象的时候编译报错。

在这里插入图片描述
List<? super A>表示泛型的下边界到底该如何理解呢?其实,我们知道向上转换是安全的、隐形的。这里添加的子类,被悄悄的向上转换为父类了。所以这里的下边界,可以理解为允许添加的对象的最低限制类型是A(父类)

这里肯定又会有很多“华生”说那为什么List<? extend A>就不会将子类悄悄的转换为父类呢?

这个答案网上有着各路神仙的解答,也有神仙从数学的角度去理解。Oracle官网内容,我也看了下,也没有做详细说明。

那我只能从凡人的角度理解了,如果List<? extend A>也可以悄悄的将子类转换为父类,那它也应该可以add才是,如果这样List<? super A>还开发出来干嘛,没事找事啊!

两者之所以有区别,我觉得是开发者给出的为了应对不同业务场景而设计的两种解决方案,而如何实现,只要在设计编译器的时候加些逻辑判断:

if? extend A){
    
    
  // do something
} else if (? super A) {
    
    
// do something
}

(完,后边部分纯属个人理解,未经官方证实,各位看官就当一乐~~也可写下您的理解。)

猜你喜欢

转载自blog.csdn.net/legendaryhaha/article/details/106170338
今日推荐