泛型-类型消除问题

上篇介绍了泛型基本用法及优点。但是,泛型也有一些缺点。

类型擦除

1、Java泛型的类型参数的实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型
2、无法直接使用基本值类型作为泛型类型参数。
3、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());
        System.out.println(arrayInteger.getClass());
        System.out.println(arrayString.getClass() == arrayInteger.getClass());
    }
}

输出:
class java.util.ArrayList
class java.util.ArrayList
true

arrayString 类型参数是String,arrayInteger 类型参数是Integer,而 arrayString 对象和 arrayInteger 对象的类信息均为 java.util.ArrayList,结果为 true。这是为什么呢??

因为在编译期间,所有的泛型信息都会被擦除,在泛型代码内部,无法获取任何有关泛型参数类型的任何信息!!ArrayList<Integer>ArrayList<String>类型,在编译后都会变成原始的ArrayList

Java的泛型就是使用擦除来实现的Java中的泛型基本上都是在编译器这个层次来实现的

泛型其实只是在编译器中实现的,而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。擦除是将泛型类型以其父类代替,如 String 变成了Object 等。泛型其实是编译器规范用户使用的一种手段。

问题一:既然泛型被擦除了,为什么调用get()不需要进行强制转换?

这就需要看源码了,以ArrayList.get()方法为例:
public E get(int index) {
    RangeCheck(index);
    return (E) elementData[index];
}
可以看到,在 return 之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date) elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。

问题二:泛型为什么不能为基本数据类型?

Java中类的泛型会在编译期间经过泛型擦除的过程,当泛型类型被擦除后,我们显示声明的泛型类型就相当于失效了,同时退化成为默认类型也就是Object类型,然后 Object 类型是一个类,如 int、double 等这种基本类型并不是引用类型其父类不是Object(它们本身也没有父类),因此泛型不能为基本数据类型。

类型擦除示例二:

class Dog{
    public void dog(){
        System.out.println("dog");
    }
}

class DogGeneric<T>{
    private T t;
    public DogGeneric(T t){
        this.t = t;
    }
    public void callDog(){
        //t.dog();//这里不能编译
    }
}

public class GenericType {
    public static void main(String[] args) {
        Dog dog = new Dog();
        DogGeneric<Dog> dg = new DogGeneric<Dog>(dog);
        dg.callDog();
    }
}

代码第13行编译报错,因为 DogGeneric<Dog> 擦除了 Dog 参数类型,在 DogGeneric 中编成了Object,并没有和类 Dog 绑定,所以不能调用 Dog 类方法dog()

其实也可以理解,声明类 DogGeneric 的时候,并不知道后面会传入什么类型参数,就不可能知道传入类型对应的方法。要想提前知道传入类型类方法,就必须事先声明。

所以,可以给定泛型的边界。修改为 class DogGeneric<T extends Dog>这个边界声明了 T 必须具有类型 Dog 或者 Dog 的子类型

方法一:边界
class Dog{ ... }

class DogGeneric<T extends Dog>{
    private T t;
    public DogGeneric(T t){
        this.t = t;
    }
    public void callDog(){
        t.dog();
    }
}

public class GenericType { ... }

输出:
dog
第二种方法:借助 Class<T> 保留数据类型
class Dog{ ... }

class TestHello<T>{
    private T t;
    public Class<T> clazz;

    public TestHello(T t, Class<T> clazz){
        this.t = t;
        this.clazz = clazz;
    }
    public void callHello(){
        //t.hello();//这里不能编译
        String name = clazz.getName();
        System.out.println(name);
        
        Object obj = null;
        try {
            obj = Class.forName(name).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Hello h = (Hello) obj;
        h.hello();
    }
}

public class Abrasion {
    public static void main(String[] args) {
        Hello hello = new Hello();
        TestHello<Hello> testHello = new TestHello<Hello>(hello, Hello.class);
        testHello.callHello();
    }
}

输出:
泛型.Hello
hello

引申:在不能创建泛型数组的情况下,一般的解决方案是使用 ArrayList 代替泛型数组。因为 ArrayList 内部就是使用数组,因此使用 ArrayList 能够获取数组的行为,比由泛型提供的编译器的类型安全。
但是,某种特定的场合,你仍然要使用泛型数组,推荐的方式是使用 Class<T>+Array.newInstance来实现。

public class TestInstance<T> {
    private Class<T> t;
    public TestInstance(Class<T> t){
        this.t = t;
    }

    //抑制警告
    @SuppressWarnings("unchecked")
    T[] create(int size){
        return (T[]) Array.newInstance(t, 10);
    }

    public static void main(String[] args) {
        TestInstance<Integer> ti = new TestInstance<Integer>(Integer.class);
        Integer[] as = ti.create(10);
        System.out.println(as.length);
    }
}

边界

因为泛型信息在编译之后被擦除了,最终类型变量(T)被替换为 Object ,所以,无界泛型参数只能调用Object的方法。但是,Java提供了给定边界泛型,将这个参数限制为某个类型的子集,就可以使用这些类型子集来调用方法。

指定单个边界

interface Dog{
    void dog();
}

public class DogBorder<T extends Dog> {
    T t;
    public DogBorder(T t){
        this.t = t;
    }
    public void testBorder(){
        t.dog();
    }

    public static void main(String[] args) {
        DogBorder db = new DogBorder(new Dog() {
            @Override
            public void dog() {
                System.out.println("DogBorder dog");
            }
        });

        db.testBorder();
    }
}

输出:
DogBorder dog

代码11行,类型 T 可以使用 Dog 类方法。

指定多个边界

interface Dog{
    void shout();
}

interface Cat{
    void run();
}

interface Pig{
    void eat();
}

public class TestBorder<T extends Dog & Cat & Pig> {
    T t;
    public TestBorder(T t){
        this.t = t;
    }
    public void test(){
        t.shout();
        t.run();
        t.eat();
    }
}

注意:extends 后面跟的第一个边界可以为类或接口,之后的均为接口

通配符和泛型上界和下界

为什么要使用通配符呢?

通配符修饰相当于声明了一种变量,它可以作为参数在方法中传递。这么做带来的好处就是我们可以将应用于包含某些数据类型的列表的方法也应用到包含其子类型的列表中。通俗讲,通配符可以解决泛型中子父类之间无关的问题。用通配符帮助限定边界就能模拟面向对象中继承关系。代码参考 “边界”。

class Aniaml{}
class Cat extends Aniaml{}

public class TongPei {
    public static void arrayTest(Aniaml[] l){
    }

    public static void test1(List<Aniaml> l){
    }
    public static void test2(List<? extends Aniaml> l){
    }

    public static void main(String[] argc){
        Cat[] cat = new Cat[10];
        arrayTest(cat);

        LinkedList<Cat> al = new LinkedList<Cat>();
        //test1(al);
        test2(al);
    }
}

对象数组可以接收其子类对象数组,而泛型集合就不行,通过<? extends Aniaml>声明,才可以接收 Aniaml 及其子类的集合。

常见的通配符:

通配符含义
T (type)表示具体的一个java类型
表示不确定的 java 类型
K代表java键值中的Key
V代表java键值中的Value
E (element)代表Element元素

上述的T,E,K,V,?,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个字母,只不过代码可读性上可能会弱一些。

注:通配符 ? 应用于具体类型不确定的时候:当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。ArrayList<?>相当于ArrayList<>,区别在于ArrayList<?>告诉编译器我不是没注意类型安全,只是我不确定或者不关心实际要操作的类型。

在介绍上界通配符和下界通配符之前,先要知道通配符泛型用在什么地方?

  1. 用在函数声明
    void foo(List<? extends E> list)
  2. 用在类、接口

上界通配符 < ? extends E>

可以接收E类型或者E的子类型对象。

class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}

public class UppBound {
    public static void testUppBound(List<? extends Animal> list) {
        // add 添加
        //list.add(new Animal()); //编译错误
        //list.add(new Dog());    //编译错误
        //list.add(new Object()); //编译错误
        list.add(null);

        // get 获取
        Animal a = list.get(0);
    }

    public static void main(String[] argc){

        List<Animal> list1 = new ArrayList<Animal>();
        List<Dog> list2 = new ArrayList<Dog>();
        List<Cat> list3 = new ArrayList<Cat>();

        testUppBound(list1);
        testUppBound(list2);
        testUppBound(list3);
    }
}

定义了类 Dog、Cat 继承类 Animal,代码23、24、25行调用方法testUppBound正确,因为方法声明 List<? extends Animal>,可以接收 Animal 类型或者 Animal 的子类型对象。

但是,方法 testUppBound 方法体中使用 list 时,却不能 add 任何类型,甚至 Object 都不行,除了null,因为 null 代表任何类型。list.get(0);获取元素没有问题。

解释:
1、List<? extends Animal> list 你可以理解为这个 list 可能是List<Animal>也可能是List<Dog>List<Cat>,只要是 Animal 或其子类就行。这时候你要存入一个 Dog,有可能是往 List<Cat> 里存,这样是不安全的,因为编译器判断不了你这个List是Dog还是Cat(因为都可以),所以就只能什么都不能存(null除外)。
2、从list获取的元素要么是Animal对象或者Dog或者Cat对象,反正都是Animal或其子类对象,最终都可以用Animal引用接收

总结一句话:
指定上限情况下,只能从容器中取对象,不能添加对象。因为没办法确定你添加的是哪种类型的对象,但是可以确定的是你取出来的对象是上限类及其子类。

下界通配符 < ? super E>

可以接收E类型或者E的父类型对象。

class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}
class SonDog extends Dog{}

