一文搞懂泛型

泛型

泛型是什么?

泛型,即“参数化类型”。类型像参数一样,具有多种类型,在使用时才确定。
比如我们需要一个装 int 类型的容器,和一个装 String 类型的容器,要分别制造几个容器吗?比如 IntArrayList 和 StringArrayList ,这样就需要无数个容器了,这种场景就需要泛型。

List<Integer> intList = new ArrayList();
List<String> strList = new ArrayList();

参数化类型意味着可以通过执行泛型类型调用时分配一个类型,用分配的具体类型替换泛型类型。下面 ArrayList 中<E>就是泛型类型,在使用时才分配具体类型:

public class ArrayList<E> extends AbstractList<E>

通俗的说,就是我要一个篮子,可能用这个篮子装水果,那它就是一个水果篮,也可能水果吃完后用来装垃圾,那它就是垃圾篮,没必要写死这个篮子只能装水果或者垃圾,但是装水果的时候不希望篮子里有垃圾,装垃圾的时候不希望有水果。这时候就要泛型了。

泛型的好处

  1. 提高安全性: 将运行期的错误转换到编译期. 如果我们在对一个对象所赋的值不符合其泛型的规定, 就会编译报错.
  2. 避免强转: 比如我们在使用List时, 如果我们不使用泛型, 当从List中取出元素时, 其类型会是默认的Object, 我们必须将其向下转型为String才能使用。比如:
List l = new ArrayList();
l.add("abc");
String s = (String) l.get(0);

而使用泛型,就可以保证存入和取出的都是String类型, 不必在进行cast了,也可以直接调用类型独有的方法,比如:

List<String> l = new ArrayList();
l.add("abc");
l(0).split("b");

如何使用泛型

类型参数用作占位符,在运行时为类分配类型。根据需要,可能有一个或多个类型参数,根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出每个用例的标准类型参数:

  • E:元素
  • K:键
  • N:数字
  • T:类型
  • V:值
  • S、U、V 等:多参数情况中的第 2、3、4 个类型
public class Test<T> {

    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

使用:

    Test<String> t = new Test<>();
        t.setObj("abc");

在JDK1.7时就推出了一个新特性叫菱形泛型(The Diamond), 就是说后面的泛型可以省略直接写成<>, 反正前后一致。

泛型中的通配符

?和关键字extends或者super在一起其实就是泛型的高级应用:通配符。

固定上边界通配符 <? extends E>

interface Fruit {

     double getWeight();
}

class Apple implements  Fruit{

    @Override
    public double getWeight() {
        return 5;
    }
}
class Orange implements  Fruit{

    @Override
    public double getWeight() {
        return 4;
    }
}

加入小明买了一个果篮,可以装苹果,也可以装橘子。

        ArrayList<Fruit> fruits = new ArrayList<>();
        Apple apple = new Apple();
        fruits.add(apple);
        Orange orange = new Orange();
        fruits.add(orange);

但是小明只想让这个篮子装橘子:

ArrayList<Fruit> fruits = new ArrayList<Orange>(); // 编译报错

这样编辑器就报错了,因为泛型不支持向上转型。那怎么实现小明的需求呢:

ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();

这样编辑器就不报错了,但是问题又来了,这个篮子不能装东西,会报错:

        ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
        fruits.add(orange); // 编译报错

这是因为,如果可以装水果,那并不知道你装的是苹果还是橘子,如果你装了苹果,小明女朋友想吃橘子,取出的是苹果,小明女朋友就要分手了。

编辑器也为了我们想取苹果的时候取出橘子,导致类型错误,所以不允许调用 addset,等参数是泛型的方法。

那这个篮子什么用呢?其实,还真有用,比如小明要给水果称重,你不知道是苹果篮还是橘子篮,所以这样写:

