Java 基础-泛型的约束和局限性

Java基础-泛型的约束和局限性

    Java中的泛型是一个非常重要知识点,在这里,简单的介绍一下Java泛型的几个注意点。这里不会讲解Java中的泛型是怎么使用的,只会讲解在Java中使用的泛型的注意点

1. 不能使用基本数据类型实例化类型参数

    不能使用类型参数代替基本数据。因此,没有Pair<double>,只有Pair<Double>(这里我们假设Pair是一个public class Pair<T> 类型的一个类)。这个非常的好理解,想一想我们在使用List集合时,不能这样子来定义一个集合:List<int> list = new ArrayList<>(),通常都是这样来定义一个int类型的集合:List<Integer> list = new ArrayList<>();
    这个是什么原因呢?有人可能要问。我们这里需要讲一下Java中泛型的类型擦除

(1).Java泛型的类型擦除

    在我们定义一个泛型类的时候,都会自动的给我们提供一个相应的原始类型(这个原始类型不是像Integer对应的是原始数据类型是int)。这里的原始数据类型就是删除类型参数之后的泛型类型名、擦除类型变量,并且替换为限定类型(如果没有限定类型,那么就用Object来代替)。
    例如:

擦除类型之前的Pair类

public class Pair<T> {
    private T first = null;
    private T second = null;
    public Pair() {
        this.first = null;
        this.second = null;
    }
    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setSecond(T second) {
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }

    
}

擦除类型之后的Pair类

public class Pair {
    private Object first;
    private Object second;
    public Pair() {
        this.first = null;
        this.second = null;
    }
    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
    
    public void setFirst(Object first) {
        this.first = first;
    }
    
    public void setSecond(Object second) {
        this.second = second;
    }
    public Object getFirst() {
        return first;
    }
    public Object getSecond() {
        return second;
    }
}

    我们会发现在在擦除之前,Pair里面的成员变量类型都是T类型,也就是泛型类型。但是在擦除之后,所有T类型都变成了Object类型。这个也就是我们之前说的,如果一个类型是无限定类型的话,会被替换成为Object类型。
    如果泛型类型被限制了的,也就是T extends其他的类或者接口,就取extends关键字之后第一个类型来作为擦除之后的类型。为什么这里要强调是extends关键字之后第一个类型呢?因为extends关键字之后可以跟多个类或者接口,多个类或者接口使用&来连接。
    例如,可以这样写:

public class Interval<T extends Comparable & Serializable>{
    private T lower;
    private T upper;
    .....
}

    擦除类型之后:

public class Interval{
    private Comparable lower;
    private Comparable upper;
    ......
}

(2).不用基本数据类型的原因

    非常的明显,这里的原因肯定是类型擦除导致的。擦除之后,Pair类含有Object类型的成员变量,但是Object不能存储double类型的值。

2.运行时类型查询只适用于原始类型

    在Java中,我们知道可以使用instanceof关键字来判断一个引用是否是一个类的对象。在泛型里面,这种代码是不支持的:

if(a instanceof Pair<String>) 

    或者是强制类型转换:

Pair<String> p = (Pair<String>)a;

同样的道理,使用getClass方法返回的原始类型:

Pair<String> stringPair = new Pair<>();
Pair<Integer> integerPair = new Pair<>();
if(stringPair.getClass() == integerPair.getClass()){ //true
    
}

    他们的比较结果是true,因为两次调用getClass方法都将返回的是Pair.class对象,是同一个对象。

3. 不能创建泛型类型的数组

    不能创建泛型类型的数组,例如:

Pair<String> pairs[] = new Pairs<String>[10];

    这个是为什么呢?
    假设,记住这里是假设,如果能够创建泛型类型的数组,也就是说,我们上面的pairs数组是定义成功了的,那么我们如此操作,编译器是会报错的:

pairs[0] = "Hello";

    这个报错的原因是非常简单的,Pair<String>类型的数组,不能存储一个String类型的数据。
    但是类型擦除会导致这个机制失效(类型匹配的机制)。因为如果定义泛型类型的数组成功的话,在擦除类型之后,数组的类型就从Pair<String>[]类型转换为Pair[],那么Pair[]类型可以变为Object[]类型:

Pair pairs[] = new Pairs[10];//这里重新定义一个Pair类型的数组,表示类型擦除
Object[] obejcts = pairs;//将Pair类型的数组转换为Object类型的数组
objects[0] = "Hello";//这里就不会报错,因为这里数组存储的是Object类型,所以不会报错。

    由于这个原因--能够通过数组存储数组的类型检查,出于这个原因,不允许创建泛型类型的数组。
    需要说明的是,只是不允许创建泛型类型的数组,而生命类型为Pair<String>[]的变量仍是合法的,只是不允许使用new Pair<String>[10]这种方式来初始化变量。
    注意:可以声明通配类型的数组,然后进行强制类型转换:Pair<String> pairs = (Pair<String>[]) new Pair<?>[10]

