谈谈Java的泛型

前言

JDK1.5号称是Java最重要的版本更新,而泛型又是JDK1.5中一个最重要的特征。
使用泛型机制编写的程序代码要比哪些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用。
泛型涉及的内部机制比较复杂,所有分开两章来讲。

泛型入门

1. 泛型解决了什么问题

先看下面的一段代码

ppublic class GenericDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Winson");
        list.add("Tom");
        list.add(1);
        list.forEach(str -> System.out.println(((String)str).length()));
    }
}

我们创建了一个list,希望是用来保存String的,但“不小心”把整数1放入了list中,当程序将Integer转成String的时候会报ClassCastException。
上面这种"不小心",编译器不会检查出来。

我们使用泛型改进这个程序

public class GenericDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("Winson");
        list.add("Tom");
        list.add(1);    //会提示该list不接受int类型
        list.forEach(str -> System.out.println(str.length()));//无须强制转换
    }
}
  1. 我们在List后面加了尖括号并指定了String类型,表示这个List只能保存String。如果add其他类型,编译时就会检查出来。
  2. list会记住所有元素的数值类型,无须对集合元素进行强制转换。

2. 泛型接口和泛型类类

我们先定义个简单的泛型类和接口

class Apple<T>{
    private T first;
    private T second;
    
    public Apple(T first,T second){
        this.first=first;
        this.second=second;
    }

    public T getFirst() {return first;}
    public T getSecond() {return second;}

    public void setSecond(T second) {this.second =second;}
    public void setFirst(T first) {this.first = first;}
}
public interface List<E>{
    void add(E x);
}
public interface Map<K,V>{
    V put(K key,V value);
}

类型变量

泛型类可以有多个类型变量。例如:

class Apple<T,U>{...}

类型变量使用大写单个大写的字母:
E表示集合的元素类型
K和V表示关键字和值的类型
T、U和S表示任意类型

3. 泛型方法

class ArrayUtils{
    public static <T> T getMiddle(T... a){
        return a[a.length/2];
    }
}

这个泛型方法是在一个普通类中定义的,当然,也可以定义在泛型类中。
类型变量放在修饰符(这是public static)的后面,返回类型的前面。

调用泛型方法

System.out.println(ArrayUtils.<String>getMiddle("a","b","c"));

在这种情况(实际也是大多数情况)下,方法调用中可以省略<String>类型参数,编译器能自动判断。

System.out.println(ArrayUtils.getMiddle("a","b","c"));

4. 类型变量的限定

我们先看看下面这段代码

class ArrayUtils{
    public static <T> T min(T[] a){
        if (a==null||a.length==0) return null;
        T smallest =a[0];
        for (int i=1;i<a.length;i++)
            if (smallest.compareTo(a[i])>0) smallest = a[i];
        return smallest;
    }
}

这段代码存在的问题是如何确定T所属的类有compareTo方法。
解决这个问题的方法是将T限定为实现了Comparable接口的类:

public static <T extends Comparable> T min(T[] a)

为什么使用extends而不是implements呢?
T是绑定类型的子类型(subtype)
T和绑定类型可以是类、也可以是接口
选用关键字extends原因是更接近子类的概念,而且Java的设计者也不打算在语言中在添加一个新的关键字(如sub)

另外,一个类型变量可以有多个限定

限定类型用&分隔
类型变量用逗号分隔

T extends Comparable & Serializable

泛型进阶

1. 类型擦除

Java的泛型是伪泛型

虚拟机里没有泛型类型对象,所有对象都是普通类。在编译期间,所有的泛型信息都会被擦除(erased)。

什么是擦除?

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除

如在代码中定义的List<object>和List<String>等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。

泛型类在Java源码上看起来与一般的类不同,在执行时被虚拟机翻译成对应的“原始类型”

例子

可以通过两个简单的例子,来证明java泛型的类型擦除。

