菜鸟笔记
提升您的技术认知

泛型是什么?-ag真人游戏

泛型,一个所有人都知道怎么用,在java世界老生常谈的特性。更需要知其然,知其所以然。

一、泛型是什么

泛型是在jdk1.5引入的参数化类型特性,可以在同一段代码上操作多种数据类型。

1.1 参数化类型

我们以泛型类的使用作为事例,如下:

// 泛型类的定义
public class generics {
  
    // 未知类型
    private t mdata;
    public t getdata() {
  
        return mdata;
    }
    public void setdata(t data) {
  
        this.mdata = data;
    }
}

在泛型类内定义了泛型【t】,此时【t】是一个未知类型。

// 泛型类的使用,将person类作为参数传入泛型类
generics generics = new generics();

在泛型类创建对象时,我们将person类作为参数传入泛型类,此时泛型类内部的【t】就变成了已知类型person。

通过参数传入,作为泛型的类型,就是参数化类型。

二、泛型种类及边界

2.1 泛型种类

1. 泛型接口

public interface base {
  
    public t getdata();
    public void setdata(t data);
    
}

2. 泛型类

public class generics{
  
    private t mdata;
    public t getdata() {
  
        return mdata;
    }
    public void setdata(t data) {
  
        this.mdata = data;
    }
}

3. 泛型方法

// public后面的是泛型方法的关键
public  generics getgenerics() {
  
    return new generics();
}

2.2 泛型边界

以上几种类型均可定义泛型的边界,语法 、,泛型重载了extends的关键字,与通常java中使用的extends不同。

  • < t extends a>:单个边界,a可以是类或接口,只能接收继承或者实现a的类型。
  • < t extends a&b&…>:多个边界,a可以是类或接口,a之后的只能是接口。比如:里面,t必须继承a类型或实现a接口,并且必须实现b和c接口。

三、泛型的好处

3.1 代码更健壮

泛型将集合的类型检测提前到了编译期,保证错误在编译时就会抛出,基本上代码编辑器(android studio、idea等)在书写代码阶段给泛型传入错误类型就会报错。

拥有泛型之前只能在运行时抛出类型转换异常(classcastexception),代码十分脆弱。

// 泛型存在之前
// 集合里存入fruit和dog,编译不会报错
list fruits = new arraylist();
fruits.add(new fruit());
fruits.add(new dog()); // x 错误的插入,直到运行时报错 
// 泛型存在之后
list fruits = new arraylist();
fruits.add(new fruit());
fruits.add(new dog());// x 编译时就会报错

3.2 代码更简洁

泛型省去了类型的强制转换。在没有泛型之前,集合内的对象都会被向上转型为object,所以需要强转。

// 没有泛型之前,获取对象需要强转
fruit fruit = (fruit) fruits.get(0);

3.3 代码复用性强

泛型就是使用参数化类型,在一段代码上操作多种数据类型。比如:对几个类的处理,在逻辑上完全相同,那自然会想这段逻辑代码只写一遍就好了,所以泛型就产生了。

四、泛型的原理

泛型在jdk1.5才出现,为了向下兼容,虚拟机是并不支持泛型的,所以java在编译阶段除了进行类型判断,还对泛型进行了擦除,于是所有的泛型在字节码里都变成了原始类型,和c#的泛型不同,java使用的是伪泛型

4.1 泛型擦除

在编译阶段生成字节码时,会进行泛型擦除,所以我们看下生成的字节码文件,就可以清晰的看到泛型【t】被转换成了object。

// java代码
public class generics {
  
    private t mdata;
    public t getdata() {
  
        return mdata;
    }
    public void setdata(t data) {
  
        this.mdata = data;
    }
}

下面是generics类生成的字节码

// class version 51.0 (51)
// access flags 0x21
// signature ljava/lang/object;
// declaration: com/kproduce/androidstudy/test/generics
public class com/kproduce/androidstudy/test/generics {
  
