Java 泛型 泛型的约束与局限性

@author ixenos

不能用基本类型实例化类型参数


不能用类型参数代替基本类型:例如,没有Pair<double>,只有Pair<Double>,其原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。这体现了Java语言中基本类型的独立状态。

运行时类型查询只适用于原始类型(raw type)


运行时:通常指在Classloader装载之后,JVM执行之时

类型查询:instanceof、getClass、强制类型转换

原始类型:即(raw type),泛型类型经编译器类型擦除后是Object或泛型参数的限定类型(例如Pair<T extends Comparable>,Comparable就是T的限定类型,转化后泛型的原始类型就是Comparable,所以Pair类不带泛型是Pair<Comparable>),即Pair类含有Comparable类型的域

JVM中没有泛型

eg:

if(a instanceof Pair<String>) //ERROR,仅测试了a是否是任意类型的一个Pair,会看到编译器ERROR警告

if(a instanceof Pair<T>) //ERROR

Pair<String> p = (Pair<String>) a;//WARNING,仅测试a是否是一个Pair

Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass() == employeePair.getClass()) //会得到true,因为两次调用getClass都将返回Pair.class

不能创建参数化类型的数组(泛型数组)


参数化类型的数组:指类型带有泛型参数的数组,也即泛型数组,如Pair<T>[] 、 T[]

  不能实例化参数化类型的数组,例如:

Pair<String> table = new Pair<String>[10]; //ERROR

  在这里我们假设可以实例化,那么经编译器类型擦除后,table的类型是Pair[],我们再让它协变为Object[]:

Object[] objArray = table;

  而一般来说,数组会记住他的元素类型Pair,我们如果试图存储其他类型的元素,就会抛出异常(数组存储检查),例如:

objArray[0] = "Hello"; //ERROR--component type is Pair

  但是,对于泛型类型Pair<String>,类型擦除会使这种不同类检查机制无效,这就是不能实例化泛型数组的原因

objArray[0] = new Pair<Employee>();  //如果泛型机制允许我们实例化数组,那么这一步就没理由出错了!而这违背了我们的初衷(限定类型)
  • 数组存储只会检查擦除后的类型,又因为Java语言设计数组可以协变,所以可以通过编译
  • 能够通过数组存储检查,不过仍会导致一个类型错误,故不允许创建参数化类型的数组
  • 注意,声明类型为Pair<String>[]的变量是合法的,只是不能创建这些实例(我们应该直接用new Pair<String>[10]{......}来初始化这个变量)

泛型数组的间接实现

通过泛型数组包装器,如ArrayList类,维护一个Object数组,然后通过进出口方法set、get来限定类型和强制转换数组类型,从而间接实现泛型数组,

例如:ArrayList: ArrayList<Pair<T>>、ArrayList<T>

不能实例化类型变量T


  • 即不能使用new T(..) , new T[..] 或 T.class 这样的表达式中的类型变量

    • 例如: public Pair() { first = new T(); } //ERROR! 类型擦除将T改变成Object,调用非本意的new Object()
  • 不能使用new T(..) 
    • 但是,可通过反射调用Class.newInstance方法来构造泛型对象(要注意表达式T.class是非法的)
    • public static <T> Pair<T> makePair(Class<T> cl){
      try{ return new Pair<>(cl.newInstance() , cl.newInstance()); }
      catch(Exception ex) { return null; }
      } //这个方法可以按照下列方式调用:
      Pair<String> p = Pair.makePair(String.class);  
    • 注意:Class类本身是泛型。String.class是一个Class<String>的实例,因此makePair方法能够推断出pair的类型
  • 不能使用new T[..]
    • 即不能(直接)创建泛型数组,参考我另一篇博文(http://www.cnblogs.com/ixenos/p/5648519.html),这里不再赘述
    • 解决方案:使用泛型数组包装器,例如ArrayList
      • 然而,当在设计一个泛型数组包装器时,例如方法minmax返回一个T[]数组,则泛型数组包装器无法施展,因为类型擦除,return (T [])new Object是没有意义的强转不了。此时只好利用反射,调用Array.newInstance:
      • import java.lang.reflect.*;
        ...
        public static <T extends Comparable> T[] minmax(T... a){
        T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType() , );
        ...
        }
        • 【API文档描述】public Class<?> getComponentType() 返回表示数组组件类型的 Class。如果此类不表示数组类,则此方法返回 null。
    • 而ArrayList类中的toArray方法的实现就麻烦了

      • public Object[] toArray() 无参,返回Object[]数组即可 

        public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
        }
        • 【API文档描述】public static <T> T[] copyOf(T[] original,int newLength)
            复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。对于在原数组和副本中都有效的所有索引,这两个数组将包含相同的值。对于在副本中有效而在原数组无效的所有索引,副本将包含 null。当且仅当指定长度大于原数组的长度时,这些索引存在。所得数组和原数组属于完全相同的类。 
      • public <T> T[] toArray(T[] a) a - 要存储列表元素的T[]数组(如果它足够大)否则分配一个具有相同运行时类型的新数组,返回该T[]数组
      • @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
        if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass()); //a.getClass()得运行时目的数组的运行时类型
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
        a[size] = null;
        return a;
        }
        •  【API文档描述】

          public static <T,U> T[] copyOf(U[] original,int newLength, Class<? extends T[]> newType)
          复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。对于在原数组和副本中都有效的所有索引,这两个数组将包含相同的值。对于在副本中有效而在原数组无效的所有索引,副本将包含 null。当且仅当指定长度大于原数组的长度时,这些索引存在。所得数组属于 newType 类。

 泛型类的静态上下文中类型变量无效


  • 泛型类不能在静态域或静态方法中引用类型变量
  • public class Singleton<T>{
    private static T singleInstance; //ERROR
    public static T getSingleInstance(){...} //ERROR
    }
    • 类型擦除后只剩下Singleton类,因为静态所以他只包含一个singleInstance域,如果能运行则以Singleton类为模板生成不同类型的域,因此产生了冲突

