什么是泛型?

145 阅读11分钟

一、什么是泛型

Java 推出泛型之前,程序员可以构建一个元素类型为 Object 的集合,该集合能够存储任意的数据类型对象,而在使用该集合的过程中,需要程序员明确知道存储每个元素的数据类型,否则很容易引发 ClassCastException 异常。

Java 泛型是 JDK5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存放的是什么类型的对象,所以在 JDK1.5 之前只能把元素类型设计为 Object,JDK1.5 之后使用泛型来解决。因为这个时候除了元素的类型不能确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection< E >,List< E >,ArrayList< E > 这个 < E > 就是类型参数,即泛型。

  • 所谓的泛型就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
  • 从 JDK1.5 以后,Java 引入了“参数化类型”的概念,允许我们在创建集合时再指定集合元素的类型,比如:List< String >,这表明该List 只能保存字符串类型的对象。
  • JDK1.5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。

为什么使用泛型?

  1. 解决元素存储的安全性问题,比如商品、药品标签等,不会弄错
  2. 解决获取数据元素时,需要类型强制转换的问题,比如不用每回拿商品、药品都要进行辨别

二、集合中使用泛型

  • 不使用泛型问题
/**
*   问题1:类型不安全,有可能在添加数据的时候混杂其他类型的数据
*   问题2:强转类型的时候可能会出现类型转换异常 ClassCastException
**/
@Test
public void test1() {
    // 定义一个集合,没有指定泛型
    List list = new ArrayList<>();
    // 想 list 中添加数据,比如要添加的是数值型的数据
    list.add(1);
    list.add(2);
    // 问题1:类型不安全,有可能在添加数据的时候混杂其他类型的数据
    list.add("张三");
    // 对list 集合中的数据进行遍历
    for (Object o : list) {
        // 问题2:强转类型的时候可能会出现类型转换异常 ClassCastException
        int num = (Integer)o;
        System.out.println(num);
    }
}
  • 使用泛型规定集合中的数据类型

@Test
public void test2() {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(11);
    list.add(22);
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        int next = iterator.next();
        System.out.println(next);
    }
}

在集合中使用泛型:

  1. 集合接口或者集合类在 JDK1.5 时都修改为带泛型的结构。
  2. 在实例化集合类时可以指明具体泛型类型
  3. 指明完之后,在集合类或者接口中凡是定义类或接口时,内部结构(如:方法、构造器、属性等)使用到类的泛型的位置都指定为实例化时的数据类型。
  4. 注意泛型类型必须是类,不能是基本数据类型,需要用到基本数据类型的时候用包装类替换
  5. 如果实例化时没有指明泛型类型,默认类型为 java.lang.Object 类型

三、自定义泛型

1. 泛型类&泛型接口

泛型类和泛型接口类似,都是在类名后或者接口名后加泛型的标识

  • 定义一个泛型类
/**
 * 该类定义为一个泛型类,在类名后面加 <T>,
 *          其中 T 就是一个标识符,可以换成 E、K、V 等
 **/
public class Order<T> {
​
    private String orderName;
    private Integer orderNum;
​
    // 在类的内部结构中可以使用泛型 T
    private T genericInfo;
    
    // 省略 有参无参构造方法,getter/setter 方法
}
  1. 如果定义了泛型类,但是实例化的时候没有指定类的泛型,则认为此泛型为 Object 类型,如果定义了带泛型的类,建议在实例化的时候要指明类的泛型。

  1. 实例化指定了类的泛型,那么在该参数就有了明确的类型约定。

  1. 创建一个子类,然后让该子类继承 Order 泛型类,那么在实例化子类的时候可以重新指定子类需要的泛型类型
/**
 * 子类继承了已经定义好的泛型类
 * 继承的时候子类可以指定自己需要的泛型类型
 **/
public class SubOrder extends Order<Integer> {
​
}

由于子类在继承带泛型的父类时,指明了泛型类型,则实例化子类对象时,不再需要指明泛型。


  • 泛型接口,比如说 Java 中的 Comparable< T > 接口就是一个泛型接口
