java泛型探秘(一):泛型是什么

目录

一. 泛型基本概念

二. java泛型是什么&为什么使用泛型

三. java泛型的继承关系


一. 泛型基本概念

在维基百科上泛型是用这样一句话定义的:

Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.

中文翻译:

泛型编程是一种编程风格,提供“待具体化”类型的算法,实例化时才需要提供具体类型当做参数传递进来

通俗点解释:

在部分代码中可以定义可变类型,即将类型定义为参数,只有当这部分代码需要实例化的时候,才需要具体类型当做参数传递进来。

       常见的参数传递一般都是值传递,参数类型是确定的,并且无法改变的,如果需要传入其他类型的值,就需要修改代码。而泛型编程可以在不需要修改代码的情况下,传入不同类型的值,这么做的好处是显而易见的,如代码块复用、类型安全检查、编码更灵活、代码可读性更高、提高效率等。常见的泛型实现有c++模板和java泛型等,它们的实现机制和使用方法有很大不同,本文主要讨论java泛型。

二. java泛型是什么&为什么使用泛型

       java泛型可以在类、接口和方法上定义参数化类型,在需要实例化类或者调用方法时,提供实际参数类型。java泛型提供了编译期类型检查,如下代码:

/**
 * 苹果
 */
class Apple{ }

/**
 * 橙子
 */
class Orange{ }

/**
 * 储物盒
 */
public class StoreBox<T> {
    
    List<T> boxs = new ArrayList<T>();
    
    /**
     * 放入物品
     * @param t
     */
    public void put(T t){
        boxs.add(t);
    }
    
    /**
     * 取出物品
     * @return
     */
    public T pop(){
        return boxs.get(boxs.size() - 1);
    }
    
    public static void main(String[] args) {
        
        StoreBox<Apple> appleBox = new StoreBox<Apple>(); // 定义一个装苹果的储物盒
        
        appleBox.put(new Apple()); // 可以成功放入一个苹果
        
        appleBox.put(new Orange()); // 无法编译, 提示无法将一个橙子放入到一个装苹果的储物盒中
    }
}

      上面代码定义了储物盒StoreBox,但是不确定储物盒具体装什么类型的东西,可以装苹果,也可以装橙子,所以在定义StoreBox时,使用了T占位符代表物品类型,在使用时,需要用具体的类型替换T,如在上面代码定义储物盒appleBox时,需要将T替换成Apple,指明appleBox只能装苹果,装橙子则不通过。

       上面代码使用了泛型,很容易地构造了一个装苹果和装橙子的储物盒,如果试着在不使用泛型的前提下,要实现既可以装苹果也能装橙子的储物盒,就要用到java继承的特性,定义一个可以装Object的储物盒,如下:

/**
 * Object储物盒
 */
public class ObjectStoreBox {
    
    List<Object> boxs = new ArrayList<>();
    
    /**
     * 放入物品
     * @param t
     */
    public void put(Object t){
        boxs.add(t);
    }
    
    /**
     * 取出物品
     * @return
     */
    public Object pop(){
        return boxs.get(boxs.size() - 1);
    }
    
    public static void main(String[] args) {
        
        ObjectStoreBox appleBox = new ObjectStoreBox(); // 定义一个装苹果的储物盒
        
        appleBox.put(new Apple()); // 可以成功放入一个苹果
        
        appleBox.put(new Orange()); // 成功将一个橙子放入到一个装苹果的储物盒中
    }
}

       上面代码定义了一个储物盒ObjectStoreBox,储存类型为Object的物品,java的所有类都继承自Object,所以ObjectStoreBox能存储任意类型的物品。上面代码创建了一个希望装苹果的储物盒appleBox,苹果继承自Object,可以放入苹果,又因为橙子也继承自Object,所以也能放入到appleBox,违反了希望appleBox只能装苹果的本意。如果使用泛型编程则可以避免这类问题,泛型会进行编译期类型检查,避免脏数据的编码插入,保证运行时数据类型都是正确的。

三. java泛型的继承关系

       众所周知,数组具有协变关系的,即如果有两个类A、B,A是B的父类,那么A a = new B()是成立的, A[] a = new B[3]也是成立的,数组的协变意味着两个类的继承关系延续到数组中也是同样成立的。数组设计成协变是有原因的,比如一些方法Arrays.sort,Arrays.equals等,这些方法提供了排序、判断两个数组是否相等的功能,这些功能都是所有数组经常能用到的,如果数组不支持协变,并且在1.5版本之前还没有泛型,要想让这些方法对所有数组都有效,实现起来难度很大,那么支持协变无疑是最简单的实现方式。但是同时,数组支持协变意味着放弃了编译期类型检查,如以下代码能正常编译,但运行时会报错:

class A { }

/**
 * B是A的子类
 */
class B extends A { }

/**
 * C是A的子类
 */
class C extends A { }


public class Array {
    
    public static void main(String[] args) {
        
        A[] tArray = new B[10];
        
        tArray[0] = new C(); // 编译成功, 运行时抛出异常java.lang.ArrayStoreException
    }

}

       上面代码中,B是A的子类,因为数组支持协变,所以可以用A[]声明数组tArray,实际初始化数组B[10],另外C也是A的子类,所以可以将tArray第一个元素赋值C实例。这段代码可以成功编译,但是运行时,数组会检查赋值类型,因为tArray整个数组实际指向的B[10],tArray的第一个元素tArray[0]错误赋值了C的实例,虚拟机检查实例类型,发现类型不匹配抛出运行时异常java.lang.ArrayStoreException。

       既然数组是协变的,那么泛型也支持协变吗?如下代码:

class A { }

class B extends A { }

class C extends A { }

/**
 * Box
 * @param <T>
 */
public class GenericBox<T> {
    
    
    List<T> boxs = new ArrayList<T>();
    
    /**
     * 放入物品
     * @param t
     */
    public void put(T t){
        boxs.add(t);
    }
    
    /**
     * 取出物品
     * @return
     */
    public T pop(){
        return boxs.get(boxs.size() - 1);
    }
    
    
    public static void main(String[] args) {
        
        GenericBox<A> tBox = new GenericBox<B>(); // 编译不通过, 提示: Type mismatch: cannot convert from GenericBox<B> to GenericBox<A>
        
        // tBox.put(new C());  // 放入C物品
        
        // B tb = (B)tBox.pop(); // 取出物品
    }
}

       上面这段代码中的类A是类B的父类,如果泛型也支持协变,那么GenericBox<B>和GenericBox<A>也具有继承关系,上面代码试图将GenericBox<B>实例赋值给声明为GenericBox<A>的tBox,提示类型无法匹配无法编译,表明java泛型是不支持协变的。java泛型不支持协变的根本原因在于java泛型目前的实现机制,java泛型会在编译期擦除泛型信息,在虚拟机运行阶段,虚拟机是不知道有泛型存在的。假如java泛型支持协变,即GenericBox<A> tBox = new GenericBox<B>()如果能通过编译,向tBox 放入元素,因为C是A的子类,那么上面代码中的tBox.put(new C())也会成功编译。在运行阶段,泛型信息被擦除了,虚拟机无法检查类型,tBox.put(new C())正常执行,向tBox插入了C的实例。tBox指向的是GenericBox<B>的实例,本意是存储B类型物品,当从tBox中取出B类型的物品,运行上面代码中的B tb = (B)tBox.pop(),tBox.pop()实际取出的C类型物品,强转B类型会抛出异常。所以按照现有java泛型的擦除机制,如果强行支持协变,会同时丢失编译器类型安全检查和运行期安全检查,不可避免地会造成脏数据乱入和运行时异常,因此java泛型为了保证类型安全检查,是不支持协变的,如下图:

         虽然java泛型不支持协变(类继承关系在泛型中的延续),但是java泛型是支持继承的,以jdk中的集合类为例,如下图:

         具有两个泛型的类型也能继承自只有一个泛型的类型,如下代码:

interface PayloadList<E,P> extends List<E> {

  void setPayload(int index, P val);
}

         PayloadList<E,P>具有两个泛型,继承关系如下:

       需要注意是虽然可以用泛型类的实例赋值给该泛型类的rawType(原生类型),但是rawType和该泛型类(或者该泛型类的子类或实现类)不具有继承关系,如下代码:

List rawList = new ArrayList<String>(); 

       上面代码中,用泛型类List<E> 的rawType声明rawList,虽然可以将List<E>子类(实现类)的ArrayList<E>的实例赋值给rawType,但是List 和List<E>(或者ArrayList<E>)并没有继承关系。java之所以允许rawType存在,是因为List等集合类在泛型出现之前就已经存在了,为了不增加用户的学习成本,java设计者直接将原先的List等集合类直接泛型化,没有重新编写新的泛型集合类,同时为了兼容之前非泛型的List等集合类,将之前未泛型化的类称为rawType,允许将泛型化的集合类的实例赋值给rawType。

 👉👉👉 自己搭建的租房网站:全网租房助手,m.kuairent.com,每天新增 500+房源

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值