Java泛型详细分析

什么是泛型

Java泛型( generics) 是JDK 5中引⼊的⼀个新特性, 允许在定义类和接口的时候使⽤类型参数( type parameter) 。

声明的类型参数在使⽤时⽤具体的类型来替换。 泛型最主要的应⽤是在JDK 5中的新集合类框架中。

泛型最⼤的好处是可以提⾼代码的复⽤性。 以List接⼜为例,我们可以将String、 Integer等类型放⼊List中, 如不⽤泛型, 存放String类型要写⼀个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题。

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

程序运行出现异常java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。这是因为我们第二个存放的元素是Integer类型不能被强转为String

ArrayList可以看成是一个什么都能存的容器。假设我们每次取相应的类型数据都需要相对应的工具。ArrayList中存了String,Integer.....。我们可以使用String相对应的工具取出String类型数据。但是我们却不能用它来取出Integer型的数据。(强取报错)

而且我们很多时候从这个容器中拿东西是不知道每次要取出的数据的类型。所以我们就需要一个专门存放特定一种类型的容器。

List<String> list = new ArrayList<>(); 只存String类型的容器

List<Integer> list = new ArrayList<>(); 只存Integer类型的容器

List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在编译阶段,编译器就会报错

这就是泛型的最常用的使用。

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

泛型的使用

<mark>泛型的类型参数只能是类类型(包括自定义类),不能是简单类型</mark>

泛型类

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

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

一个最普通的泛型类

//此处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;
    }
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());

泛型测试: key is 123456
泛型测试: key is key_vlaue

定义的泛型类,就一定要传入泛型类型实参么?

<mark>并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。</mark>

//并未传人泛型实参 例如  Generic<String> generic = new Generic<>();
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
泛型测试: key is 111111
泛型测试: key is 4444
泛型测试: key is 55.55
泛型测试: key is false

<mark>不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。</mark>

  List<Integer> list = new ArrayList<>();
    //可以使用 true
  if (list instanceof  List){}
    //编译报错 确切的泛型类型List<Integer>
  if (list instanceof List<Integer>){}

泛型接口

泛型接口与泛型类的定义及使用基本相同。

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

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

//未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

//如果不声明泛型 编译器会报错:"Unknown class"
class FruitGenerator implements Generator<T>{}

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

//在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型(Idea我帮我们自动替换)
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)];
    }
}

泛型方法

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

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

具体看这篇文章中写的。https://blog.csdn.net/s10461/article/details/53941091。太多了,分多种情况。

泛型通配符

无边界的通配符,固定上边界的通配符,固定下边界的通配符

无边界通配符

一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer>等所有List<具体类型实参>的父类。

类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;            

上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么

List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

最后一行代码成立吗?编译会通过吗?答案是否定的。

编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>List<Base>有继承关系。

但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法

public class TestWildCards {

    public void testWildCards(Collection<?> collection){

        collection.add(123);//报错
        collection.add("hello");//报错
        collection.add(new Object());//报错

        collection.size();
    }
}

有人说,<?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求还是很常见的吧。

有同学可能会想,<?>既然作用这么渺小,那么为什么还要引用它呢? �

个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。(????这或许就是高手这样写代码吧)

上下界限定符

<? extends T><? super T>是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。

<mark><? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”,即泛型中的类必须为当前类的子类或当前类。</mark>

<mark><? super T>:是指 “下界通配符(Lower Bounds Wildcards)”,即泛型中的类必须为当前类或者其父类。</mark>

public class Food {}
public class Fruit extends Food {}
public class Apple extends Fruit {}
public class Banana extends Fruit{}

public class GenericTest {

    public void testExtends(List<? extends Fruit> list){

        //报错,extends为上界通配符,只能取值,不能放.
        //因为Fruit的子类不只有Apple还有Banana,这里不能确定具体的泛型到底是Apple还是Banana,所以放入任何一种类型都会报错
        //list.add(new Apple());

        //可以正常获取
        Fruit fruit = list.get(1);
    }

    public void testSuper(List<? super Fruit> list){

        //super为下界通配符,可以存放元素,但是也只能存放当前类或者子类的实例,以当前的例子来讲,
        //无法确定Fruit的父类是否只有Food一个(Object是超级父类)
        //因此放入Food的实例编译不通过
        list.add(new Apple());
//        list.add(new Food());

        Object object = list.get(1);
    }
}

extends为上界通配符,只能取值,不能放.

super为下界通配符,可以存放元素,但是也只能存放当前类或者子类的实例,以当前的例子来讲。

在testExtends方法中,因为泛型中用的是extends,在向list中存放元素的时候,我们并不能确定List中的元素的具体类型,即可能是Apple也可能是Banana。因此调用add方法时,不论传入new Apple()还是new Banana(),都会出现编译错误。

理解了extends之后,再看super就很容易理解了,即我们不能确定testSuper方法的参数中的泛型是Fruit的哪个父类,因此在调用get方法时只能返回Object类型。结合extends可见,在获取泛型元素时,使用extends获取到的是泛型中的上边界的类型(本例子中为Fruit),范围更小。

在使用泛型时,存取元素时用super,获取元素时,用extends。

频繁往外读取内容的,适合用上界Extends。经常往里插入的,适合用下界Super。

K T V E ? object等的含义

  1. T - Type(Java 类)
  2. K - Key(键)
  3. V - Value(值)
  4. N - Number(数值类型)
  5. ? - 表示不确定的java类型(无限制通配符类型)
  6. Object - 是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。

类型擦除

类型擦除(type erasue)指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。

类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。

先给大家奉上一道经典的测试题。

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
        
System.out.println(l1.getClass() == l2.getClass());//ture

面的代码中涉及到了泛型,而输出的结果缘由是类型擦除

类型擦除的主要过程如下:

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  2. 移除所有的类型参数。
public static void main(String[] args) {  
    Map<String, String> map = new HashMap<String, String>();  
    map.put("name", "hollis");  
    map.put("age", "22");  
    System.out.println(map.get("name"));  
    System.out.println(map.get("age"));  
}  

反编译后

public static void main(String[] args) {  
    Map map = new HashMap();  
    map.put("name", "hollis");  
    map.put("age", "22"); 
    System.out.println((String) map.get("name"));  
    System.out.println((String) map.get("age"));  
}  

我们发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型。

interface Comparable<A> {
    public int compareTo(A that);
}

public final class NumericValue implements Comparable<NumericValue> {
    private byte value;

    public NumericValue(byte value) {
        this.value = value;
    }

    public byte getValue() {
        return value;
    }

    public int compareTo(NumericValue that) {
        return this.value - that.value;
    }
}

反编译后

 interface Comparable {
  public int compareTo( Object that);//最顶级的父类型替换
} 

public final class NumericValue
    implements Comparable
{
    public NumericValue(byte value)
    {
        this.value = value;
    }
    public byte getValue()
    {
        return value;
    }
    public int compareTo(NumericValue that)
    {
        return value - that.value;
    }
    public volatile int compareTo(Object obj)
    {
        return compareTo((NumericValue)obj);
    }
    private byte value;
}
public class Collections {
    public static <A extends Comparable<A>> A max(Collection<A> xs) {
        Iterator<A> xi = xs.iterator();
        A w = xi.next();
        while (xi.hasNext()) {
            A x = xi.next();
            if (w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }
}

反编译后

public class Collections
{
    public Collections()
    {
    }
    public static Comparable max(Collection xs)
    {
        Iterator xi = xs.iterator();
        Comparable w = (Comparable)xi.next();
        while(xi.hasNext())
        {
            Comparable x = (Comparable)xi.next();
            if(w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }
}

第2个泛型类Comparable <A>擦除后 A被替换为最左边界ObjectComparable<NumericValue>的类型参数NumericValue被擦除掉,但是这直 接导致NumericValue没有实现接口Comparable的compareTo(Object that)方法,于是编译器充当好人,添加了一个桥接方法

第3个示例中限定了类型参数的边界<A extends Comparable<A>>A,A必须为Comparable<A>的子类,按照类型擦除的过程,先讲所有的类型参数 ti换为最左边界Comparable<A>,然后去掉参数类型A,得到最终的擦除后结果。

<mark>移除所有的类型参数。将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。</mark>

泛型带来的问题

重载

public class GenericTypes {  

    public static void method(List<String> list) {  
        System.out.println("invoke method(List<String> list)");  
    }  

    public static void method(List<Integer> list) {  
        System.out.println("invoke method(List<Integer> list)");  
    }  
}  
image-20201115153056534

上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>另一个是List<Integer> ,但是,这段代码是编译通不过的。因为我们前面讲过,参数List<Integer>List<String>编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。

异常

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

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

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

try{
}catch(Problem<Integer> e1){
。。
}catch(Problem<Number> e2){
...
} 

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

try{
}catch(Problem<Object> e1){
。。
}catch(Problem<Object> e2){
...
//两个一样。。

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

try{
}catch(Exception e1){
。。
}catch(Exception  e2){//编译错误

2.不能再catch子句中使用泛型变量

public static <T extends Throwable> void doWork(Class<T> t){
        try{
            ...
        }catch(T e){ //编译错误
            ...
        }
   }
image-20201115160018663

Cannot catch type parameters不能捕获类型参数

那么如果可以再catch子句中使用泛型变量,那么,下面的定义呢:

public static <T extends Throwable> void doWork(Class<T> t){
        try{
            ...
        }catch(T e){ //编译错误
            ...
        }catch(IndexOutOfBounds e){
        }                         
 }

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

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

   public static<T extends Throwable> void doWork(T t) throws T{
       try{
           t.getMessage();//类型变量
       }catch(Throwable realCause){
           t.initCause(realCause);//类型变量
           throw t; 
       }
  }

静态变量

public class StaticTest{
    public static void main(String[] args){
        GT<Integer> gti = new GT<Integer>();
        gti.var=1;
        GT<String> gts = new GT<String>();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class GT<T>{
    public static int var=0;
    public void nothing(T x){}
}
image-20201115153351073

答案是——2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。

区别

List<Object>和原始类型List之间的区别?

原始类型List和带参数类型List<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。

通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。

它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型List,但却不能把List<String>传递给接受 List<Object>的方法,因为会产生编译错误。

List<?>List<Object>之间的区别?

List<?> 是一个未知类型的List,而List<Object> 其实是任意类型的List。你可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给 List<Object>

?和T有什么区别 ?

T 代表一种类型。

?是通配符,泛指所有类型。

T t = (T) new ArrayList();
? s = (?) new ArrayList();//不可以

泛型数组

sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。

也就是说这个例子是不可以的:List<String>[] ls = new ArrayList<String>[10];

而使用通配符创建泛型数组是可以的:List<?>[] ls = new ArrayList<?>[10];

这样也是可以的:List<String>[] ls = new ArrayList[10];

后面的自己看吧。

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

参考文章

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容