public class LowBound {
    public static void testLowBound(List<? super Dog> list) {
        // add 添加
        list.add(new Dog());
        list.add(new SonDog());
        //list.add(new Animal()); //编译错误
        //list.add(new Object()); //编译错误
        list.add(null);

        // get 获取
        //Dog d = list.get(0);    //因为编译器不能确定列表中的是 Dog的哪个子类,所以只能返回Object。
        Object d = list.get(0);
    }

    public static void main(String[] argc){

        List<Animal> list1 = new ArrayList<Animal>();
        List<Dog> list2 = new ArrayList<Dog>();
        List<Cat> list3 = new ArrayList<Cat>();
        List<SonDog> list4 = new ArrayList<SonDog>();

        testLowBound(list1);
        testLowBound(list2);
        //testLowBound(list3);  //编译错误
        //testLowBound(list4);  //编译错误
    }
}

定义了类 SonDog 继承类 Dog,代码27、28行调用方法testUppBound正确,因为方法声明 List<? super Dog>,可以接收 Dog 类型或者 Dog 的父类型对象。
代码9、10行,可以add 添加Dog及其子类对象,add添加其父类Animal编译错误。而且,get返回值只能是Object类型

解释:
1、List<? super Dog> list,你可以理解为这个list可能是List<Dog>List<Animal>List<Object>,所以编译器允许你存Dog或其子类,因为都能向上转型成功,但是你要存Animal就不一定成功了,因为这个List可能是List<Dog>。
2、因为列表类型可能是Dog、Animal或者Object,编译器不能确定类型,所以list.get(0);返回值是Object类型。

总结一句话:
指定下限情况下,只能往容器里添加E及其子类对象。因为没办法确定你要获取的对象是哪种类型的,但是可以确定你放入的对象必定是下界的类或者超类。

无界通配符 < ? >

public class UppBound {
    public static void show(List<?> l){
        for (Object object : l) {
            System.out.println(object);
        }
    }
    public static void upBound(List<? extends Number> l){
        for (Object object : l) {
            System.out.println(object);
        }
    }
    public static void downBound(List<? super Number> l){
        for (Object object : l) {
            System.out.println(object);
        }
    }

    public static void main(String[] args) {
        //因为show方法是用List<?>通配符接收的,所以可以是任意类型!
        List<String> list1 = new ArrayList<>();
        List<Double> list2 = new ArrayList<>();
        List<Number> list3 = new ArrayList<>();
        List<Object> list4 = new ArrayList<>();

        show(list1);
        show(list2);
        show(list3);
        show(list4);

        //使用up方法的话接收类型为Number或者其子类
        //upBound(list1);  //错误,因为up方法接收类型为Number或者其子类,l1(String)不符合!
        upBound(list2);
        upBound(list3);

        //使用down方法的话接收类型为Number或者其父类
        //downBound(list2); //error
        downBound(list3);
        downBound(list4);
    }
}

PECS原则

如果要从集合中读取类型T的数据,并且不能写入,  可以使用 ? extends 通配符;
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;
如果既要存又要取,那么就不要使用任何通配符。

泛型应用

项目中,通常会有很多张表,每张表对应一个DAO,但一般常规的增删查改都一样,可不可以抽象出一个DAO,其他DAO继承该抽象DAO,就有对应的方法了。
要实现该功能,需要使用泛型,因为抽象DAO中,不可能知道哪个DAO继承自己,所以不可能知道其具体类型,只有创建子类DAO时候才能确认类型。而泛型就是在创建的时候才指定其具体的类型。

抽象类

public abstract class BaseDao<T> {
    //模拟hibernate....
    //private Session session;
    private Class clazz;

    //哪个子类调的这个方法,得到的class就是子类处理的类型(非常重要)
    public BaseDao(){
        Class clazz = this.getClass();  //拿到的是子类
        System.out.println(clazz);
        ParameterizedType pt = (ParameterizedType) clazz.getGenericSuperclass();  //BaseDao<Category>
        clazz = (Class) pt.getActualTypeArguments()[0];
        System.out.println(clazz);
    }

    public void add(T t){
        System.out.println("add");
        //session.save(t);
    }

    public T find(String id){
        //return (T) session.get(clazz, id);
        return (T) id;
    }

    public void update(T t){
        //session.update(t);
    }

    public void delete(String id){
        //T t = (T) session.get(clazz, id);
        //session.delete(t);
    }
}

CategoryDao类

class Category{}
public class CategoryDao extends BaseDao<Category> {
    public static void main(String[] args) {
        CategoryDao cd = new CategoryDao();

        cd.add(new Category());
    }
}

输出:
class 泛型.CategoryDao
class 泛型.Category
add

CategoryDao继承抽象类BaseDao,该实现类就有对应的增删改查的方法了。

参考资料:
https://blog.csdn.net/jeffleo/article/details/52250948
https://blog.csdn.net/sunxianghuang/article/details/51982979
https://segmentfault.com/a/1190000014120746

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值