Java泛型原理详解
2017-07-27 21:04
309 查看
在Java 5时,引入了泛型,本文主要从原理的层面上分析泛型。
水果类Fruits,有set()和get()设置水果和取出水果,Apple和Orange分别是Fruits的子类,在addFruit()添加一个Apple,然后通过get()取出来,在取出来的水果转换为Orange的时候,会抛出ClassCastException。
上述的代码在语法上是可以的,导致编译器无法检查出类型转换非法的问题,而在运行时会导致强制转换失败而导致崩溃。而泛型的作用就是用来在编译时来进行类型安全检查,能够让开发人员在编译的时候能够检查出非法的类型。
同样是在addFruit()添加一个Apple,然后通过get()取出来,在取出来的水果转换为Orange,此时代码编译无法通过,泛型设置的是Apple,而当把它强制转化为Orange的时候,编译器检查到非法类型转换,提示编译错误Inconvertible types: “xxx.Apple” to “xxx.Orange”。
通过apktool反编译,得到Apple、GenericFruit等smail文件,直接上GenericFruit的smail代码
smail代码比较长,挑重点来看
这是定义字段fruits,我们看到后面的Ljava/lang/Object;编译之后fruits变为了Object类型,那再看看get()与set()方法是怎样的呢?
从上面的代码中发现,get()的返回类型变为了object,同时set()的返回类型也成了Object,参数fruits也变成了Object。将泛型中的T类型变成了Object,而虚拟机运行的时候将泛型参数变成了Object来处理,无法感知到泛型的存在,这就是泛型类型的擦除性质。
既然编译的时候泛型类型参数被转换为了Object,那看看下面这段代码
其中Apple apple = fruits.get();并不需要进行强制转换为Apple,那么就有一个疑问,什么时候将Object转化为Apple的呢?继续看上面这段代码反编译之后的代码
首先看到get()Ljava/lang/Object;此时返回的是Object类型,接下来check-cast v0, Lcom/example/forone/genericdemo/Apple;此时将Object类型转换为了Apple类型。
根据上面的分析,
上面的代码太长,先看get()方法
Banana类在调用Banane get()方法时,首先调用父类也就是泛型类的get()方法,返回Object类型,并强制转换为Banana,实现的是重载功能。而多出了一个用bridge修饰的Object get() ,其返回值是Object,内部调用了Banane get()。到这里可以发现,其实重写父类即泛型类的Object get()是编译之后自动生成的用bridge修饰的Object get()方法,而Banana get()为重载的方法,被bridge修饰的Object get()方法调用,从而起到了重写的作用。
再看看set(Banana)方法
跟get(Banana)一样,set(Banana)也是被编译器自动生成的bridge set(Object)的方法调用,而bridge set(Object)才是真正重写父类即泛型类的set(Object)方法。而set(Banana)只是一个重载的方法。
从上面的分析可以得知,泛型子类的重写其实只是重载,通过编译器生成的用bridge修饰的方法来实现重写,而该函数再调用子类重写(实际是重载)的方法是模拟实现重写的功能。
泛型的类型参数T在编译后为Object类型,编译器自动生成代码对Object进行强制转换成类型参数T
泛型子类的重写其实只是重载,通过编译器生成的用bridge修饰的方法来实现重写,而该函数再调用子类重写(实际是重载)的方法是模拟实现重写的功能。
泛型的作用
为什么需要引入泛型呢?泛型有什么样的作用呢?先看段代码public class Fruits { Fruits fruits; protected void set(Fruits item){ fruits = item; } protected Fruits get(){ return fruits; } } public class Apple extends Fruits { private static final String name = "apple"; } public class Orange extends Fruits { private static final String name = "orange"; } private void addFruit() { Fruits fruits = new Fruits(); fruits.set(new Apple()); Apple apple = (Apple) fruits.get(); Orange orange = (Orange) fruits.get();//强制转换崩溃 }
水果类Fruits,有set()和get()设置水果和取出水果,Apple和Orange分别是Fruits的子类,在addFruit()添加一个Apple,然后通过get()取出来,在取出来的水果转换为Orange的时候,会抛出ClassCastException。
上述的代码在语法上是可以的,导致编译器无法检查出类型转换非法的问题,而在运行时会导致强制转换失败而导致崩溃。而泛型的作用就是用来在编译时来进行类型安全检查,能够让开发人员在编译的时候能够检查出非法的类型。
编译时泛型
不知道大家有没有听过“Java伪泛型”或者“编译时泛型”的说法。这两种说法都体现了Java的泛型只是在编译时期起作用,对于运行时没有任何影响。由编译器来执行类型安全检查和类型推断,然后生成非泛型的字节码。而虚拟机无法感知到泛型的存在,这种实现方式叫做擦除(erasure)。那接下来从泛型的编译时类型安全检查和擦除性来分析。1.编译时类型安全检查
修改上面的代码Fruits为GenericFruits,通过泛型实现,具体代码如下public class GenericFruits<T> { T fruits; protected void set(T item) { fruits = item; } protected T get() { return fruits; } } private void addFruit() { GenericFruits<Apple> fruits = new GenericFruits<>(); f ee0b ruits.set(new Apple()); Apple apple = fruits.get(); Orange orange = (Orange) fruits.get();//编译无法通过 }
同样是在addFruit()添加一个Apple,然后通过get()取出来,在取出来的水果转换为Orange,此时代码编译无法通过,泛型设置的是Apple,而当把它强制转化为Orange的时候,编译器检查到非法类型转换,提示编译错误Inconvertible types: “xxx.Apple” to “xxx.Orange”。
2.泛型类型擦除
既然泛型是由编译器来执行类型安全检查和类型推断,然后生成非泛型的字节码,而虚拟机无法感知到泛型的存在。那么我们反编译APK来看看编译之后是怎样的。通过apktool反编译,得到Apple、GenericFruit等smail文件,直接上GenericFruit的smail代码
.class public Lcom/example/forone/genericdemo/GenericFruits; .super Ljava/lang/Object; .source "GenericFruits.java" # annotations .annotation system Ldalvik/annotation/Signature; value = { "<T:", "Ljava/lang/Object;", ">", "Ljava/lang/Object;" } .end annotation # instance fields .field fruits:Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "TT;" } .end annotation .end field # direct methods .method public constructor <init>()V .locals 0 .prologue .line 8 .local p0, "this":Lcom/example/forone/genericdemo/GenericFruits;, "Lcom/example/forone/genericdemo/GenericFruits<TT;>;" invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # virtual methods .method protected get()Ljava/lang/Object; .locals 1 .annotation system Ldalvik/annotation/Signature; value = { "()TT;" } .end annotation .prologue .line 17 .local p0, "this":Lcom/example/forone/genericdemo/GenericFruits;, "Lcom/example/forone/genericdemo/GenericFruits<TT;>;" iget-object v0, p0, Lcom/example/forone/genericdemo/GenericFruits;->fruits:Ljava/lang/Object; return-object v0 .end method .method protected set(Ljava/lang/Object;)V .locals 0 .annotation system Ldalvik/annotation/Signature; value = { "(TT;)V" } .end annotation .prologue .line 13 .local p0, "this":Lcom/example/forone/genericdemo/GenericFruits;, "Lcom/example/forone/genericdemo/GenericFruits<TT;>;" .local p1, "item":Ljava/lang/Object;, "TT;" iput-object p1, p0, Lcom/example/forone/genericdemo/GenericFruits;->fruits:Ljava/lang/Object; .line 14 return-void .end method
smail代码比较长,挑重点来看
# instance fields .field fruits:Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "TT;" } .end annotation .end field
这是定义字段fruits,我们看到后面的Ljava/lang/Object;编译之后fruits变为了Object类型,那再看看get()与set()方法是怎样的呢?
# virtual methods .method protected get()Ljava/lang/Object;//get()的返回类型为object .locals 1 .annotation system Ldalvik/annotation/Signature; value = { "()TT;" } .end annotation /...省略部分代码.../ return-object v0 .end method .method protected set(Ljava/lang/Object;)V//set()的参数类型也为Object .locals 0 .annotation system Ldalvik/annotation/Signature; value = { "(TT;)V" } .end annotation /...省略部分代码.../ return-void .end method
从上面的代码中发现,get()的返回类型变为了object,同时set()的返回类型也成了Object,参数fruits也变成了Object。将泛型中的T类型变成了Object,而虚拟机运行的时候将泛型参数变成了Object来处理,无法感知到泛型的存在,这就是泛型类型的擦除性质。
既然编译的时候泛型类型参数被转换为了Object,那看看下面这段代码
private void addGenericFruit() { GenericFruits<Apple> fruits = new GenericFruits<>(); fruits.set(new Apple()); Apple apple = fruits.get(); }
其中Apple apple = fruits.get();并不需要进行强制转换为Apple,那么就有一个疑问,什么时候将Object转化为Apple的呢?继续看上面这段代码反编译之后的代码
.method private addGenericFruit()V /...省略部分代码.../ .line 26 invoke-virtual {v1}, Lcom/example/forone/genericdemo/GenericFruits;->get()Ljava/lang/Object; move-result-object v0 check-cast v0, Lcom/example/forone/genericdemo/Apple; .line 28 .local v0, "apple":Lcom/example/forone/genericdemo/Apple; return-void .end method
首先看到get()Ljava/lang/Object;此时返回的是Object类型,接下来check-cast v0, Lcom/example/forone/genericdemo/Apple;此时将Object类型转换为了Apple类型。
花非花 —— 重写非重写,而是重载
新建一个Banana类,继承GenericFruits<Banana>类,重写get()与set()方法
public class Banana extends GenericFruits<Banana> { private static final String name = "Banana"; @Override protected void set(Banana item) { super.set(item); } @Override protected Banana get() { return super.get(); } }
根据上面的分析,
GenericFruits<Banana>中的set()和get()方法的泛型类型会被擦除,变成set(Object)和Object get();然而在Java中普通重写方法时方法的形参是不能改变的,否则是编译器会提示报错,这一点应该是编译器针对泛型进行了特殊的处理,没有报编译错误。而上面的set(Banana item),参数从Object变成了Banana,set(Banana)与set(Object)实际上是重载,而我们用Override注解修饰时,我们是去实现重写的功能,那么虚拟机在运行时是怎么将重载变成重写的呢?在反编译的字节码中发现在泛型编译之后,引入了一个bridge的概念,先看代码
.class public Lcom/example/forone/genericdemo/Banana; .super Lcom/example/forone/genericdemo/GenericFruits; .source "Banana.java" # annotations .annotation system Ldalvik/annotation/Signature; value = { "Lcom/example/forone/genericdemo/GenericFruits", "<", "Lcom/example/forone/genericdemo/Banana;", ">;" } .end annotation # static fields .field private static final name:Ljava/lang/String; = "apple" # direct methods .method public constructor <init>()V .locals 0 .prologue .line 7 invoke-direct {p0}, Lcom/example/forone/genericdemo/GenericFruits;-><init>()V return-void .end method # virtual methods .method protected get()Lcom/example/forone/genericdemo/Banana; .locals 1 .prologue .line 17 invoke-super {p0}, Lcom/example/forone/genericdemo/GenericFruits;->get()Ljava/lang/Object; move-result-object v0 check-cast v0, Lcom/example/forone/genericdemo/Banana; return-object v0 .end method .method protected bridge synthetic get()Ljava/lang/Object; .locals 1 .prologue .line 7 invoke-virtual {p0}, Lcom/example/forone/genericdemo/Banana;->get()Lcom/example/forone/genericdemo/Banana; move-result-object v0 return-object v0 .end method .method protected set(Lcom/example/forone/genericdemo/Banana;)V .locals 0 .param p1, "item" # Lcom/example/forone/genericdemo/Banana; .prologue .line 12 invoke-super {p0, p1}, Lcom/example/forone/genericdemo/GenericFruits;->set(Ljava/lang/Object;)V .line 13 return-void .end method .method protected bridge synthetic set(Ljava/lang/Object;)V .locals 0 .prologue .line 7 check-cast p1, Lcom/example/forone/genericdemo/Banana; invoke-virtual {p0, p1}, Lcom/example/forone/genericdemo/Banana;->set(Lcom/example/forone/genericdemo/Banana;)V return-void .end method
上面的代码太长,先看get()方法
# virtual methods .method protected get()Lcom/example/forone/genericdemo/Banana; .locals 1 .prologue .line 17 invoke-super {p0}, Lcom/example/forone/genericdemo/GenericFruits;->get()Ljava/lang/Object;//调用父类即泛型类的get()方法,返回object类型 move-result-object v0 //将Object强制转换为banana check-cast v0, Lcom/example/forone/genericdemo/Banana; return-object v0 .end method .method protected bridge synthetic get()Ljava/lang/Object; .locals 1 .prologue .line 7 //调用Banana get()方法 invoke-virtual {p0}, Lcom/example/forone/genericdemo/Banana;->get()Lcom/example/forone/genericdemo/Banana; move-result-object v0 return-object v0 .end method
Banana类在调用Banane get()方法时,首先调用父类也就是泛型类的get()方法,返回Object类型,并强制转换为Banana,实现的是重载功能。而多出了一个用bridge修饰的Object get() ,其返回值是Object,内部调用了Banane get()。到这里可以发现,其实重写父类即泛型类的Object get()是编译之后自动生成的用bridge修饰的Object get()方法,而Banana get()为重载的方法,被bridge修饰的Object get()方法调用,从而起到了重写的作用。
再看看set(Banana)方法
.method protected set(Lcom/example/forone/genericdemo/Banana;)V .locals 0 .param p1, "item" # Lcom/example/forone/genericdemo/Banana; .prologue .line 12 invoke-super {p0, p1}, Lcom/example/forone/genericdemo/GenericFruits;->set(Ljava/lang/Object;)V .line 13 return-void .end method .method protected bridge synthetic set(Ljava/lang/Object;)V .locals 0 .prologue .line 7 check-cast p1, Lcom/example/forone/genericdemo/Banana; invoke-virtual {p0, p1}, Lcom/example/forone/genericdemo/Banana;->set(Lcom/example/forone/genericdemo/Banana;)V return-void .end method
跟get(Banana)一样,set(Banana)也是被编译器自动生成的bridge set(Object)的方法调用,而bridge set(Object)才是真正重写父类即泛型类的set(Object)方法。而set(Banana)只是一个重载的方法。
从上面的分析可以得知,泛型子类的重写其实只是重载,通过编译器生成的用bridge修饰的方法来实现重写,而该函数再调用子类重写(实际是重载)的方法是模拟实现重写的功能。
总结
泛型是编译时泛型,用于检查类型安全以及类型推断,虚拟机运行时已经被擦除泛型的类型参数T在编译后为Object类型,编译器自动生成代码对Object进行强制转换成类型参数T
泛型子类的重写其实只是重载,通过编译器生成的用bridge修饰的方法来实现重写,而该函数再调用子类重写(实际是重载)的方法是模拟实现重写的功能。
相关文章推荐
- (26) java泛型实现原理 及 java泛型详解
- word2vec 中的数学原理详解(一)目录和前言
- MySQL备份原理详解
- Java泛型详解
- 图像处理中的数学原理详解(Part4) ——傅立叶级数的概念1
- APK安装过程及原理详解
- 邻接表原理详解
- GreenDao源码详解第一篇(Dao、Mater等类生成原理)
- LVS专题-(2) LVS原理详解及部署
- android 自定义View原理详解01
- php urlencode()与urldecode()函数字符编码原理详解
- Skip List(跳跃表)原理详解与实现
- xargs的原理剖析及用法详解
- 哈希表(散列表)原理详解
- 主成分分析(PCA)原理详解
- 详解还原系统保护技术原理和攻防
- DNS原理总结及其解析过程详解
- Nginx详解(正向代理、反向代理、负载均衡原理)
- 拒绝服务***原理及解决方法详解
- UIScrollView 原理详解