泛型理解总结及类型擦除概述

1. 什么是泛型

泛型字面理解“广泛适用的类型”,其本质是指类型参数化,即可以将类型当作参数传递给一个类或者是方法。

注意: 允许在定义类、接口、方法时使用类型形参,分别称为泛型类、泛型接口、泛型方法,当使用时 指定具体类型。
     所有使用该泛型参数的地方都被统一化,保证类型一致。 如果未指定具体类型,默认是Object类型。
     集合体系中所有的类都增加了泛型,泛型主要用在集合

2. 为什么要用泛型?(泛型的优势)

  • 不需要强转
    不使用泛型:
      泛型是jdk1.5的新特性。在jdk1.5之前没有泛型时通常采用的是强转类型
    例如:有Cache实体类如下
    static class Cache{
      Object value;
      public Object getValue() {
          return value;
      }
      public void setValue(Object value) {
          this.value = value;
      }
    }
    
    将Cache进行存取值测试,得到具体类型时必须强转
    //Cache存取String类型强转
      Cache cache = new Cache();
      cache.setValue("test");
      String valueString = (String)cache.getValue();
    //Cache存取int类型强转
      cache.setValue(123);
      int valueInt = (int)cache.getValue();
    
    使用泛型:
    Cache加入泛型约束
    static class Cache<T>{
      T value;
      public  T  getValue() {
          return value;
      }
    
      public void setValue(T value) {
          this.value = value;
      }
    }
    
    Cache进行存取值时,将value 这个属性的类型参数化了,即参数化类型(再也不用手动去强转了)
    //将String参数化创建Cache
      Cache<String> cache = new Cache();
      cache.setValue("test");
      String valueString = cache.getValue();
     //将Integer参数化创建Cache
      Cache<Integer> cache1 = new Cache();
      cache1.setValue(123);
      Integer valueInt = cache1.getValue();
    
  • 编译时更强大的类型检查
    Java 编译器将强类型检查应用于通用代码,并在代码违反类型安全性时发出错误。java在修复编译时错误比修复运行时错误要容易得多。
    在这里插入图片描述
    如图,无法将String类型设置到objectTool中,因为泛型让它只接受 Integer 的类型。
    参数一旦确定好,如果类型不匹配,编译器就不通过。
 总结:
  • 与Object代替一切类型相比,泛型提供了一种扩展能力,使得数据的类别可以像参数一样由外部传递进来,其更符合面向抽象开发的软件编程宗旨。
  • 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。其是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
  • 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

3. 泛型基础

下面分别从泛型基础的四大模块:泛型类、泛型方法、泛型类派生出的子类(泛型接口)、泛型通配符来理解泛型。

补充: 这里先补充些Java规范中常见的代表类型参数的规范

  • T 代表一般的任何类。
  • E 代表 Element 的意思,或者 Exception 异常的意思。
  • K 代表 Key 的意思。
  • V 代表Value 的意思,通常与 K 一起配合使用。
  • S 代表 Subtype 的意思。
    如果一个类被 的形式定义,那么它就被称为是泛型类。

3.1 泛型类

基于类上做泛型约束,创建对象做类型定义
例:构建一个可以存储任何类型对象的工具类ObjectTool,并约束泛型类。

public class ClassGenericity {
    public static void main(String[] args) {
        //创建对象并指定元素类型为String
        ObjectTool<String> tool = new ObjectTool<>();
        tool.setObj("zzm");
        System.out.println(tool.getObj());

        //创建对象并指定元素类型为Integer
        ObjectTool<Integer> objectTool = new ObjectTool<>();
        // 如果我在这个对象里传入的是String类型的,它在编译时期就通过不了了.
        //objectTool.setObj("test");
        objectTool.setObj(10);
        System.out.println(objectTool.getObj());
    }

    /**构建可以存储任何类型对象的工具类*/
    static class ObjectTool<T> {
        private T obj;
        public T getObj() {
            return obj;
        }
        public void setObj(T obj) {
            this.obj = obj;
        }
    }
    
}

3.2 泛型方法

基于方法做泛型约束,方法入参时做类型定义
例:定义如下泛型方法show(T t),调用方法时,传入的参数是什么类型,返回值就是什么类型。

public class MethodGenericity<T> {
    public static void main(String[] args) {
        //创建对象
        ObjectTool tool = new ObjectTool();
        //调用方法,传入的参数是什么类型,返回值就是什么类型
        tool.show("hello");
        tool.show(12);
        tool.show(12.5);
    }

