美文网首页
Java泛型

Java泛型

作者: 0x70e8 | 来源:发表于2018-07-11 00:14 被阅读0次

    [TOC]

    什么是泛型,为什么使用

    泛型是将类型作为参数,像方法一样在使用时能够传入对象的类型,这样即约束了对象的类型,也无需使用Object作为通用类型带来的强制转换和很差的可读性。

    泛型的使用一方面能让人一眼看出来对象所属的类型,可读性大大提高,另外对应编译器,参数化类型的静态约束,使得对集合这些容器的元素类型检查在编译期就能判断,增加了代码的安全性。

    如在泛型出现之前的ArrayList类,底层的存储元素的数组是一个Object数组,这意味着所有的java对象都可以放入此容器中,在取出时需要类型强制转换,如果在add的时候添加了不符合预期的类型,可能就会抛ClassCastException。且在运行时才会抛出。

    如何使用泛型

    定义简单泛型类

    设计一个泛型栈,代码不完整,且非并发安全类。

    public class Stack<E> {
        private E[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
        @SuppressWarnings("unchecked")
        public Stack() {
            elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
        }
    
        public void push(E e) {
            ensureCapacity();
            elements[size++] = e;
        }
    
        public E pop() {
            if (size == 0)
                throw new EmptyStackException();
            E result = elements[--size];
            elements[size] = null; // Eliminate obsolete reference
            return result;
        }
         
        public void ensureCapacity(){}
     
    }
    

    定义泛型方法

    泛型方法可以定义在泛型类中,也可以定义在普通类中
    上面泛型类已经有泛型方法

    // 普通类定义泛型方法
    class Example{
        // <T>必须
        public static <T> T get() {
            return null;
        }
    }
    class TestG<T> {
            // error ,静态方法不能使用类的泛型参数
        public static T get() {
            return null;
        }
    }
    

    类型变量的限定

    限定类型的边界,使用extends和super关键字声明接受的类型的范围。

    
    public <T extends Number> T find(T a){
        
    }
    
    

    上面使用extends表明类型T是Number类的子类。如果有多个限定,使用T extends A & B,AB为具体类或者接口;

    泛型运行时擦除

    使用泛型的类在编译后,泛型是被擦除的,有限定的泛型使用限定类替换,没有限定的使用Object类替换。所以泛型的主要作用就是静态类型的检查。

    需要注意的是,在编译器擦除类型后,可能会使得重载方法产生冲突,因为T被擦除成Object,会生成Object返回值或者参数的方法,这可能与原本类中的实现方法重载了,如:

    PS: 重载是方法名相同,入参类型不同,和返回值没有关系

    class People<T> {
        private T t;
    
        public T get() {
            return null;
        }
    
        public void setT(T t) {
            this.t = t;
        }
    }
    
    class Man extends People<Integer> {
    
        @Override
        public Integer get() {
            return null;
        }
    
        @Override
        public void setT(Integer t) {
    
        }
    
    }
    

    擦除之后(反编译):

    class People
    {
    
        People()
        {
        }
    
        public Object get()
        {
            return null;
        }
    
        public void setT(Object obj)
        {
            t = obj;
        }
    
        private Object t;
    }
    
    class Man extends People
    {
    
        Man()
        {
        }
    
        public Integer get()
        {
            return null;
        }
    
        public void setT(Integer integer)
        {
        }
    
        public volatile void setT(Object obj)
        {
            setT((Integer)obj);
        }
    
        public volatile Object get()
        {
            return get();
        }
    }
    
    

    方法签名由方法名和参数组成,返回值类型不参与,所以get方法重载冲突了,set方法在重载时有类型匹配,虽然不冲突。虚拟机使用了桥方法来解决重载冲突问题。

    类型擦除有以下几个事实:

    • 虚拟机中没有泛型,只有普通类和方法;
    • 所有类型参数都用它们的限定类型替换,没有就是Object;
    • 桥方法被用来解决重载冲突;
    • 为保证类型安全,必要时会插入强制类型转换。

    约束与局限

    大部分都是因为类型擦除:

    • 不能使用基本类型作为类型参数
    • 运行时类型检查只适用于原始类型

    instanceof判断对象类型,泛型类型在运行时是擦除的,所以不能使用这种方式来检查类型

    • 不能创建参数化类型的数组
      如:
    People<Integer>[] p = new People<Integer>(); 
    

    如果需要这种限定元素类型的集合,可以使用ArrayList<People<Integer>>

    • 方法的泛型可变参数警告
    • 不能实例化类型变量T t = new T();
    • 不能构造泛型数组 T[] ta = new T[1];
    • 泛型类的静态上下文中类型变量无效(不能在静态域和方法中使用类型参数)
    • 不能抛出或捕获泛型类的实例
    • 可以消除对受检查异常的检查

    泛型类型的继承规则

    无论S和T是什么关系,Pair<S>Pair<T>之间没有关系。

    通配符类型

    ?代表泛型的通配符,Pair<? extends Employee>,表示任何泛型Pair类型,它的参数类型是Employee的子类。指明了类型的上边界。
    Pair<? super Manager>表面任何泛型Pair类型,它的参数类型是Manager的超类,这指明了类型的下边界;

    <T>与<?>的区别

    • <T>:泛型标识符,用于泛型定义(类、接口、方法等)时,可以想象成形参。 在使用时T是一个类型参数,需要传入实际的类型变量才能使用。T相当于一个方法的参数声明,也就是形参,需要传入实际值的。
    • <?>:通配符,用于泛型实例化时,可以想象成实参,这个不是一个类型变量,就是一个实在的类型,使用时不需要传入类型。可以看成就是一个固定的类型,只是这个类型不确定,并不是需要在使用的时候再传入类型参数。

    <T>与<?>的区别很重要,是理解通配符限制的基础。

    向上、向下转换类型

    向上转型:子类型通过类型转换成为父类型(隐式的)。
    向下转型:父类型通过类型转换成为子类型(显式的,有风险需谨慎)。

    上边界限定通配符

    利用 <? extends Employee> 形式的通配符,可以实现泛型的向上转型:
    例如:

    • 定义了一个泛型类,里面含有两个泛型方法:
        class Gen<T> {
            public void add(T t) {
    
            }
    
            public T get() {
                return null;
    
            }
        }
    
    • 传类型参数的时候,传入一个通配类型的的类型参数
        @Test
        public void test3() {
            Gen<? extends Super> gen = new Gen<>();
        }
    

    则此时对应的gen对象内的实例方法就是:

        public void add(? extends Super t) {
    
            }
    
            public ? extends Super get() {
                return null;
    
            }
    

    看一下add方法,add(? extends Super t),意思是这个方法接受一个通配类型的参数,这个参数的类型是什么不知道,只知道是Super类的子类。 编译器不知道具体的类型,所以它拒绝传递任何类型的参数,所以add()都会编译时报错,只能add(null);

    再看get方法,? extends Super get(),意思是这个方法返回的对象类型不确定,只知道是Super的子类,这样就可以使用Super obj = get();来接受返回值,因为不管是哪个子类,都可以安全地转换成父类。

    所以上边界通配符适合用来接收,而不能传入数据。

    下边界限定通配符

    通配符的另一个方向是 “超类型的通配符“: ? super T,T 是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。

            Gen<? super Sub> gen2 = new Gen<>();
            gen2.add(new Sub());
            Object o = gen2.get();//只能Object接收,因为Object是所有对象的父类
    

    下边界类型通配符可以确定子类型,回顾向上转型与向下的概念:

    在获取数据时 [ ? super Sub get(){ return null;} ]:只能知道返回值的类型的子类是Sub,不知道对象的具体类型,因为向下转型有风险,所以不能使用Sub来接收结果,需要接收只能使用安全的父类:Object。

    在写入数据时 [ void add(? super Sub t){ } ],只知道参数类型的子类是Sub,所以写入Sub类或者Sub类的子类(它们能安全转为Sub类)是安全的。

    所以下边界通配符适合用来写入数据,限制读取。

    无边界通配符

    无边界类型通配符(<?>) 等同于 上边界通配符<? extends Object>
    因为可以确定父类型是Object,所以可以以Object去获取数据(向上转型)。但是不能写入数据。

    总结

    通配符 说明
    上边界类型通配符(<? extends 父类型>) 因为可以确定父类型,所以可以以父类型去获取数据(向上转型)。但是不能写入数据。
    下边界类型通配符(<? super 子类型>) 因为可以确定最小类型,所以可以以最小类型去写入数据(向上转型)。只能以Object类去获取数据,但意义不大。
    无边界类型通配符(<?>) 等同于 上边界通配符<? extends Object>,所以可以以Object类去获取数据,但意义不大。

    关于通配符,类型安全转换是编译器判断的依据。另外通配符表示是符合要求的任意类型,而非要传入类型,记忆时可以记成 “不确定的某个类”。


    参考资料

    [1] Java 泛型总结(三):通配符的使用

    [2] Java泛型06 : 通配符:上边界、下边界与无界

    [3] Java核心技术卷 I

    相关文章

      网友评论

          本文标题:Java泛型

          本文链接:https://www.haomeiwen.com/subject/nbrcpftx.html