public class Test1 {  
    public static void main(String[] args) {  
        ArrayList<String> arrayList1=new ArrayList<String>();  
        arrayList1.add("a");  
        ArrayList<Integer> arrayList2=new ArrayList<Integer>();  
        arrayList2.add(1);  
        System.out.println(arrayList1.getClass()==arrayList2.getClass());  
    }  
}  

在这个例子中,我们定义了两个ArrayList泛型数组,一个是ArrayList<String>,一个是ArrayList<Integer>。
最后,我们通过arrayList1对象和arrayList2对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。

什么是原始类型?

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。擦除类型变量,并替换为限定定类型(无限定的变量用Object)。
例如,上面的Apple<T>的原始类型如下:

class Apple{
    private Object first;
    private Object second;
    
    public Apple(Object first,Object second){
        this.first=first;
        this.second=second;
    }

    public Object getFirst() {return first;}
    public Object getSecond() {return second;}

    public void setSecond(Object second) {this.second =second;}
    public void setFirst(Object first) {this.first = first;}
}

因T是一个无限定的变量,所以直接用Object替换。就是一个普通类,就像泛型引入前实现的那样。

翻译泛型表达式

我们结合上面Apple的泛型类,再来看下面的代码

Apple<Date> apple = new Apple();
Date date = apple.getFirst(); 

擦除后,getFirst返回Object类型,为什么不需要执行强制类型转换就可以赋值给Date类型的变量呢?

原因是编译器会自动插入Date的强制类型转换。 也就是说,编译器把getFirst方法翻译成两条虚拟机指令:

  • 对原始方法Apple.getFirst的调用;
  • 将返回的Object类型强制转换成Employee类型。

当存取一个泛型域时也会插入强制类型转换,如:

Date date = apple.first;

翻译泛型方法

类型的擦除也会出现在泛型方法中。
我们来看下面的代码

public class BridgeDemo {
    public static class One<T> {
        public T getT() {
            return null;
        }
    }

    public static class Two extends One<String> {
        public String getT() {
            return null;
        }
    }
}

在泛型擦除后的代码类似于:

public class BridgeDemo {  
    public static class One {  
        public Object getT() {  
            return null;  
        }  
    }  
  
    public static class Two extends One {  
        public String getT() {  
            return null;  
        }  
    }  
}  

我们来反编译下Two这个类

  public BridgeDemo$Two();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method BridgeDemo$One."<init>":()V
       4: return

  public java.lang.String getT();
    Code:
       0: aconst_null
       1: areturn

  public java.lang.Object getT();
    Code:
       0: aload_0
       1: invokevirtual #2                  // Method getT:()Ljava/lang/String;
       4: areturn
}

在反编译后的输出中,可以看见有一个新的合成方法“java.lang.Object getT()”,这是源代码中没有的。
这个方法是一个桥方法,它负责将调用代理到“java.lang.String getT()”。因为在JVM里,方法的返回类型是方法签名的一部分,而创建桥方法是实现协变返回类型的方式。

我们在把程序改一下

public class BridgeDemo {
    public static class One<T> {
        public T getT(T args) {
            return args;
        }
    }
    
    public static class Two extends One<String> {
        public String getT(String args) {
            return args;
        }
    }
}

看下反编译的结果

public class BridgeDemo$Two extends BridgeDemo$One<java.lang.String> {
  public BridgeDemo$Two();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method BridgeDemo$One."<init>":()V
       4: return

  public java.lang.String getT(java.lang.String);
    Code:
       0: aload_1
       1: areturn

  public java.lang.Object getT(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #2                  // class java/lang/String
       5: invokevirtual #3                  // Method getT:(Ljava/lang/String;)Ljava/lang/String;
       8: areturn
}

在这里,桥方法重写了基类One,它不仅做了有参数的调用,同时还执行了到“java.lang.String”的类型转换。这意味着在执行下面的代码忽略编译器的“uncheck”警告时,桥方法将抛出ClassCastException异常。

public static void main(String[] args) {
    One one = new Two();
    one.getT(new Object());
}

Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String

你需要记住的有关Java泛型转换的事实

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

推荐阅读更多精彩内容