01、泛型是什么?——《Android打怪升级之旅》

感谢大家和我一起,在Android世界打怪升级!

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

在系列文章的前几节,我会和大家一起打牢JAVA的基础,穿上布甲拿上木剑,披荆斩棘!

一、泛型是什么

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

1.1 参数化类型

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

// 泛型类的定义
public class Generics<T> {
    // 未知类型
    private T mData;

    public T getData() {
        return mData;
    }

    public void setData(T data) {
        this.mData = data;
    }
}

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

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

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

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

二、泛型种类及边界

2.1 泛型种类

1. 泛型接口

public interface Base<T> {

    public T getData();

    public void setData(T data);
    
}

2. 泛型类

public class Generics<T>{
    private T mData;

    public T getData() {
        return mData;
    }

    public void setData(T data) {
        this.mData = data;
    }
}

3. 泛型方法

// public后面的<T>是泛型方法的关键
public <T> Generics<T> getGenerics() {
    return new Generics<T>();
}

2.2 泛型边界

以上几种类型均可定义泛型的边界,语法 <T extends A>、<T extends A&B&...>,泛型重载了extends的关键字,与通常JAVA中使用的extends不同。

  • < T extends A>:单个边界,A可以是类或接口,只能接收继承或者实现A的类型。
  • < T extends A&B&...>:多个边界,A可以是类或接口,A之后的只能是接口。比如:<T extends A&B&C>里面,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<Fruit> fruits = new ArrayList<Fruit>();
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<T> {
    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 <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: com/kproduce/androidstudy/test/Generics<T>
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 <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/kproduce/androidstudy/test/Generics; L0 L1 0
    // signature Lcom/kproduce/androidstudy/test/Generics<TT;>;
    // declaration: com.kproduce.androidstudy.test.Generics<T>
    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<TT;>;
    // declaration: com.kproduce.androidstudy.test.Generics<T>
    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<TT;>;
    // declaration: com.kproduce.androidstudy.test.Generics<T>
    LOCALVARIABLE data Ljava/lang/Object; L0 L2 1
    // signature TT;
    // declaration: T
    MAXSTACK = 2
    MAXLOCALS = 2
}

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


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

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

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

4.2 泛型擦除原则

  • 如果泛型没有限定(<T>),则用Object作为原始类型。
  • 如果有限定(<T extends A>),则用A作为原始类型。
  • 如果有多个限定(<T extends A&B>),则使用第一个边界A作为原始类型。

五、泛型的限定通配符

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

  • <? extends A> 是指“上界通配符”
  • <? super A> 是指“下界通配符”

5.1 通配符存在的意义

数组是可以向上转型的:

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

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

// Apple extends Fruit,但是这样转型会报错
List<Fruit> fruits = new List<Apple>();

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

5.2 通配符详解

  • 上界通配符:<? extends Fruit>,Fruit是最上边界,只能get,不能add。(详解在代码备注中)
public static void main(String[] args) {
    List<GreenApple> greenApples = new ArrayList<>();
    List<Apple> apples = new ArrayList<>();
    List<Food> foods = new ArrayList<>();
    setData(greenApples);
    setData(apples);
    setData(foods); // 编译错误,不在限制范围内
}

public void setData(List<? extends Fruit> list){
    // 上界通配符,只能get,不能add
    // 【只能get】因为可以确保list被指定的对象一定可以向上转型成Fruit
    // 【不能add】因为设置的话无法确定是哪个子类,
    // 有可能会将Banana设置到List<Apple>里面,所以不能set
    Fruit fruit = list.get(0);
}
  • 下界通配符:<? super Fruit>,Fruit是最下边界,只能add,不能get。(详解在代码备注中)
public static void main(String[] args) {
    List<Food> foods = new ArrayList<>();
    List<Apple> apples = new ArrayList<>();
    setData(foods);
    setData(apples); // 编译错误,不在限制范围内
}

public void setData(List<? super Fruit> list){
    // 下界通配符,只能add,不能get
    // 【只能add】因为可以确保list被指定的对象一定是Fruit的父类,
    // 那Fruit的子类一定能向上转型成对应的父类,所以可以add。
    // 【不能get】因为被指定对象没有固定的上界,不知道是哪个父类,所以无法精准获取转型成某一个类。
    list.add(new Apple());
    list.add(new Banana());
}

总结

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

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

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

下次再见!


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

推荐阅读更多精彩内容