public interface Comparable<T> {
    public int compareTo(T o);
}
  1. 泛型类可能有多个参数,此时应该将多个参数一起放在尖括号内,比如:<E1,E2,E3>

  2. 泛型类的构造器如下:public GenericClass() {}

  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

  4. 泛型不同的引用不能相互赋值。

    尽管在编译时 ArrayList< String > 和 ArrayList< Integer > 是两种类型,但是,在运行时只有一个 ArrayList 被加载到 JVM 中

  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照 Object 处理,但不等价于 Object。

  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

  7. JDK1.7,泛型的简化操作: ArrayList< Fruit > list = new ArryList<>();

  8. 泛型的指定不能使用基本数据类型,可以使用包装类替换。

  9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。

  10. 异常类不能是泛型的。

  11. 不能使用 new E[],但是可以:E[] element = (E[]) new Object[capacity];

    在 ArrayList源码中声明:Object[] elementData ,非泛型参数类型数组。

  12. 子类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现

      没有类型,擦除;具体类型

    • 子类保留父类的泛型:泛型子类

      全部保留;部分保留

    子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型。

class Person<T1,T2> {}
子类不保留父类的泛型
// 1. 没有类型,擦除;
// 等价于:class p1 extends Person<Object, Object>{}
class p1 extends Person{}
// 2. 具体类型
class p2 extends Person<Integer, Integer>{}
子类保留父类的泛型
// 1. 全部保留
class p3<T1, T2> extends Person<T1, T2>{}
// 2. 部分保留
class p4<T2> extends Person<Integer, T2>{}
​
// ---------------------- 子类扩展泛型 ---------------------------------------------------------------
子类不保留父类的泛型
// 1. 没有类型,擦除;
// 等价于:class p1 extends Person<Object, Object>{}
class p1<A,B> extends Person{}
// 2. 具体类型
class p2<A,B> extends Person<Integer, String>{}
子类保留父类的泛型
// 1. 全部保留
class p3<T1, T2,A,B> extends Person<T1, T2>{}
// 2. 部分保留
class p4<T2,A,B> extends Person<Integer, T2>{}

