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

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

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

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

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

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

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

擦除类型之前的Pair类

public class Pair {
    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{
    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) 

    或者是强制类型转换:

Pair p = (Pair)a;

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

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

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

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

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

Pair pairs[] = new Pairs[10];

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

pairs[0] = "Hello";

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

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

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

4. Varagrs警告

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

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

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

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

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

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

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

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

    现在可以调用:

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

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

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

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

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

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

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

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

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

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

public static  Pair makePair(Supplier 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  Pair makePair(Class clazz){
    try {
        return new Pair<>(clazz.newInstance(), clazz.newInstance());
    }catch(Exception e) {
        return null;
    }
}

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

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

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

6. 不能构造泛型数组

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

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

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

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

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

public static 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[] minAndMax(IntFunction constr, T...ts){
    return constr.apply(2);
}

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

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

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

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

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

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

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

public class Problem extends Throwable{
    
}

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

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

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

public static  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  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里面抛出

你可能感兴趣的:(Java 基础-泛型的约束和局限性)