Java泛型的使用

115 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情

精通Java泛型的使用

泛型之前

在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。

通过继承设计通用程序

在Java增加泛型类型之前,通用程序的设计就是利用继承实现的,例如,ArrayList类只维护一个Object引用的数组,Object为所有类基类。

    public class BeforeGeneric {
    /**
     * 泛型之前的通用程序设计
     */
    static class ArrayList{
        private Object[] elements=new Object[0];
        public Object get(int i){
            return elements[i];
        }
        /**
         * 这里的实现,只是为了演示,不具有任何参考价值
         * */
        public void add(Object o){
            int length = elements.length;
            Object[] newElements = new Object[length+1];
            for(int i=0; i<length; i++){
                newElements[i] = elements[i];
            }
            newElements[length] = o;
            elements = newElements;
        }
    }
 
    public static void main(String[] args) {
        ArrayList stringValues = new ArrayList();
        //可以向数组中添加任何类型的对象
        stringValues.add(1);
        //问题1——获取值时必须强制转换
        String str = (String) stringValues.get(0);
        //问题2——上述强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException
        System.out.println(str);
    }
}

面临的问题

  • 当我们获取一个值的时候,必须进行强制类型转换。
  • 当我们插入一个值的时候,无法约束预期的类型。假定我们预想的是利用stringValues来存放String集合,因为ArrayList只是维护一个Object引用的数组,我们无法阻止将Integer类型(Object子类)的数据加入stringValues。然而,当我们使用数据的时候,需要将获取的Object对象转换为我们期望的类型(String),如果向集合中添加了非预期的类型(如Integer),编译时我们不会收到任何的错误提示。但当我们运行程序时却会报异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at generic.BeforeGeneric.main(BeforeGeneric.java:24)

这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。

泛型

针对利用继承来实现通用程序设计所产生的问题,泛型提供了更好的解决方案:类型参数。例如,ArrayList类用一个类型参数来指出元素的类型。

    ArrayList<String> stringValues=new ArrayList<String>();

这样的代码具有更好的可读性,我们一看就知道该集合用来保存String类型的对象,而不是仅仅依赖变量名称来暗示我们期望的类型。

    public class GenericType {
    public static void main(String[] args) {  
        ArrayList<String> stringValues=new ArrayList<String>();
        stringValues.add("str");
        stringValues.add(1); //编译错误
    } 
}

现在,如果我们向ArrayList添加Integer类型的对象,将会出现编译错误。

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
The method add(int, String) in the type ArrayList is not applicable for the arguments (int)
at generic.GenericType.main(GenericType.java:8)

编译器会自动帮我们检查,避免向集合中插入错误类型的对象,从而使得程序具有更好的安全性。

总之,泛型通过类型参数使得我们的程序具有更好的可读性和安全性。

Java泛型的实现原理

擦除

   public class GenericType {
    public static void main(String[] args) {  
        ArrayList<String> arrayString=new ArrayList<String>();   
        ArrayList<Integer> arrayInteger=new ArrayList<Integer>();   
        System.out.println(arrayString.getClass()==arrayInteger.getClass());  
    }  
}
 

输出:

true

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型,只能存储字符串。一个是ArrayList泛型类型,只能存储整型。最后,我们通过arrayString对象和arrayInteger对象的getClass方法获取它们的类信息并比较,发现结果为true。

这是为什么呢,明明我们定义了两种不同的类型?因为,在编译期间,所有的泛型信息都会被擦除,List和List类型,在编译后都会变成List类型(原始类型)。Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。

原始类型

原始类型就是泛型类型擦除了泛型信息后,在字节码中真正的类型。无论何时定义一个泛型类型,相应的原始类型都会被自动提供。原始类型的名字就是删去类型参数后的泛型类型的类名。擦除 类型变量,并替换为 限定类型(T为无限定的 类型变量,用Object替换)。

    //泛型类型
class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
}
    
    //原始类型
class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}  

因为在Pair中,T是一个无限定的类型变量,所以用Object替换。如果是Pair,擦除后,类型变量用Number类型替换。

突破泛型约束

    public class ReflectInGeneric {
    public static void main(String[] args) throws IllegalArgumentException, 
                        SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {  
        ArrayList<Integer> array=new ArrayList<Integer>();  
        // 这样调用add方法只能存储整形,因为泛型类型的实例为Integer 
        array.add(1); 
        // 通过泛型可以突破泛型类型约束
        array.getClass().getMethod("add", Object.class).invoke(array, "asd");  
        for (int i=0;i<array.size();i++) {  
            System.out.println(array.get(i));  
        }  
    }  
}

输出:

1

asd

为什么呢?我们在介绍泛型时指出向ArrayList中插入String类型的对象,编译时会报错。现在为什么又可以了呢?

  • 我们在程序中定义了一个ArrayList泛型类型,如果直接调用add方法,那么只能存储整形的数据。
  • 不过当我们利用反射调用add方法的时候,却可以存储字符串。

这说明ArrayList泛型信息在编译之后被擦除了,只保留了原始类型,类型变量(T)被替换为Object,在运行时,我们可以行其中插入任意类型的对象。

再次应证:Java中的泛型基本上都是在编译器这个层次来实现的“伪泛型”。

但是,并不推荐以这种方式操作泛型类型,因为这违背了泛型的初衷(减少强制类型转换以及确保类型安全)。当我们从集合中获取元素时,默认会将对象强制转换成泛型参数指定的类型(这里是Integer),如果放入了非法的对象这个强制转换过程就会出现异常。

泛型方法的类型推断

在调用泛型方法的时候,可以指定泛型类型,也可以不指定。

在不指定泛型类型的情况下,泛型类型为该方法中的几种参数类型的共同父类的最小级,直到Object。

在指定泛型类型的时候,该方法中的所有参数类型必须是该泛型类型或者其子类。

    public class Test { 
    public static void main(String[] args) {  
        /**不指定泛型的时候*/  
        int i=Test.add(1, 2); //这两个参数都是Integer,所以T替换为Integer类型  
        Number f=Test.add(1, 1.2);//这两个参数一个是Integer,另一个是Float,所以取同一父类的最小级,为Number  
        Object o=Test.add(1, "asd");//这两个参数一个是Integer,另一个是String,所以取同一父类的最小级,为Object
  
        /**指定泛型的时候*/  
        int a=Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类  
        int b=Test.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float  
        Number c=Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
    }  
      
    //这是一个简单的泛型方法  
    public static <T> T add(T x,T y){  
        return y;  
    } 
}

正确的运转

既然说类型变量会在编译的时候擦除掉,那为什么定义了ArrayList泛型类型,而不允许向其中插入String对象呢?不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存放别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
java是如何解决这个问题的呢?java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。以如下代码为例:

         Pair<Integer> pair=new Pair<Integer> ();
        pair.setValue(3);
        Integer integer=pair.getValue();
        System.out.println(integer);
   

擦除getValue()的返回类型后将返回Object类型,编译器自动插入Integer的强制类型转换。也就是说,编译器把这个方法调用翻译为两条字节码指令:

  • 对原始方法Pair.getValue的调用
  • 将返回的Object类型强制转换为Integer

此外,存取一个泛型域时,也要插入强制类型转换。因此,我们说Java的泛型是在编译器层次进行实现的,被称为“伪泛型”,相对于C++。