Java 深入理解泛型T

275 阅读2分钟

什么是泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

实际上,泛型的本质是参数化类型,即把类型当成参数一样传递,可以在程序运行的时候动态的修改它的类型,泛型可以定义在类、接口、方法上,分别被称为泛型类,泛型接口,泛型方法。

为什么需要泛型

泛型能够保证在编译时检查类型安全

没有使用泛型之前,我们使用List集合在向其加入的每一个元素我们都需要进行强制类型转化,以确保加入的类型时相同的。

不使用泛型

public static void main(String[] args) {
    List stringList=new ArrayList<>();
    stringList.add("tom");
    stringList.add(new A());

}
//编译正常

使用泛型

public static void main(String[] args) {
    List<String> stringList=new ArrayList<>();
    stringList.add("tom");
    stringList.add(new A());

}
//编译不通过

使用泛型可以在编译时候就告诉编译器我们需要什么类型的变量,让编译器来做类型安全检查,让程序更加安全。

泛型能够让我们程序表达更加高效

我们定义一个类

在定义类的时候我们自己也无法确定属性c的类型是什么,只有在使用的时候才能确定c的类型,或者说这个类的属性c本就是不确定的,我们使用object来表示,但在我们每次使用c属性时,我们都需要进行强制类型转化。

public class B {
    private Object c;

    public B(Object c) {
        this.c = c;
    }



    public static void main(String[] args) {

        B b=new B("tom");
        String a= (String) b.c;
    }
}

我们使用泛型来参数化类型,这使得我们可以在动态的决定c的类型,不需要我们进行强制类型转化。实际上泛型的底层也是通过强制类型转化,对类型进行动态的修改,只不过有了泛型不需要我们来进行转化。

public class B<T> {
    private T c;

    public B(T c) {
        this.c = c;
    }



    public static void main(String[] args) {

        B<String> b=new B<>("tom");
        String a=b.c;
    }
}

如何使用泛型

泛型类

只需要在类名后面打上尖括号,在尖括号内书写泛型,如果有多个泛型,用逗号隔开

public class B<T,TV> {
    private T c;
    private TV d;

    public B(T c) {
        this.c = c;
    }



    public static void main(String[] args) {

        B<String,Integer> b=new B<>("tom");
        String a=b.c;
    }
}

泛型方法

必须在修饰符之后,返回类型之前定义泛型。

public <OB> OB methods(){
    System.out.println("这是一个泛型方法");
    return null;
}

泛型类的写法和泛型类类似。

泛型擦除

泛型擦除是指泛型类在编译时期,所有的泛型消息都会被消除。Java虚拟机在编译泛型类时不会保留泛型的字节码信息,也就是说虚拟机中没有List和List 这个两个类的字节码文件,只有List这个类的字节码文件,而我们在类中使用的泛型信息,都会被替代成Obejct类型。

比如以下这个泛型类

public class B<T,TV> {
    private T c;
    private TV d;

    public B(T c) {
        this.c = c;
    }

    public <OB> OB methods(){
        System.out.println("这是一个泛型方法");
        return null;
    }
}

在编译之后

public class B {
    private Object c;
    private Object d;

    public B(Object c) {
        this.c = c;
    }

    public Object methods() {
        System.out.println("这是一个泛型方法");
        return null;
    }

}

我们也可以通过反射去证明

public static void main(String[] args) {
   List<String> stringList=new ArrayList<>();
   List<Integer> integerList=new ArrayList<>();
   System.out.println(stringList.getClass()==integerList.getClass());
   //true

}

代码创建了两个list 实例对象,它们分别有不同的泛型,但是通过getClass获取的Class对象却是相等的,说明在内存之中不存在List和List的class对象。

我们还可以通过一个例子来证明

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
   List<String> stringList=new ArrayList<>();
   stringList.getClass().getMethod("add", Object.class).invoke(stringList,1);

}
//编译正常

我们通过反射拿到Method 对象并且对集合添加一个非String类型对象,结果发现编译通过。说明在编译之后,泛型的信息就被擦除了。

泛型擦除带来的问题

自动类型转化

q:既然泛型在编译的时候会被擦除,那么在运行使用的时候为什么不需要做类型转化。

a:我们不用自己去做类型转化,虚拟机帮我们完成了这个工作。

类型擦除与多态冲突

看以下代码

public interface A<T> {
   public T getA();
}
public class B implements A<String> {


    @Override
    public String getA() {
        return null;
    }
}

类B实现了接口A,但是a经过类型擦除后方法又会改变

public interface A {
    public Object getA();
}

B没有继承到A的这个方法,那为何编译能够通过? jvm提供了一种桥接方法,帮我们解决这个问题。