4. Varagrs警告

    在上一节中,我们已经了解到了Java中不支持泛型类型的数组。这一节中我们再来讨论一下相关的问题:向参数个数可变的方法传递一个泛型类型的对象。
    例如:

public static <T> void addAll(Collection<T> coll, T...ts){
    for(T t:ts){
        coll.add(t);
    }
}

    我们知道,在addAll方法中的ts参数是一个数组。
    现在我们这样调用这个方法:

Collection<Pair<String>> coll = new ArrayList<>();
Pair<String> pair1 = new Pair<>();
Pair<String> pair2 = new Pair<>();
Pair<String> pair3 = new Pair<>();
addAll(coll, pair1, pair2, pair3);

    为了成功的调用addAll方法,Java虚拟机必须为我们创建一个Pair<String>类型的数组,这个就违反了前面的规则。不过,对于这种情况,规则有所放松,这里只是一个警告,而不是错误。
    可以采用两种方法来抑制这个警告。一种方法是在addAll方法的前面增加注解@SuppressWarnings("unchecked");或者在Java7中,还可以使用@SafeVarargs直接标注addAll方法:

@SafeLVarargs
public static <T> void addAll(Collection<T> coll, T...ts)

    注意:
    这里我们可以使用@SafeVarargs注解来消除泛型数组的有关限制,方法如下:

@SafeVarargs
public static <E> E[] array(E...array){
    return array;
}

    现在可以调用:

Pair<String>[] pairs = array(pair1, pair2);

    这个看起来非常的方便,不过隐藏着危险,以下代码:

Object[] oejcts = pairs;
objects[0] = new Pair<Integer>();

    这里能够顺利运行而且不会出现ArrayStoreException异常(因为数组存储时,只会检查擦除之后的类型),但是在处理pairs[0]时,有可能会在别处得到一个异常。

5. 不能创建泛型类型的变量

    不能使用像new T(...)、new T[...]或者T.class这样的表达式。例如,下面Pair<T>的构造方法是非法:

public Pair(){
    this.first = new T();
    this.second = new T();
}

    类型擦除之后,将T变为了Object,而且本意上不是调用Object().在Java 8 出现之后,最好的解决办法是:让调用提供一个构造器的表达式,例如:

Pair<String> p = Pair.makePair(String::new);

    makePair方法接收一个Supplier<T>类型的对象,这是一个函数式接口,表示一个无参数但是返回类型为T的函数:

public static <T> Pair<T> makePair(Supplier<T> constr){
    return new Pair<>(constr.get(), constr.get());
}

    这种方式在Java 8比较适用,如果各位读者对Java 8不是很熟悉的,可以先去看看Java 8中方法引用,这里其实就是将Lambda表达式简写成为了方法引用的形式,也就是所谓的语法糖。
    但是在传统的想法中,我们比较倾向于通过反射调用Class.newInstance方法来创建泛型对象:

first = T.class.newInstance();

    但是遗憾的是,细节比较复杂,而且不能调用。表达式T.class是不合法的,因为擦除之后,类型成为Object.class。所以必须通过以下方法来设计,以便得到一个Class对象:

public static <T> Pair<T> makePair(Class<T> clazz){
    try {
        return new Pair<>(clazz.newInstance(), clazz.newInstance());
    }catch(Exception e) {
        return null;
    }
}

    然后通过如下方法来调用:

Pair<String> pair = Pair.makePair(String.class);

    注意,Class类本身是泛型。例如,String.class是一个Class<String>的对象。因此,makePair方法能够判断出pair的类型。

6. 不能构造泛型数组

    就像不能创建一个泛型类型的对象,也不能创建泛型类型的数组。不过原因有所不同,毕竟数组会填充null值,构造是看上去是安全的。不过,数组本身也有类型,用来监控在虚拟机中的数组,这个类型会被擦除。例如:

public static <T extends Comparable> T[] minAndMax(T a[]){
    T ts[] = new T[2];
    ......
    return ts;
}

    类型擦除会让这个方法永远构造Comparable类型的数组。
    但是如果数组是一个类的私有成员变量,就可以使用Object类型的数组,并且在获取元素时,进行类型转化。例如,ArrayList类可以这样实现:

public class ArrayList<E>{
    private Object[] elements;
    ......
    @SuppressWarnings("unchecked")
    public E get(int index){
        return (E)elements[index];
    }
    
