美文网首页
一文搞懂泛型

一文搞懂泛型

作者: A_si | 来源:发表于2020-10-14 23:18 被阅读0次

    泛型

    泛型是什么?

    泛型,即“参数化类型”。类型像参数一样,具有多种类型,在使用时才确定。
    比如我们需要一个装 int 类型的容器,和一个装 String 类型的容器,要分别制造几个容器吗?比如 IntArrayList 和 StringArrayList ,这样就需要无数个容器了,这种场景就需要泛型。

    List<Integer> intList = new ArrayList();
    List<String> strList = new ArrayList();
    

    参数化类型意味着可以通过执行泛型类型调用时分配一个类型,用分配的具体类型替换泛型类型。下面 ArrayList 中<E>就是泛型类型,在使用时才分配具体类型:

    public class ArrayList<E> extends AbstractList<E>
    

    通俗的说,就是我要一个篮子,可能用这个篮子装水果,那它就是一个水果篮,也可能水果吃完后用来装垃圾,那它就是垃圾篮,没必要写死这个篮子只能装水果或者垃圾,但是装水果的时候不希望篮子里有垃圾,装垃圾的时候不希望有水果。这时候就要泛型了。

    泛型的好处

    1. 提高安全性: 将运行期的错误转换到编译期. 如果我们在对一个对象所赋的值不符合其泛型的规定, 就会编译报错.
    2. 避免强转: 比如我们在使用List时, 如果我们不使用泛型, 当从List中取出元素时, 其类型会是默认的Object, 我们必须将其向下转型为String才能使用。比如:
    List l = new ArrayList();
    l.add("abc");
    String s = (String) l.get(0);
    

    而使用泛型,就可以保证存入和取出的都是String类型, 不必在进行cast了,也可以直接调用类型独有的方法,比如:

    List<String> l = new ArrayList();
    l.add("abc");
    l(0).split("b");
    

    如何使用泛型

    类型参数用作占位符,在运行时为类分配类型。根据需要,可能有一个或多个类型参数,根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出每个用例的标准类型参数:

    • E:元素
    • K:键
    • N:数字
    • T:类型
    • V:值
    • S、U、V 等:多参数情况中的第 2、3、4 个类型
    public class Test<T> {
    
        private T obj;
    
        public T getObj() {
            return obj;
        }
    
        public void setObj(T obj) {
            this.obj = obj;
        }
    }
    

    使用:

        Test<String> t = new Test<>();
            t.setObj("abc");
    

    在JDK1.7时就推出了一个新特性叫菱形泛型(The Diamond), 就是说后面的泛型可以省略直接写成<>, 反正前后一致。

    泛型中的通配符

    ?和关键字extends或者super在一起其实就是泛型的高级应用:通配符。

    固定上边界通配符 <? extends E>

    interface Fruit {
    
         double getWeight();
    }
    
    class Apple implements  Fruit{
    
        @Override
        public double getWeight() {
            return 5;
        }
    }
    class Orange implements  Fruit{
    
        @Override
        public double getWeight() {
            return 4;
        }
    }
    

    加入小明买了一个果篮,可以装苹果,也可以装橘子。

            ArrayList<Fruit> fruits = new ArrayList<>();
            Apple apple = new Apple();
            fruits.add(apple);
            Orange orange = new Orange();
            fruits.add(orange);
    

    但是小明只想让这个篮子装橘子:

    ArrayList<Fruit> fruits = new ArrayList<Orange>(); // 编译报错
    

    这样编辑器就报错了,因为泛型不支持向上转型。那怎么实现小明的需求呢:

    ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
    

    这样编辑器就不报错了,但是问题又来了,这个篮子不能装东西,会报错:

            ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
            fruits.add(orange); // 编译报错
    

    这是因为,如果可以装水果,那并不知道你装的是苹果还是橘子,如果你装了苹果,小明女朋友想吃橘子,取出的是苹果,小明女朋友就要分手了。

    编辑器也为了我们想取苹果的时候取出橘子,导致类型错误,所以不允许调用 addset,等参数是泛型的方法。

    那这个篮子什么用呢?其实,还真有用,比如小明要给水果称重,你不知道是苹果篮还是橘子篮,所以这样写:

        static double getWeight(List<? extends Fruit> list) {
    
            double weight = 0;
            for (int i = 0; i < list.size(); i++) {
                weight += list.get(i).getWeight();
            }
            return weight;
        }
    
            ArrayList<Orange> oranges = new ArrayList<>();
            oranges.add(orange);
            ArrayList<Apple> apples = new ArrayList<>();
            apples.add(apple);
            getWeight(apples);
            getWeight(oranges);
    

    这样就可以给水果称重了。

    <? extends E>就是固定上界通配符
    重点说明:我们不能对List<? extends E>使用add方法。原因是,我们不确定该List的类型, 也就不知道add方法的参数类型。
    但是也有特例,可以添加null

        ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
            fruits.add(null);
    

    固定下边界通配符 <? super E>

    小明女朋友喜欢橘子,也喜欢喝橙汁,小明送了2个橘子,一个放果篮里准备生吃,一个放厨房篮子做果汁:

    interface Fruit {
    
        double getWeight();
    }
    
    interface Juice {
    
    }
    
    class Orange implements Fruit,Juice {
    
        @Override
        public double getWeight() {
            return 4;
        }
        public void addList(List<? super Orange> list) {
            list.add(this);
        }
    }
    
    
            List<Juice> juices = new ArrayList<>();
            List<Fruit> fruits = new ArrayList<>();
    
            Orange orange1 = new Orange();
            Orange orange2 = new Orange();
            orange1.addList(juices);
            orange2.addList(fruits);
            Fruit object = fruits.get(0);
    

    小明女朋友想要装篮子的时候,取出第一个:

        public void addList(List<? super Orange> list) {
            Juice object = list.get(0); // 编译报错
            list.add(this);
        }
    

    竟然报错了,并不知道是水果篮子还是橙汁篮子。万一小明女朋友想从水果篮子拿出一个,结果拿的是橙子篮子的,那么小明又要被分手了。

    重点说明:我们不能对List<? super E>使用 get 方法。
    原因是,我们不确定该List的类型, 也就不知道 get 方法的参数类型。
    但是也有特例, Object 类型就可以:

        public void addList(List<? super Orange> list) {
            Object object = list.get(0);
            list.add(this);
        }
    

    无边界通配符 <?>

    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
     
    小明女朋友有个篮子,没有告诉小明是装什么的,于是小明用来装了橙子:

            List<?> fruits = new ArrayList<Orange>();
            fruits.add(orange); // 编译报错
            fruits.get(0); // 编译报错
    

    小明又被分手了,why?
    小明事后想到:这个篮子可能是小明女朋友装脏袜子的

    <?>就是无边界通配符,它具有上边界和下边界的限制,不能 add 也不能 get 。因为不能确定类型。

    但是也有特例,就是可以 get 到 Object,也可以存入 null。

    总结

    泛型限定符有一描述:上界不存下界不取。

    上界不存的原因:例如 List,编译器只知道容器内是 Fruit 及其子类,具体是什么类型并不知道,编译器在看到 extends 后面的 Fruit 类,只是标上一个 CAP#1 作为占位符,无论往里面插什么,编译器都不知道能不能和 CAP#1 匹配,所以就不允许插入。

    下界不取的原因:下界限定了元素的最小粒度,实际上是放松了容器元素的类型控制。例如 List, 元素是 Orange,可以存入 Orange 及其超类。但编译器并不知道哪个是 Orange 的超类,如 Juice。读取的时候,自然不知道是什么类型,只能返回 Object,这样元素信息就全部丢失了。

    kotlin 中的泛型

    和 Java 泛型一样,Kolin 中的泛型也有通配符:

    • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。
    • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super。

    泛型方法和类型推断

    小明的前女友柳岩只吃橘子这一种水果,会收各种礼物,因为礼物类型不确定,所以收礼物需要泛型方法,为什么不用Obje呢?因为泛型方法具有类型推断,不用强转,避免类型转换异常。

    interface GirlFriend<T> {
    
        T eatFruit(T i);
    
        <E> E getGift(E e);
    }
    
    
    class LiuYan<T> implements GirlFriend<T> {
    
    
        @Override
        public T eatFruit(T t) {
            return t;
        }
    
        @Override
        public <E> E getGift(E e) {
            return null;
        }
    }
    
            GirlFriend<Orange> liuyan = new LiuYan<>();
            liuyan.eatFruit(new Orange());
            Apple gift = liuyan.getGift(new Apple());
    

    送柳岩一个苹果,因为有类型推断,所以 Apple 不要强转。

    理解嵌套

    小明把前女友分类,爱吃水果的分一类:

    
    interface GirlFriend<T extends Fruit> {
    
        T eatFruit(T i);
    
        <E> E getGift(E e);
    }
    
    
    class LiuYan<T extends Fruit> implements GirlFriend<T> {
    
    
        @Override
        public T eatFruit(T t) {
            return t;
        }
    
        @Override
        public <E> E getGift(E e) {
            return null;
        }
    }
    
    
    List<? extends  GirlFriend<? extends Fruit>> list = new ArrayList<? extends  GirlFriend<? extends Fruit>>(); // 编译报错
    
    

    列表右边和左边一样,报错了,右边去掉 ?:

    List<? extends  GirlFriend<? extends Fruit>> list = new ArrayList< GirlFriend<? extends Fruit>>(); 
    

    不报错了,为什么呢?

    泛型实例化的时候要确定类型,所以 List 实例化的时候要确定具体类型, GirlFriend 代表这是个女朋友列表。那为什么 GirlFriend 后面的泛型却可以不确定呢?因为这个列表是女朋友列表,但是那种类型的女朋友列表不管。在装进去的时候才确定。

    类型擦除

            List<String> list1 = new ArrayList<>();
            List<Integer> list2 = new ArrayList<>();
            System.out.println(list1.getClass()==list2.getClass());
    

    上面输出是 true,因为虚拟机只会看到 List,泛型被擦除了。

        public class ObjectContainer<T> {
            private T contained;
            public ObjectContainer(T contained) {
                this.contained = contained;
            }
            public T  getContained() {
                return contained;
            }
        }
    

    这段代码,编译器会生成以下代码:

    public class ObjectContainer {
        private Object contained;
    public ObjectContainer(Object contained) {
        this.contained = contained;
    }
    public Object getContained() {
        return contained;
    }
    }
    

    为什么会有泛型擦除呢?是因为泛型的支持是在JDK1.5之后,那么以前的版本运行时JVM是不能识别泛型的,所以有了一个擦除机制,擦除之后,类型信息转为它的边界类型。

    擦除会带来2个问题,那就是继承中方法的重载问题,还有就是类型信息擦除之后如何获取信息的问题。

         void putList(List<Integer> list){
    
        }
         void putList(List<String> list){
    
        }
    

    因为泛型擦除,所以上面两个方法签名一致,重载失败,编译器报错。

    class Pair<T> {  
        private T value;  
        public T getValue() {  
            return value;  
        }  
        public void setValue(T value) {  
            this.value = value;  
        }  
    }
    

    用一个子类继承:

    class DateInter extends Pair<Date> {  
        @Override  
        public void setValue(Date value) {  
            super.setValue(value);  
        }  
        @Override  
        public Date getValue() {  
            return super.getValue();  
        }  
    } 
    

    那么问题来了,不是泛型擦除了吗?
    编译后的 Pair,

    class Pair {  
        private Object value;  
        public Object getValue() {  
            return value;  
        }  
        public void setValue(Object  value) {  
            this.value = value;  
        }  
    }
    

    子类重写的方法:

    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue() {  
        return super.getValue();  
    } 
    

    方法重写的话,要求参数返回值一致,为什么子类会重写成功呢?

    class DateInter extends Pair {
    
        // 我们重写的方法
        public void setValue(Date value) {
            super.setValue(value);
        }
        
        // 我们重写的方法
        public Date getValue() {
            return (Date) super.getValue();
        }
            
        // 虚拟机生成的桥接方法
        @Override
        public Object getValue() {
            return getValue();
        }
    
        // 虚拟机生成的桥接方法
        @Override
        public void setValue(Object value) {
            setValue( (Date)value);
        }
    }
    

    从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

    并且,还有一点也许会有疑问,子类中的桥方法 Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起 来“不合法”的事情,然后交给虚拟器去区别。

    这样就巧妙的解决了重载的问题。

    泛型类型获取

    List<Fruit> ps = gson.fromJson(str, new TypeToken<List<Fruit>>(){}.getType());  
    
    

    既然类型擦除了,为什么 Gson 在转 json 的时候还能获取到?

            List<Fruit> list  = new ArrayList<Fruit>();
    
    
    我们获取到的 class 类型是 List,因为 ArrayList<E> 这个类并没有类型。但是我们写个子类继承 ArrayList<E>,就能获取子类的类型:
    
         class FruitArrayList extends ArrayList<Fruit>{}
    
    
            List<Fruit> list  = new ArrayList<Fruit>();
            List<Fruit> list2  = new FruitArrayList();
    

    list2 的类型是 FruitArrayList,在看下面的 list3 :

            List<Fruit> list  = new ArrayList<Fruit>();
            List<Fruit> list2  = new FruitArrayList();
            List<Fruit> list3  = new ArrayList<Fruit>(){};
    

    list3 和 list1 的区别是后面有个中括号,代表这个就是一个 ArrayList<Fruit> 的类,就能获取这个类型。所以 Gson 在转化的时候,是一样的方法:

    new TypeToken<List<Fruit>>(){}.getType()
    

    如果改为

    new TypeToken<List<Fruit>>().getType()
    

    就获取不到类型,当然编辑器就报错了。

    相关文章

      网友评论

          本文标题:一文搞懂泛型

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