不能throws或catch泛型类的实例(有关异常)


  • 泛型类继承Throwable类不合法,如public class Problem<T> extends Exception {...} //ERROR 不能通过编译
  • catch子句不能使用类型变量
  • public static <T extends Throwable> void doWork(Class<T> t){
    try{
    do work
    }catch (T e){ // ERROR
    Logger.global.info(...)
    }
    }
  • 不过,在异常规范中使用类型变量是允许的:  
  • public static <T extends Throwable> void doWork(T t) throws T { //此时可以throws T
    try{
    do work
    }catch (Throwable realCause){ //捕获到具体实例
    t.initCause(realCause);
    throw t; //这时候抛具体实例,所以throw t 和 throws T 是可以的!
    }
    }
    • 此特性作用:可以利用泛型类、类型擦除、SuppressWarnings标注,来消除对已检查(checked)异常的检查,

      • unchecked和checked异常: Java语言规范将派生于Error类或RuntimeException的所有异常称为未检查(unchecked)异常,其他的是已检查(checked)异常
      • Java异常处理原则:必须为所有已检查(checked)异常提供一个处理器,即一对一个,多对多个
        @SuppressWarnings("unchecked")  //SuppressWarning标注很关键,使得编译器认为T是unchecked异常从而不强迫为每一个异常提供处理器
        public static <T extends Throwable> void throwAs(Throwable e) throws T{ //因为泛型类型擦除,可以传递任意checked异常,例如RuntimeException类异常
        throw (T) e;
        }
        • 假设该方法放在类Block中,如果调用 Block.<RuntimeException>throwAs(t); 编译器就会认为t是一个未检查的异常
        • public abstract class Block{
          public abstract void body() throws Exception;
          public Thread toThread(){
          return new Thread(){
          public void run(){
          try{
          body();
          }catch(Throwable t){
          Block.<RuntimeException>throwAs(t);
          }
          }
          };
          } @SuppressWarnings("unchecked")
          public static <T extends Throwable> void throwAs(Throwable e) throws T{
          throw (T) e ;
          }
          }
        • 再写个测试类
        • public class Test{
          public static void main(String[] args){
          new Block(){
          public void body() throws Exception{
          //不存在ixenos文件将产生IOException,checked异常!
          Scanner in = new Scanner(new File("ixenos"));
          while(in.hasNext())
          System.out.println(in.next());
          }
          }.toThread().start();
          }
          }
          • 启动线程后,throwAs方法将捕获线程run方法所有checked异常,“处理”成unchecked Exception(其实只是骗了编译器)后抛出;
          • 有什么意义?正常情况下,因为run()方法声明为不抛出任何checked异常,所以必须捕获所有checked异常并“包装”到未检查的异常中;意义:而我们这样处理后,就不必去捕获所有并包装到unchecked异常中,我们只是抛出异常并“哄骗”了编译器而已

注意擦除后的冲突


  • Java泛型规范有个原则:“要想支持擦除的转换,就需要强行限制一个泛型类或类型变量T不能同时成为两个接口类型的子类而这两个接口是统一接口的不同参数化

    • 注意:非泛型类可以同时实现同一接口,毕竟没有泛型,很好处理
    • class Calender implements Comparable<Calender>{...}
      
      class GGCalender extends Calender implements Comparable<GGCalender>{...} //ERROR
      • 在这里GGCalender类会同时实现Comparable<Calender> 和 Comparable<GGCalender>,这是同一接口的不同参数化