    static class ObjectTool {
        //定义泛型方法..
        public <T> void show(T t) {
            System.out.println(t);
        }
    }
}

相较于泛型类,泛型方法更灵活(入什么值,就可以直接去存储)

3.3 泛型类派生出的子类(泛型接口)

/**
 * 把泛型定义在接口上,方法也对应同样的泛型
 */
interface Inter<T> {
    void show(T t);
}

其实现有两种方式:
1)子类明确泛型类的类型参数变量

class InterImpl1 implements Inter<String> {
    @Override
    public void show(String s) {
        System.out.println(s);
    }
}

2)子类不明确泛型类的类型参数变量,实现类也要定义出T的类型

class InterImpl2<T> implements Inter<T> {
    @Override
    public void show(T t) {
        System.out.println(t);
    }
}

main方法分别测试两个实现:

public static void main(String[] args) {
        // 测试第一种情况,只支持String类型
        Inter<String> i = new InterImpl1();
        i.show("hello");
        // 编译错误,因为子类实现时明确了类型为String,所以不支持Integer
//        Inter<Integer> ii = new InterImpl1();
//        ii.show(1);

        // 第二种情况测试,子类不明确泛型类时,相对较灵活
        Inter<String> iii = new InterImpl2();
        iii.show("100");
        Inter<Integer> iii2 = new InterImpl2<>();
        iii2.show(123);
    }
}

3.4 通配符

  • <?> 被称作无限定的通配符。
  • <? extends T> 有上限的通配符。表示参数化类型的可能是T 或是 T的子类
  • <? super T> 有下限的通配符。表示参数化类型是此类型的超类型(父类型),直至Object

现定义三个List集合,分别约束泛型为Integer、Object、String:

private final static List<Integer> LIST_INTEGER = Arrays.asList(1, 2, 3, 4);
private final static List<Object> LIST_OBJECT = Arrays.asList(1, 2, 3, 4);
private final static List<String> LIST_STRING = Arrays.asList("a", "b", "c", "d");
1)List<Object>

Object类型,可以传入Integer、String等。
但List<Object>只是List<Object>,和List<String>、List<Integer>等没父子级关系

public void test2(List<Object> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

测试

//typeWildcard.test2(LIST_STRING); //编译报错
typeWildcard.test2(LIST_OBJECT);//输出1 2 3 4 
2)List<?> 和List <T>
  1. List<T>是确定的某一个类型,T为Object及子类或自定义实体类都可以。表示List集合中的元素都为T类型;可以进行读写操作如add、remove;因为它的类型是固定的T类型,在编译期 不需要进行任何的转型操作。
  2. List<?>表示任意未知类型,默认是允许Object及其下的子类,也就是java的所有对象。
    是只读类型的,且List<?>读取出的元素都是Object类 型的,需要主动转型。
    不能进行增加、修改操作,可以进行删除操作如remove、clear等,因为删除动作与泛型类型无关。
    例子说明:
public void test3(List<?> list) {
     //list.add(list.get(0)); // 编译错误
     for (int i = 0; i < list.size(); i++) {
         System.out.println(list.get(i));
     }
}
public <T> void test4(List<T> list) {
     for (int i = 0; i < list.size(); i++) {
     // list.add(list.get(0)); // 编译阶段ok,但是运行时抛异常,Exception in thread "main" java.lang
     // .UnsupportedOperationException
     System.out.println(list.get(i));
     }
}

test3编译错误解释:
list.get(0)获取到的是Object类型,而再次将list进行add操作时,此时通配符会捕获具体的Object类型,但编译器不叫它Object,而是起个临时的代号,比如”capture“。所以list<?>不能进行add。除了list.add(null)。
(具体深层次原因?待补充)
在这里插入图片描述
在这里插入图片描述
测试test3传入LIST_INTEGER、LIST_OBJECT、LIST_STRING:
在这里插入图片描述
test4运行错误解释:
?待补充
测试test4传入LIST_INTEGER、LIST_OBJECT、LIST_STRING:
在这里插入图片描述