    public void set(E e, int index){
        elements[index] = e;
    }
}

    实际上也可以这样写:

public class ArrayList<E> {
    private E[] elements;
    
    @SuppressWarnings("unchecked")
    public ArrayList() {
        this.elements = (E[])new Object[10];
    }
}

    在minAndMax方法中,由于该方法返回的是一个泛型类型的数组,所以像上面的操作不能进行,但是如果想要实现功能的话,可以如下实现:

public static<T extends > T[] minAndMax(T...ts){
    Object[] objects = new Object[10];
    ......
    return (T[]) objects;
}

    然后调用代码:

String ss[] = ArrayAlg.minAndMax("pby", "pby123", "pby456");

    上面这段代码在编译阶段是没有错误的,但是当我们调用这个方法会抛出一个ClassCastException异常。
    在这种情况下,可以让用户提供一个数组的构造器表达式:

String [] ss = ArrayAlg.minAndMax(String[]::new, "pby", "pby123", "pby456");

    然后在minAndMax方法中使用这个参数生成一个正确类型的数组:

public static <T extends Comparable> T[] minAndMax(IntFunction<T[]> constr, T...ts){
    return constr.apply(2);
}

    上面的写法是基于Java 8中的方法引用。如果使用老式的Java反射,调用Array.newInstance方法:

public static <T extends Comparable> T[] minAndMax(T...a){
    return (T[])Array.newInstance(a.getClass().getComponentType(), 2);
}

7.泛型类的静态上下文在泛型类型中无效

    静态变量不能定义泛型类型,静态方法的返回类型不能定义为泛型类型。例如,下面的写法是错误的:

public class Interval<T>{
    private static T singleInstance; //错误,静态变量的类型不能为泛型类型
    public static T getSingleInstance(){ //错误,静态方法的返回类型为泛型类型。
        return singleInstance;
    }
}

8.不能抛出或者捕获泛型类的异常

    在Java中,不能对泛型类的异常对象进行抛出捕获。实际上,泛型类继承于Throwable类都是不合法的,例如,以下的代码是错误的:

public class Problem<T> extends Throwable{
    
}

    同时不能再catch语句中使用泛型类型的异常对象。例如:

public static <T extends Throwable> void doWork(Class<T> clazz){
    try{
        
    }catch(T e){ //错误,不能抛出泛型类型的异常对象
        
    }
}

    不过,在异常规范中,使用泛型类型的对象是允许的:

public static <T extends Throwable> void doWork(T t){
    throws T
    try{
        
    }catch(Throwable realCause){
        t.initCause(realCause);
        throw t;
    }
}

9. 可以消除对受查异常的检查

    Java异常处理的一个基本规则是:必须为所有受查异常提供一个处理器。不过我们可以利用这个泛型来取消这个限制。例如:

public abstract class Block {
    public abstract void body() throws Exception;
    public Thread toThread() {
        return new Thread() {
            @Override
            public void run() {
                try {
                    body();
                }catch(Throwable t) {
                    Block.throwAs(t);
                }
            }
        };
    }
    
    @SuppressWarnings("unchecked")
    public static <T extends Throwable> void throwAs(Throwable t) throws T{
        throw (T) t;
    }
}

    然后我们在main方法里面开启一个线程来调用我们的方法。

public class Demo {
    public static void main(String []args) {
        new Block() {
            
            @Override
            public void body() throws Exception{
                
            }
        }.toThread().start();
    }
}

    有人可能会问这个有什么意义上呢?正常情况下,我们必须捕获run方法里面所有受查异常,不能从run方法里面向外面抛出一个异常,因为在Thread类里面的run方法没有抛出任何的异常,所以我们这里向外抛出任何的异常,所有的受查异常都必须爱run方法里面进行捕获。但是我们这里的操作就是,将受查异常包装为非受查异常,然后在catch里面抛出

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

推荐阅读更多精彩内容

  • 引言:泛型一直是困扰自己的一个难题,但是泛型有时一个面试时老生常谈的问题;今天作者就通过查阅相关资料简单谈谈自己对...
    cp_insist阅读 1,804评论 0 4
  • Why ——引入泛型机制的原因 假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到...
    absfree阅读 5,029评论 1 6
  • 泛型是Java 1.5引入的新特性。泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别...
    何时不晚阅读 2,976评论 0 2
  • 在经过一次没有准备的面试后,发现自己虽然写了两年的android代码,基础知识却忘的差不多了。这是程序员的大忌,没...
    猿来如痴阅读 2,741评论 3 10
  • 最近看了一部电影,时光尽头的恋人,感触颇深。 一次车祸,落入水中,体温下降,生命终止。闪电击中,基因端粒不...
    飘舞的星空阅读 166评论 0 1