    static double getWeight(List<? extends Fruit> list) {

        double weight = 0;
        for (int i = 0; i < list.size(); i++) {
            weight += list.get(i).getWeight();
        }
        return weight;
    }
        ArrayList<Orange> oranges = new ArrayList<>();
        oranges.add(orange);
        ArrayList<Apple> apples = new ArrayList<>();
        apples.add(apple);
        getWeight(apples);
        getWeight(oranges);

这样就可以给水果称重了。

<? extends E>就是固定上界通配符
重点说明:我们不能对List<? extends E>使用add方法。原因是,我们不确定该List的类型, 也就不知道add方法的参数类型。
但是也有特例,可以添加null

    ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
        fruits.add(null);

固定下边界通配符 <? super E>

小明女朋友喜欢橘子,也喜欢喝橙汁,小明送了2个橘子,一个放果篮里准备生吃,一个放厨房篮子做果汁:

interface Fruit {

    double getWeight();
}

interface Juice {

}

class Orange implements Fruit,Juice {

    @Override
    public double getWeight() {
        return 4;
    }
    public void addList(List<? super Orange> list) {
        list.add(this);
    }
}

        List<Juice> juices = new ArrayList<>();
        List<Fruit> fruits = new ArrayList<>();

        Orange orange1 = new Orange();
        Orange orange2 = new Orange();
        orange1.addList(juices);
        orange2.addList(fruits);
        Fruit object = fruits.get(0);

小明女朋友想要装篮子的时候,取出第一个:

    public void addList(List<? super Orange> list) {
        Juice object = list.get(0); // 编译报错
        list.add(this);
    }

竟然报错了,并不知道是水果篮子还是橙汁篮子。万一小明女朋友想从水果篮子拿出一个,结果拿的是橙子篮子的,那么小明又要被分手了。

重点说明:我们不能对List<? super E>使用 get 方法。
原因是,我们不确定该List的类型, 也就不知道 get 方法的参数类型。
但是也有特例, Object 类型就可以:

    public void addList(List<? super Orange> list) {
        Object object = list.get(0);
        list.add(this);
    }

无边界通配符 <?>

无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
 
小明女朋友有个篮子,没有告诉小明是装什么的,于是小明用来装了橙子:

        List<?> fruits = new ArrayList<Orange>();
        fruits.add(orange); // 编译报错
        fruits.get(0); // 编译报错

小明又被分手了,why?
小明事后想到:这个篮子可能是小明女朋友装脏袜子的

<?>就是无边界通配符,它具有上边界和下边界的限制,不能 add 也不能 get 。因为不能确定类型。

但是也有特例,就是可以 get 到 Object,也可以存入 null。

总结

泛型限定符有一描述:上界不存下界不取。

上界不存的原因:例如 List,编译器只知道容器内是 Fruit 及其子类,具体是什么类型并不知道,编译器在看到 extends 后面的 Fruit 类,只是标上一个 CAP#1 作为占位符,无论往里面插什么,编译器都不知道能不能和 CAP#1 匹配,所以就不允许插入。

下界不取的原因:下界限定了元素的最小粒度,实际上是放松了容器元素的类型控制。例如 List, 元素是 Orange,可以存入 Orange 及其超类。但编译器并不知道哪个是 Orange 的超类,如 Juice。读取的时候,自然不知道是什么类型,只能返回 Object,这样元素信息就全部丢失了。

kotlin 中的泛型

和 Java 泛型一样,Kolin 中的泛型也有通配符:

  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。
  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super。

泛型方法和类型推断

小明的前女友柳岩只吃橘子这一种水果,会收各种礼物,因为礼物类型不确定,所以收礼物需要泛型方法,为什么不用Obje呢?因为泛型方法具有类型推断,不用强转,避免类型转换异常。

interface GirlFriend<T> {

    T eatFruit(T i);

    <E> E getGift(E e);
}


class LiuYan<T> implements GirlFriend<T> {


    @Override
    public T eatFruit(T t) {
        return t;
    }

    @Override
    public <E> E getGift(E e) {
        return null;
    }
}
        GirlFriend<Orange> liuyan = new LiuYan<>();
        liuyan.eatFruit(new Orange());
        Apple gift = liuyan.getGift(new Apple());

送柳岩一个苹果,因为有类型推断,所以 Apple 不要强转。

理解嵌套

小明把前女友分类,爱吃水果的分一类:


interface GirlFriend<T extends Fruit> {

    T eatFruit(T i);

    <E> E getGift(E e);
}


class LiuYan<T extends Fruit> implements GirlFriend<T> {