  // compiled from: generics.java
  // access flags 0x2
  // signature tt;
  // declaration: t
  private ljava/lang/object; mdata
  // access flags 0x1
  public ()v
   l0
    linenumber 6 l0
    aload 0
    invokespecial java/lang/object. ()v
    return
   l1
    localvariable this lcom/kproduce/androidstudy/test/generics; l0 l1 0
    // signature lcom/kproduce/androidstudy/test/generics;
    // declaration: com.kproduce.androidstudy.test.generics
    maxstack = 1
    maxlocals = 1
  // access flags 0x1
  // signature ()tt;
  // declaration: t getdata()
  public getdata()ljava/lang/object;
   l0
    linenumber 10 l0
    aload 0
    getfield com/kproduce/androidstudy/test/generics.mdata : ljava/lang/object;
    areturn
   l1
    localvariable this lcom/kproduce/androidstudy/test/generics; l0 l1 0
    // signature lcom/kproduce/androidstudy/test/generics;
    // declaration: com.kproduce.androidstudy.test.generics
    maxstack = 1
    maxlocals = 1
  // access flags 0x1
  // signature (tt;)v
  // declaration: void setdata(t)
  public setdata(ljava/lang/object;)v
   l0
    linenumber 14 l0
    aload 0
    aload 1
    putfield com/kproduce/androidstudy/test/generics.mdata : ljava/lang/object;
   l1
    linenumber 15 l1
    return
   l2
    localvariable this lcom/kproduce/androidstudy/test/generics; l0 l2 0
    // signature lcom/kproduce/androidstudy/test/generics;
    // declaration: com.kproduce.androidstudy.test.generics
    localvariable data ljava/lang/object; l0 l2 1
    // signature tt;
    // declaration: t
    maxstack = 2
    maxlocals = 2
}

看完上面的代码有的同学就喊了,这不备注里面还是有泛型【t】吗?

是的,你们说的没错!那为什么泛型还在备注里面?这时候要说到反射这个概念。

反射是在运行时对于任何一个类,都可以知道里面所有属性和方法。对于任何一个对象,都可以调用它的方法和属性。是java被视为动态语言的关键。

既然反射要知道所有的方法和属性,但是泛型在字节码里面被进行了擦除,那java就使用备注的方式将泛型偷偷的写入到了字节码里面,保证反射的正常使用。

4.2 泛型擦除原则

  • 如果泛型没有限定(),则用object作为原始类型。
  • 如果有限定(),则用a作为原始类型。
  • 如果有多个限定(),则使用第一个边界a作为原始类型。

五、泛型的限定通配符

通配符是让泛型的转型更灵活

  • 是指“上界通配符”
  • 是指“下界通配符”

5.1 通配符存在的意义

数组是可以向上转型的:

object[] nums = new integer[2];
nums[0] = 1;
nums[1] = "string"; // nums在运行时是一个interger数组,所以会报错

再看一段会报错的泛型转型代码:

// apple extends fruit,但是这样转型会报错
list fruits = new list();

由此可知,泛型的转型和泛型类型是否继承(apple extends fruit)没有任何关系,泛型无法像数组一样直接向上转型,所以通配符的意义就是让泛型的转型更灵活

5.2 通配符详解

  • 上界通配符,fruit是最上边界,只能get,不能add。(详解在代码备注中)
public static void main(string[] args) {
  
    list greenapples = new arraylist<>();
    list apples = new arraylist<>();
    list foods = new arraylist<>();
    setdata(greenapples);
    setdata(apples);
    setdata(foods); // 编译错误,不在限制范围内
}
public void setdata(list list){
  
    // 上界通配符,只能get,不能add
    // 【只能get】因为可以确保list被指定的对象一定可以向上转型成fruit
    // 【不能add】因为设置的话无法确定是哪个子类,
    // 有可能会将banana设置到list里面,所以不能set
    fruit fruit = list.get(0);
}
  • 下界通配符,fruit是最下边界,只能add,不能get。(详解在代码备注中)
public static void main(string[] args) {
  
    list foods = new arraylist<>();
    list apples = new arraylist<>();
    setdata(foods);
    setdata(apples); // 编译错误,不在限制范围内
}
public void setdata(list list){
  
    // 下界通配符,只能add,不能get
    // 【只能add】因为可以确保list被指定的对象一定是fruit的父类,
    // 那fruit的子类一定能向上转型成对应的父类,所以可以add。
    // 【不能get】因为被指定对象没有固定的上界,不知道是哪个父类,所以无法精准获取转型成某一个类。
    list.add(new apple());
    list.add(new banana());
}

总结

最后咱们再总结一下泛型的知识点:

  1. 泛型是在jdk1.5引入的参数化类型特性。
  2. 泛型包括泛型接口、泛型类、泛型方法,可以使用设置边界
  3. 泛型可以使代码更健壮(编译期报错)、代码简洁(不强转)、复用性强
  4. 泛型在java中是伪泛型,虚拟机内不支持泛型类型,在编译阶段会进行泛型擦除,但是会留有备注给反射使用。
  5. 泛型的通配符让转型更加灵活。上界通配符只能get,不能add。下界通配符,只能add,不能get。

这样泛型的介绍就结束了,希望大家读完这篇文章,会对泛型有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。

网站地图