3)List <? extends T> 和List <? super T>
例子说明1:
    //限制入参的类型,只能传number及其的子类
    public void test5(List<? extends Number> list) {
//        list.add(list.get(0));//编译报错
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

    //限制入参的类型,只能传number及极其的父类
    public void test6(List<? super Number> list) {
//        list.add(list.get(0));//编译报错
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

test5编译错误解释:

在这里插入图片描述
测试test5传入LIST_INTEGER、LIST_OBJECT、LIST_STRING:

typeWildcard.test5(LIST_INTEGER);//1,2,3,4

在这里插入图片描述
在这里插入图片描述
test6编译错误解释:

在这里插入图片描述
测试test6传入LIST_INTEGER、LIST_OBJECT、LIST_STRING:

typeWildcard.test6(LIST_OBJECT); //1,2,3,4

在这里插入图片描述
在这里插入图片描述

例子说明2:

1. 实体:
Animal
在这里插入图片描述
Dog
在这里插入图片描述
Cat
在这里插入图片描述
方法及测试说明:

    /**
     * 【读取】
     * 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
     */
    public void testPECSextends() {
        List<Dog> dogs = Lists.newArrayList();
        dogs.add(new Dog());
        List<? extends Animal> animals = dogs; //约束的是anmial及其animal的子类

        /**
         * animals是一个Animal的子类的List,由于Dog是Animal的子类,因此将dogs赋给animals是合法的,但是编译器会阻止将new Cat()加入animals。
         * 因为编译器只知道animals是Animal的某个子类的List,但并不知道究竟是哪个子类,为了类型安全,只好阻止向其中加入任何子类。那么可不可以加入
         * new Animal()呢?很遗憾,也不可以。事实上,不能够往一个使用了? extends的数据结构里写入任何的值。
         */
//         animals.add(new Cat()); // 编译失败
//         animals.add(new Animal()); // 编译失败
//         animals.add(new Dog()); // 编译失败,编译期只知道animals是一个Animal的子类的List

        /**
         * 由于编译器知道它总是Fruit的子类型,因此我们总可以从中读取出Animal对象:
         */
        Animal animal = animals.get(0);
        // Dog dog = animals.get(0); // 编译失败,只知道是Animal极其子类,并不确定是dog,所以编译失败
    }

    /**
     * 【写入】
     * 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
     * <p>
     * 如果既要存又要取,那么就不要使用任何通配符。
     */
    public void testPECSsuper() {
        List<Animal> animals = Lists.newArrayList();
        List<? super Dog> dogs = animals; //可存放dog及其dog的父类
        /**
         * 这里的animals是一个Animal的超类(父类,superclass)的List。同样地,出于对类型安全的考虑,我们可以加入Dog对象或者其任何子类(如WhiteDog)对象,
         * 但由于编译器并不知道List的内容究竟是Dog的哪个超类,因此不允许加入特定的任何超类型。
         */
        dogs.add(new Dog());
        dogs.add(new WhiteDog());
        // dogs.add(new Animal()); // 编译失败
        // dogs.add(new Cat()); // 编译失败
        // dogs.add(new Object()); // 编译失败

        /**
         * 而当我们读取的时候,编译器在不知道是什么类型的情况下只能返回Object对象,因为Object是任何Java类的最终祖先类。
         */
        Object obj = dogs.get(0);
        // Dog dog = dogs.get(0); // 编译失败
        // Animal animal = dogs.get(0); // 编译失败
    }

4. 类型擦除与桥接方法

4.1 什么是类型擦除与桥接方法

泛型是提供给javac编译器使用的,它用于限定集合的输入类型, 让编译器在源代码级别上,即挡住向集合中插入非法数据。
但编 译器编译完带有泛形的java程序后,生成的class文件中将不再带 有泛型信息,以此使程序运行效率不受到影响,这个过程称之为 “擦除”
由于类型被擦除了,为了维持多态性,所以编译器就自动生成了 桥接方法。

4.2 例子说明

MyNode 继承Node,并指定了String类型
通常使用泛型时这样写:

public class Node<T> {
	public T data;
	public void setData(T data) {
		this.data = data;
	}
}

public class MyNode extends Node<String> {
	@Override
	public void setData(String data) {
		this.data = data;
	}
} 

没有泛型时需做向下兼容处理:

  1. 首先进行类型擦除
public class Node {
	public Object data;
	public void setData(Object data) {
		this.data = data;
	}
}

public class MyNode extends Node {
	public void setData(String data) {
		this.data = data;
	}
	//产生桥接方法
	@Override
	public void setData(Object data) {
		//调用String类型的方法
		setData((String)data);
	}
} 
  1. 说明:
    即保留了最初的泛型的关系setData(String data),继承的关系、Override的关系、类型限制(转为String);当传String类型时,通过桥接方法调用;而传Integer类型时,报类型异常。

自己做笔记的同时也参考几篇大佬的文章(创作不易,贴下原链接):
https://blog.csdn.net/briblue/article/details/76736356
https://www.jianshu.com/p/6733de8fe844
https://cloud.tencent.com/developer/article/1353549

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小骄傲_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值