JAVA泛型的理解

泛型大家都接触的不少,但是由于Java历史原因,Java中泛型一直被称为伪泛型,因此对Java中的泛型,有很多不注意就会遇到的坑,在这里详细讨论下。

什么是泛型

自JDK1.5之后,Java通过泛型解决了容器类型安全这一问题,而几乎所有人接触泛型也是通过Java的容器,那么泛型究竟是什么?
泛型的本质是参数化类型
也就是说,泛型就是将所操作的数据类型作为参数的一种语法。

public class Play<T> {

    T t;
    
    Play(T t){
        this.t=t;
    }
    
    T play(){
        return t;
    }
}

其中T 就是作为一个类型参数在Play被实例化的时候锁传递来的参数,比如

Play<Integer> playInteger=new Play<>(2);

这里T就会被实例化为Integer。

泛型的作用

使用泛型能写出更加灵活通用的代码
泛型的设计主要参照了C++模板,旨在能让人写出更加通用化,更加灵活的代码。泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这就是泛型最初设计的宗旨。

泛型将代码安全性检查提前到编译期
泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时比如:

List dogs=new ArrayList();
dogs.add(new Cat())

在没有泛型之前,这种代码除非运行,否则你永远找不到他的错误。但是加入泛型后

List<Dog> dogs=new ArrayList<>();
dogs.add(new Cat());

会在编译的时候就检查出来。

泛型能够省去类型强制转换
在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在容器中取出来的时候需要手动的强制转换。

Dog dog=(Dog)dogs.get(1);

加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅。

泛型的具体实现
我们可以定义泛型类,泛型方法,泛型接口等,那么泛型的底层是怎么实现的呢?
Java设计者将泛型完全作为了语法糖加入了新的语法中,什么意思呢?也就是说泛型对于JVM来说是透明的,有泛型的和没有泛型的代码,通过编译器编译后所生成的二进制代码完全是相同的。 这个语法糖的实现被称为擦除
擦除的过程
泛型是为了将具体的类型作为参数传给方法,类,接口。
擦除是在代码运行过程中将具体的类型都擦除。
前面说过,Java 1.5之前需要编写模板代码的地方都是通过Object来保存具体的值。比如:

public class Node{
   private Object obj;

   public Object get(){
       return obj;
   }
   
   public void set(Object obj){
       this.obj=obj;
   }
   
   public static void main(String[] argv){
    
    Student stu=new Student();
    Node  node=new Node();
    node.set(stu);
    Student stu2=(Student)node.get();
   }
}

这样的实现能满足绝大多数需求,但是泛型还是有更多方便的地方,最大的一点就是编译器类型检查,于是Java 1.5之后加入了泛型,但是这个泛型仅仅是在编译的时候帮你做了编译时类型检查,成功编译后所生成的.class文件还是一模一样的,这便是擦除。
1.5以后实现

public class Node<T> {
    private T object;

    public void set(T object) {
        this.object = object;
    }

    public T get() {
        return object;
    }

    public static void main(String[] args) {
        Student student=new Student();
        Node<Student> node=new Node<>();
        node.set(student);
        Student stu2=node.get();
    }
}

泛型语法
Java的泛型就是一个语法糖,而语法糖最大的好处就是让人方便使用,但是它的缺点也在于如果不剥开这颗语法糖,有很多奇怪的语法就很难理解。
类型边界
前面说过,泛型在最终会擦除为Object类型。这样导致的是在编写泛型代码的时候,对泛型元素的操作只能使用Object自带的一些方法,但是有时候我们想使用其他类型的方法呢?
比如:

public class Node1 {
    private People obj;

    public People get() {
        return obj;
    }

    public void set(People obj) {
        this.obj = obj;
    }
    
    public void playName(){
        System.out.println(obj.getName());
    }
}

如上,代码中需要使用obj.getName()方法,因此比如规定传入的元素必须是People及其子类,那么这样的方法怎么通过泛型体现出来呢?
答案是extends,泛型重载了extends关键字,可以通过extends关键字指定最终擦除所替代的类型。

public class Node2<T extends People> {
    
    private T obj;

    public T get() {
        return obj;
    }

    public void set(T obj) {
        this.obj = obj;
    }
    
    public void playName(){
        System.out.println(obj.getName());
    }
}

通过extends关键字,编译器会将最后类型都擦除为People类型,就好像最开始我们看见的原始代码一样。

泛型与向上转型的概念
先讲几个概念
协变:子类能向父类转换

Animal al=new Cat();

逆变:父类能向子类转换

Cat a2=(Cat)al;

不变:两者均不能转变
对于协变,我们见得最多的就是多态,而逆变常见于强制类型转换。这好像没什么奇怪的,但是看以下代码:

public class Error {

    public static void main(String[] args) {
        Object[] nums=new Integer[3];

        nums[0]=3.2;
        nums[1]="String";
        nums[2]='2';
    }
}

因为数组是协变的,因此Integer[]可以转换为Object[],在编译阶段编译器只知道nums是Object[]类型,而运行时nums则为Integer[]类型,因此上面代码能够编译,但是运行时会报错。
这就是常见的人们所说的数组是协变的。这里带来一个问题,为什么数组要设计成协变的呢?既然不让运行,那么通过编译有什么用?答案是在泛型还没出现之前,数组协变能够解决一些通用的问题:

public static void sort(Object[] a) {
        // Android-removed: LegacyMergeSort support
        // if (LegacyMergeSort.userRequested)
        //     legacyMergeSort(a);
        // else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }


public static boolean equals(Object[] a, Object[] a2) {
        //...
        for (int i=0; i<length; i++) {
            Object o1 = a[i];
            Object o2 = a2[i];
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        //..
        return true;
    }

可以看到,只操作数组本身,而不关心数组中具体保存的值,或不管什么元素,取出来就是一个Object存储的时候,只用编写一个Object[]就能写出通用的数组参数方法。比如:

Arrays.sort(new Student[]{....})
Arrays.sort(new Apple[]{...})

等,但是这样的设计留下来的诟病就是偶尔会出现对数组元素有具体的操作的代码,比如上面的error类中的错误。
泛型的出现,是为了保证类型安全的问题,如果将泛型也设计为协变的话,那也就,那也就违背了泛型设计的初衷,因此在Java中泛型是不变的,什么意思呢?

List<Number>和List<Integer> 是没有任何关系的,即使Integer是Number的子类

也就是对于
public staitc void test(List<Number> nums){...}

方法,是无法传递一个List<Integer>参数的
逆变一般常见于强制类型转换。

Object obj="test";
String str=(String)obj;

原理便是Java反射机制能够记住变量obj的实际类型,在强制类型转换的时候发现obj实际上是一个String类型,于是就正常的通过了运行。

泛型向上转型的实现
前面说了这么多,应该关心的问题在于,如何解决技能使用数组协变代来的便利性,又能解决泛型不变带来的类型安全?
答案依然是extendssuper 关键字与通配符 ?
泛型重载了extends,super关键字来解决通配泛型的表示。

注意:这句话可能比较熟悉,没错,前面说过extends还被用来指定擦除到的具体类型,比如<E extends Fruit>,表示在运行时将E替换为Fruit,注意E 表示的是一个具体的类型,但是这里的extends和通配符连续使用<? extends Fruit>这里通配符?表示一个通用类型,它所表示的泛型在编译的时候,被指定的具体的类型必须是Fruit的子类,比如<? extends Fruit> list=new ArrayList<Apple>,ArrayList<>中指定的类型必须是Apple,Orange等。不要混淆

直接看代码:
协变泛型

public class TestGenric {

    public static void main(String[] args) {

        List<Apple> apples=new ArrayList<>();
        List<Orage> oranges=new ArrayList<>();
        
        playFruit(apples);
        playFruit(oranges);
    }

    public static void playFruit(List<? extends Fruit> list){
        //do something
    }
}

可以看到,参数List<? extends Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是继承自Fruit的。
这样便解决了泛型无法向上转型的问题,前面说过,数组也能向上转型,但是存取元素有问题,这里继续深入,看看泛型是怎么解决这一问题的。

    public static void playFruit(List<? extends Fruit> list){
        //do something
        list.add(new Apple()) //compile error
    }

向传入的list添加元素,你会发现编译器直接会报错.

逆变泛型

public static void main(String[] args) {
      List<Food> foods=new ArrayList<>();
        List<Object> objects=new ArrayList<>();
        playFruitBase(foods);
        playFruitBase(objects);
        playFruitBase(oranges); //compile error
    }

    public static void playFruitBase(List<? super Fruit> list){

    }

同理,参数List< ? super Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是Fruit的父类类型.

思考为什么要这么麻烦要区分开到底是xxx的父类还是子类,不能直接使用一个关键字表示吗?
前面说过,数组的协变之所以会有问题是因为在对数组中的元素进行存取的时候出现的问题,只要不对数组元素进行操作,就不会有什么问题,因此可以使用通配符?达到此效果:

public static void playEveryList(List < ?> list){
    //..
}

对于playEventList方法,传递任何类型的List都没有问题,但是你会发现对于list参数,你无法对里面的元素存和取。这样便达到了上面所说的安全类型的协变数组的效果。
但是绝大多数的时候,我们还是希望对元素进行操作的,这就是extends和super的功能。
<? extends Fruit> 表示传入的泛型类型具体类型必须继承自Fruit,那么我们可以里面的元素一定能向上转型为Fruit,但是也仅仅能确定里面的元素一定能向上转型为Fruit。

public static  void playFruit(List < ? extends  Fruit> list){
     Fruit fruit=list.get(0);
     //list.add(new Apple());
}

比如上面这段代码,可以正确的取出元素,因为我们知道所传入的参数一定是继承自Fruit的,比如

List<Apple> apples=new ArrayList<>();
List<Orange> oranges=new ArrayList<>();

都能正确的转换为Fruit。
但是我们并不知道里面的元素具体是什么,有可能是Orange,也有可能是Apple,因此,在List.add()的时候,就会抛出问题,有可能将Apple放入Orange里面,因此,为了不出错,编译器禁止向里面添加任何元素。这也就解释了协变中使用add会出错的原因。

同理:
<? super Fruit> 表示传入的泛型具体类型必须是Fruit的父类,那么我们可以确定只要元素是Fruit以及能转型为Fruit的,一定能向上转型为对应的此类型,比如:

public static void playFruitBase(List<? super Fruit> list){

        list.add(new Apple())
    }

因为Apple继承自Fruit,而参数list最终被指定的类型一定是Fruit的父类,那么Apple一定能向上转型为对应的父类,因此可以向里面存元素。
但是我们只能确定他是Fruit的父类,并不知道具体的上限,因此无法将取出的元素统一的类型(当然可以用Object)。
除了

Object obj;
obj=foods.get(0);
obj=eatables.get(0)

之外,没有确定类型可以修饰obj以达到类似的效果。
针对上面情况,我们可以总结为:PECS原则,Producer-Extends,Customer-Super, 也就是泛型代码是生成者,使用Extends,泛型代码作为消费者Super

泛型的实现原理

泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。

Java 编译器通过如下方式实现擦除:

1.用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;
2.在恰当的位置插入强制转换代码来确保类型安全;
3.在继承了泛型类或接口的类中插入桥接方法来保留多态性。

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

推荐阅读更多精彩内容