    @Override
    public T eatFruit(T t) {
        return t;
    }

    @Override
    public <E> E getGift(E e) {
        return null;
    }
}

List<? extends  GirlFriend<? extends Fruit>> list = new ArrayList<? extends  GirlFriend<? extends Fruit>>(); // 编译报错

列表右边和左边一样,报错了,右边去掉 ?:

List<? extends  GirlFriend<? extends Fruit>> list = new ArrayList< GirlFriend<? extends Fruit>>(); 

不报错了,为什么呢?

泛型实例化的时候要确定类型,所以 List 实例化的时候要确定具体类型, GirlFriend 代表这是个女朋友列表。那为什么 GirlFriend 后面的泛型却可以不确定呢?因为这个列表是女朋友列表,但是那种类型的女朋友列表不管。在装进去的时候才确定。

类型擦除

        List<String> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        System.out.println(list1.getClass()==list2.getClass());

上面输出是 true,因为虚拟机只会看到 List,泛型被擦除了。

    public class ObjectContainer<T> {
        private T contained;
        public ObjectContainer(T contained) {
            this.contained = contained;
        }
        public T  getContained() {
            return contained;
        }
    }

这段代码,编译器会生成以下代码:

public class ObjectContainer {
    private Object contained;
public ObjectContainer(Object contained) {
    this.contained = contained;
}
public Object getContained() {
    return contained;
}
}

为什么会有泛型擦除呢?是因为泛型的支持是在JDK1.5之后,那么以前的版本运行时JVM是不能识别泛型的,所以有了一个擦除机制,擦除之后,类型信息转为它的边界类型。

擦除会带来2个问题,那就是继承中方法的重载问题,还有就是类型信息擦除之后如何获取信息的问题。

     void putList(List<Integer> list){

    }
     void putList(List<String> list){

    }

因为泛型擦除,所以上面两个方法签名一致,重载失败,编译器报错。

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}

用一个子类继承:

class DateInter extends Pair<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
} 

那么问题来了,不是泛型擦除了吗?
编译后的 Pair,

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

子类重写的方法:

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  
@Override  
public Date getValue() {  
    return super.getValue();  
} 

方法重写的话,要求参数返回值一致,为什么子类会重写成功呢?

class DateInter extends Pair {

    // 我们重写的方法
    public void setValue(Date value) {
        super.setValue(value);
    }
    
    // 我们重写的方法
    public Date getValue() {
        return (Date) super.getValue();
    }
        
    // 虚拟机生成的桥接方法
    @Override
    public Object getValue() {
        return getValue();
    }

    // 虚拟机生成的桥接方法
    @Override
    public void setValue(Object value) {
        setValue( (Date)value);
    }
}

从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

并且,还有一点也许会有疑问,子类中的桥方法 Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起 来“不合法”的事情,然后交给虚拟器去区别。

这样就巧妙的解决了重载的问题。

泛型类型获取

List<Fruit> ps = gson.fromJson(str, new TypeToken<List<Fruit>>(){}.getType());  

既然类型擦除了,为什么 Gson 在转 json 的时候还能获取到?

        List<Fruit> list  = new ArrayList<Fruit>();

我们获取到的 class 类型是 List,因为 ArrayList<E> 这个类并没有类型。但是我们写个子类继承 ArrayList<E>,就能获取子类的类型:
     class FruitArrayList extends ArrayList<Fruit>{}

        List<Fruit> list  = new ArrayList<Fruit>();
        List<Fruit> list2  = new FruitArrayList();

list2 的类型是 FruitArrayList,在看下面的 list3 :

        List<Fruit> list  = new ArrayList<Fruit>();
        List<Fruit> list2  = new FruitArrayList();
        List<Fruit> list3  = new ArrayList<Fruit>(){};

list3 和 list1 的区别是后面有个中括号,代表这个就是一个 ArrayList<Fruit> 的类,就能获取这个类型。所以 Gson 在转化的时候,是一样的方法:

new TypeToken<List<Fruit>>(){}.getType()

如果改为

new TypeToken<List<Fruit>>().getType()

就获取不到类型,当然编辑器就报错了。

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