感谢大家和我一起,在Android世界打怪升级!
泛型,一个所有人都知道怎么用,在JAVA世界老生常谈的特性。更需要知其然,知其所以然。
在系列文章的前几节,我会和大家一起打牢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 泛型边界
以上几种类型均可定义泛型的边界,语法
- < 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作为原始类型。
五、泛型的限定通配符
通配符是让泛型的转型更灵活。
- extends A> 是指“上界通配符”
- super 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 通配符详解
- 上界通配符: extends Fruit>,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 extends Fruit> list){
// 上界通配符,只能get,不能add
// 【只能get】因为可以确保list被指定的对象一定可以向上转型成Fruit
// 【不能add】因为设置的话无法确定是哪个子类,
// 有可能会将Banana设置到List里面,所以不能set
Fruit fruit = list.get(0);
}
- 下界通配符: super Fruit>,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 super Fruit> list){
// 下界通配符,只能add,不能get
// 【只能add】因为可以确保list被指定的对象一定是Fruit的父类,
// 那Fruit的子类一定能向上转型成对应的父类,所以可以add。
// 【不能get】因为被指定对象没有固定的上界,不知道是哪个父类,所以无法精准获取转型成某一个类。
list.add(new Apple());
list.add(new Banana());
}
总结
最后咱们再总结一下泛型的知识点:
- 泛型是在JDK1.5引入的参数化类型特性。
- 泛型包括泛型接口、泛型类、泛型方法,可以使用
设置边界 。 - 泛型可以使代码更健壮(编译期报错)、代码简洁(不强转)、复用性强。
- 泛型在JAVA中是伪泛型,虚拟机内不支持泛型类型,在编译阶段会进行泛型擦除,但是会留有备注给反射使用。
- 泛型的通配符让转型更加灵活。上界通配符只能get,不能add。下界通配符,只能add,不能get。
这样泛型的介绍就结束了,希望大家读完这篇文章,会对泛型有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。
下次再见!