一、為什么要用泛型?
1、看一段代碼
List list = new ArrayList();
list.add(1);
list.add("1");
list.add(false);
int m = list.get(0);//無法編譯通過
在不用泛型的情況下,我們可以向list對(duì)象中添加任何對(duì)象,這個(gè)沒問題。但是,當(dāng)我們想獲取數(shù)據(jù)的時(shí)候,麻煩就來了,我們不知道每個(gè)index對(duì)應(yīng)數(shù)據(jù)的具體類型,我們只知道它是個(gè)Object,這往往不能滿足我們的需求。
2、使用泛型的會(huì)怎么樣
還是上面的代碼,稍微做下改動(dòng)
List<String> list = new ArrayList();
list.add(1);//報(bào)錯(cuò)
list.add("1");
list.add(false);//報(bào)錯(cuò)
使用了泛型,編譯器會(huì)幫開發(fā)者做類型檢查,這就保證了此list中添加的數(shù)據(jù)類型全部都是String,那么在取數(shù)據(jù)的時(shí)候,jvm會(huì)幫我們做類型強(qiáng)轉(zhuǎn),因?yàn)椴迦胧墙?jīng)過檢查的,所以獲取的時(shí)候強(qiáng)轉(zhuǎn)是安全的。
二、泛型類、泛型接口、泛型方法
1、先看泛型類
static class Boy<T>{
T toy;
}
T表示某個(gè)類型,但具體是什么類型不知道,所以我們沒辦法將toy初始化,因?yàn)閚ew一個(gè)對(duì)象出來需要知道確切類型,要想解決這個(gè)問題,我們先看下泛型方法
2、泛型方法
static class Boy<T>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
這個(gè)泛型方法表示函數(shù)obtainToy接受一個(gè)F類型參數(shù),這個(gè)F繼承自泛型接口ToyFactory,返回一個(gè)參數(shù)化的類型T。我們來看下泛型接口
2、泛型接口
interface ToyFactory<T>{
T createToy();
}
static class Boy<T>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
ToyFactory這個(gè)泛型接口很簡(jiǎn)單,就一個(gè)方法createToy,返回值是一個(gè)參數(shù)化的類型T的對(duì)象。
那假設(shè),現(xiàn)在現(xiàn)在這個(gè)Boy的一個(gè)對(duì)象想要一個(gè)想要一個(gè)玩具船,按照上面的思路,我們應(yīng)該有個(gè)玩具船工廠類繼承自玩具工廠類,專門生產(chǎn)玩具船。
看代碼
interface ToyFactory<T>{
T createToy();
}
static abstract class Toy{
public abstract void play();
}
static class ToyBoat extends Toy{
public void play(){
Log.d("ToyBoat","ToyBoat surf");
}
}
//玩具船工廠函數(shù)
static class ToyBoatFactory implements ToyFactory<ToyBoat>{
public ToyBoat createToy(){
return new ToyBoat();
}
}
static class Boy<T>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
public static void main(String ...args){
Boy<ToyBoat> boatBoy = new Boy<>();
boatBoy.obtainToy(new ToyBoatFactory());
}
那現(xiàn)在還有一個(gè)問題,我們給Boy增加一個(gè)函數(shù)
public void play(){
toy.play();
}
這段代碼是無法編譯通過的,因?yàn)閠oy.play()涉及到了具體的類型,而toy是某一個(gè)類型,是不能確定的,那還有辦法能讓這段代碼編譯通過嗎?看下面代碼,我們改造一下Boy類
static class Boy<T extends Toy>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
public void play(){
toy.play();
}
我們給Boy的泛型結(jié)構(gòu)加了一個(gè)上線,表示這個(gè)參數(shù)化的類型必須繼承自Toy類,因此我們能夠直接調(diào)用父類的函數(shù)。
三、泛型帶來的問題
1、類型擦除
我們新聲明個(gè)泛型類
static class Girl<T>{
T toy;
public void setToy(T t){
this.toy = t;
}
}
編譯成功后,通過javap -c /path/to/.. 名利來看下生成的class文件
class com.debug.ldsplugin.MainActivity$Girl<T> {
T toy;
com.debug.ldsplugin.MainActivity$Girl();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void setToy(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field toy:Ljava/lang/Object;
5: return
}
關(guān)鍵是這一行
2: putfield #2 // Field toy:Ljava/lang/Object;
我們看到,toy的類型實(shí)際上是一個(gè)Object類型。那問題來了,上面不是說了toy是個(gè)未知的類型嗎?這涉及到j(luò)ava的泛型機(jī)制,java中的泛型是偽泛型,因?yàn)槲覀兛吹骄幾g后參數(shù)化的類型被擦除了,在這個(gè)例子中擦除到Object。
稍微改下代碼
static class Girl<T extends Toy>{
T toy;
public void setToy(T t){
this.toy = t;
}
}
我們看到泛型被擦除到Toy
2: putfield #2 // Field toy:Lcom/debug/ldsplugin/MainActivity$Toy;
2、類型擦除帶來的問題
上面我們已經(jīng)提到了,涉及到需要明確具體類型的操作無法進(jìn)行。那我們舉一些常見的例子以及給出一些解決方案。
- 如何創(chuàng)建泛型數(shù)組
我們嘗試通過常規(guī)方式創(chuàng)建一個(gè)數(shù)組
Girl<ToyBoat>[] girls = new Girl<ToyBoat>[1];//編譯器報(bào)錯(cuò),不讓這么創(chuàng)建
編譯器不讓創(chuàng)建的原因,我猜測(cè)是因?yàn)閖ava中泛型是偽泛型,它不像C#或者其他支持真泛型語言中Girl<ToyBoat>是個(gè)類型,而創(chuàng)建一個(gè)數(shù)組在java中是需要明確類型的,所以,這么做無法編譯通過。那如果就想創(chuàng)建這樣一個(gè)數(shù)組,應(yīng)該怎么做呢?如下:
Girl<ToyBoat>[] girls = (Girl<ToyBoat>[]) new Girl[1];
也就是說我們需要?jiǎng)?chuàng)建一個(gè)類型擦除的數(shù)組,然后再?gòu)?qiáng)轉(zhuǎn)
- 使用T[] array
還是先看代碼
static class GenericArray<T>{
T[] arr;
public GenericArray(int size){
arr = (T[]) new Object[size];
}
public T get(int index){
return arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return arr;
}
}
在構(gòu)造函數(shù)里做了類型強(qiáng)轉(zhuǎn),這里之所以是安全的,是因?yàn)轭愋筒脸?,相?dāng)于(Object[])new Object[size],所以這是安全的,正常get和set也是沒問題,問題出在rep()這個(gè)函數(shù),這個(gè)函數(shù)的調(diào)用有可能引發(fā)崩潰,為什么是說有可能呢?我們還是看代碼
GenericArray<String> genericArray = new GenericArray<>(3);
genericArray.set("aa",0);
genericArray.get(0);
genericArray.rep();
這樣調(diào)用是沒問題的,根據(jù)類型擦除原理我們知道,rep會(huì)返回一個(gè)Object[],我們?cè)谏傻腸lass源碼里也能看到這樣一行代碼
34: invokevirtual #11 // Method com/debug/ldsplugin/MainActivity$GenericArray.rep:()[Ljava/lang/Object;
我們把上面代碼稍微改下
String[] s = genericArray.rep();
這時(shí)候程序就會(huì)崩潰,這是為什么呢?
我們還是看下class源碼
34: invokevirtual #11 // Method com/debug/ldsplugin/MainActivity$GenericArray.rep:()[Ljava/lang/Object;
37: checkcast #12 // class "[Ljava/lang/String;"
40: astore_3
jvm把這段代碼
String[] s = genericArray.rep();
拆解成了三行代碼,先調(diào)用rep,然后轉(zhuǎn)型,問題就出在轉(zhuǎn)型上,因?yàn)闆]辦法把Object[]轉(zhuǎn)換為String[],所以異常。
為了加深理解,我們?cè)俑南麓a
static class GenericArray<T extends String>{
T[] arr;
public GenericArray(int size){
arr = (T[]) new Object[size];
}
public T get(int index){
return arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return arr;
}
}
這段代碼在new GenericArray的時(shí)候就會(huì)異常,因?yàn)闃?gòu)造函數(shù)里嘗試把Object[]轉(zhuǎn)為Integer[]這是注定要失敗的。
那么好的選擇應(yīng)該是下面這樣
static class GenericArray<T extends String>{
Object[] arr;
public GenericArray(int size){
arr = new Object[size];
}
public T get(int index){
return (T)arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return (T[])arr;
}
}
這里面rep方法會(huì)依然報(bào)錯(cuò),但是我們不會(huì)忘記數(shù)組運(yùn)行中的實(shí)際類型是Object,因此會(huì)減少很多麻煩。
那有沒有更好的選擇呢?答案是有,就是使用類型標(biāo)識(shí)
static class GenericArray<T extends String>{
T[] arr;
public GenericArray(Class<T> clz,int size){
arr = (T[])Array.newInstance(clz,size);
}
public T get(int index){
return (T)arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return arr;
}
}
GenericArray<String> genericArray = new GenericArray<>(String.class,3);
genericArray.set("aa",0);
genericArray.get(0);
String[] s = genericArray.rep();
這樣所有代碼都能正常工作了
四、泛型通配符
泛型通配符我建議大家來看這篇文章。