Java 泛型 泛型的约束与局限性的更多相关文章

  1. Java泛型解析(04):约束和局限性

    Java泛型解析(04):约束和局限性           前两节.认识和学习了泛型的限定以及通配符.刚開始学习的人可能须要一些时间去体会到泛型程序设计的优点和力量,特别是想成为库程序猿的同学就须要下 ...

  2. Java核心技术-泛型程序设计

    使用泛型机制编写的代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性. 泛型对于集合类尤其有用 1 为什么要使用泛型程序设计 泛型程序设计意味着编写的代码可以 ...

  3. java基础-泛型举例详解

    泛型 泛型是JDK5.0增加的新特性,泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数.这种类型参数可以在类.接口.和方法的创建中,分别被称为泛型类.泛型接口.泛型方法. 一.认识泛型 在没 ...

  4. Java中泛型 类型擦除

    转自:Java中泛型是类型擦除的 Java 泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,但有一点需要注意:Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类 ...

  5. 【Java】泛型学习笔记

    参考书籍 <Java核心技术:卷1> 泛型, 先睹为快 先通过一个简单的例子说明下Java中泛型的用法: 泛型的基本形式类似于模板, 通过一个类型参数T, 你可以"私人定制&qu ...

  6. Java“禁止”泛型数组

    Java“禁止”泛型数组 原文:https://blog.csdn.net/yi_Afly/article/details/52058708 1. 泛型定义泛型编程是一种通过参数化的方式将数据处理与数 ...

  7. 和我一起学Effective Java之泛型

    泛型 不要在新代码中使用原始类型 泛型(generic):声明中具有一个或多个类型参数 原始类型(raw type):不带任何实际类型参数的泛型名称 格式: 类或接口的名称 < 对应于泛型形式类 ...

  8. Java中泛型区别以及泛型擦除详解

    一.引言 复习javac的编译过程中的解语法糖的时候看见了泛型擦除中的举例,网上的资料大多比较散各针对性不一,在此做出自己的一些详细且易懂的总结. 二.泛型简介 泛型是JDK 1.5的一项新特性,一种 ...

  9. C++ Java C#泛型

    泛型概述C#中的泛型C#泛型和java泛型的比较C#泛型和C++模板的比较C#泛型中的约束 泛型概述 Bruce Eckel :您能对泛型做一个快速的介绍么? Anders Hejlsberg : 泛 ...

随机推荐

  1. JavaScript的Array.prototype.filter()详解

    摘抄与:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter 概述 ...

  2. 使用node.js编写脚本将JSON数据转换为SQL语句

    安装依赖模块 当node.js脚本在运行的时候,需要很多支持模块,这些模块存储在node_modules文件夹中.该脚本在执行过程中需要使用到fs.string-format两个支持模块,作用分别是: ...

  3. RubyMine 2016.1 下载 附注册激活码 破解版方法

    注册破解方法: 在要求输入注册的界面选择激活码,然后粘贴以下注册码: 43B4A73YYJ-eyJsaWNlbnNlSWQiOiI0M0I0QTczWVlKIiwibGljZW5zZWVOYW1lIj ...

  4. NYOJ-括号配对问题 <技巧性的非栈道法>

    括号配对问题 时间限制:3000 ms  |  内存限制:65535 KB 难度:3  描述 现在,有一行括号序列,请你检查这行括号是否配对. 输入 第一行输入一个数N(0<N<=100) ...

  5. laravel 添加第三方扩展库

    确定需要安装的位置 common.php测试代码 打开cmd 跳转到项目根目录下运行命令 composer install 打开文件 vender/composer/autoload_classmap ...

  6. html基础及心得

    html开始 <adress></adress>斜体(地址) <em><em>斜体(表示强调) <code></code>插入一 ...

  7. 图像处理_imgproc笔记(1)

    图像处理_滤波器 (1)图像的平滑处理 图像的平滑也称模糊,平滑处理需要一个滤波器,最常用的滤波器就是线性滤波器,线性滤波器的输出像素值是g(x,y),是输入像素值是  f(x,y)的加权和:    ...

  8. JavaScript忍者秘籍——运行时代码求值

    1. 代码求值机制 JavaScript中,有很多不同的代码求值机制. ● eval()函数 ● 函数构造器 ● 定时器 ● <script>元素 - 用eval()方法进行求值 作为定义 ...

  9. C#项目间循环引用的解决办法,有图有真相

    C#项目间循环引用的解决办法,有图有真相 程序间的互相调用接口,c#禁止互相引用,海宏软件,20160315 /// c#禁止互相引用,如果项目[订单]中有一个orderEdit单元,要在项目[进销存 ...

  10. [M]带属性块参照的转换

    有一张表格,表格的每一行都由带有属性的块参照组成,如图: 魔法表格不能直接识别有块参照组成的表格,需要使用 EXPLODE 命令将块参照分解,但多分解带有属性的块只能得到属性的定义 这是就需要使用 B ...