2. 泛型方法

  • 方法也是可以被泛型化,不管此时定义在其中的类是不是泛型类,在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
  • 泛型方法格式:[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 异常抛出 {}

在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系(泛型方法所属的类是不是泛型类都没有关系)。

// 泛型方法
// 可以声明为静态方法,因为泛型参数是在调用方法时确定的,并不是在实例化的时候确定
// 如果不加<E>会报错,因为编译器可能会认为是不是有个类叫E(类型),其实 E 是个变量
public static  <E> List<E> testGenericMethod(E[] arr) {
    ArrayList<E> list = new ArrayList<E>();
    for (E e : arr) {
        list.add(e);
    }
    return list;
}

3. 使用场景

  1. 创建一个 DAO 泛型类,在该方法中定义增删改查
public class DAO<T>{
    // 定义一个泛型的父类,其他类都会继承该类并使用该类的方法
​
    // 增
    public void add(T t) {}
    // 删
    public int del(int index, T t){return 1;}
    // 改
    public int upd(int index, T t){return 1;}
    // 查
    public T getIndex(int index){return null;}
}
  1. 创建一个与表想对应的实体类 Student
public class Student {
}
  1. 创建一个 StudentDao 继承 DAO 类,获得增删改查的方法,泛型参数为 Student
public class StudentDao extends DAO<Student>{
}
  1. 测试
public static void main(String[] args) {
    StudentDao studentDao = new StudentDao();
    Student student = studentDao.getIndex(1);
}

四、泛型的继承体现

在泛型操作的时候,

  • 假如A 类是 B 类的父类,但是 G<A>G<B> 之间不具备子父类关系,两者是并列关系的。
  • 又假如 A 类是 B 类的父类,A<G>B<G> 的父类

下图中 list1 和 list2 不具有子父类关系

五、通配符使用

1. 基本定义

  • 类型通配符:?

    比如:List<?>,Map< ?,? >

    List<?> 是 List< Object >、List< String > 等各种泛型 List 的父类

  • 读取 List<?> 的对象list 中的元素时,永远是安全的,因为不管 list 的真实类型是什么,它包含的都是 Object

  • 将任意元素加入到其中不是类型安全的

    Collection<?> c = new ArrayList< String > ();

    c.add(new Object()); // 编译时错误

    因为我们不知道 c 的元素类型,我们不能向其中添加对象。add 方法有类型参数 E 作为集合的元素类型。我们传给 add 的任何参数都必须是一个未知类型的子类,因为我们不知道那是什么类型,所以我们无法传任何东西进去。

  • 写入 list 中的元素时,不行。因为我们不知道 c 的元素类型,不能向其中添加对象

    例外的是可以添加 null

@Test
public void test1() {
    /**
     * A 类是 B类的父类,G<A> 和 G<B> 是没有关系的,但是两个类的共同的父类是:G<?>
     */
    List<Object> list1 = new ArrayList<>();
    List<String> list2 = new ArrayList<>();
    list1.add(11);
    list1.add(true);
    list2.add("张三");
    list2.add("李四");
    print(list1);
    print(list2);
}
public void print(List<?> list){
    Iterator<?> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
List<?> all = null;
all = list;
// 对于 List<?> 就不能向其内部添加数据,null类型除外
all.add(null);
// 允许读取集合中的数据,类型为 Object
Object o = all.get(0);

2. 有限制条件的通配符

  • <?> 允许所有泛型的引用调用

  • 通配符指定上限

    上限 extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即 <=

  • 通配符指定下限

    下限 super:使用时指定的类型不能小于操作的类,即 >=

    • <?extends Number> (无穷小,Number】

      只允许泛型为 Number 及 Number 子类的引用调用

    • <?super Number> 【Number,无穷大)

      只允许泛型为 Number 及 Number 父类的引用调用

    • <?extends Comparable>

      只允许泛型为实现 Comparable 接口的实现类的引用调用

六、类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,称之为--类型擦除。

@Test
public void test1() {
    // 定义两个类型的泛型集合
    ArrayList<Integer> intList = new ArrayList<>();
    ArrayList<String> strList = new ArrayList<>();
    // 获得编译之后字节码的类型
    System.out.println(intList.getClass().getSimpleName());
    System.out.println(strList.getClass().getSimpleName());
    // 如果两个字节码相等则编译之后进行了类型擦除
    System.out.println(intList.getClass() == strList.getClass());
}

// 结果
ArrayList
ArrayList
true

1. 无限制类型擦除

  1. 定义一个泛型类,包含一个泛型的变量
public class Student<T>{
    private T key;
}
  1. 通过反射会的泛型类中的信息,查看泛型 T 编译后的类型
public void test1() {
    Student<Integer> stu = new Student<>();
    // 利用反射,获取Student 类的字节码文件的 Class 对象
    Class<? extends Student> clz = stu.getClass();
    // 获取所有的成员变量
    Field[] declaredFields = clz.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        // 打印所有的成员变量的名称和类型
        System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());
    }
}

2. 有限制类型擦除

测试:只需要修改 Student类的泛型为:<?extends Number>

public class Student<T extends Number>{
    private T key;
}

3. 擦除方法中类型定义的参数

在 Student 定义一个泛型方法 show

// 定义一个泛型方法
public <T extends List> T show(T t) {
    return t;
}

通过反射查看方法的返回值信息

public void test1() {
    Student<Integer> stu = new Student<>();
    // 利用反射,获取Student 类的字节码文件的 Class 对象
    Class<? extends Student> clz = stu.getClass();
	// 获得所有的方法
    Method[] methods = clz.getMethods();
    // 遍历所有方法
    for (Method method : methods) {
        // 打印方法名和返回值类型
        System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName());
    }
}

4. 桥接方法

桥接方法就是为了保证接口实现的规范和约束,得到对接口的重写,所以会生成两个方法 一个是为了保证类型擦除的 Object,另一个是接口实现的 Integer

  1. 接口
public interface Info<T> {

    T info(T t);

}
  1. 接口实现类
public class InfoImpl implements Info<Integer> {
    @Override
    public Integer info(Integer integer) {
        return integer;
    }
}
  1. 测试
@Test
public void test1() {
    Class<InfoImpl> clz = InfoImpl.class;
    Method[] methods = clz.getMethods();
    for (Method method : methods) {
        System.out.println(method.getName() + ": " + method.getReturnType().getSimpleName());
    }
}
  1. 结果

学习参考视频: