泛型学不好,同事说我代码抽象太拉了!

151 阅读5分钟

1.概述

1.1 背景

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

1.2 泛型的概念

        Java泛型(Generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。

    泛型的本质就是参数化类型,也就是所操作的数据类型被指定为一个参数

1.3 泛型的好处

  • 类型安全
  • 消除了强制类型的转换

2.泛型类

class 类名称<泛型标识,泛型标识,......> {
  private 泛型标识 变量名;
  ......
}

常用的泛型标识:T、E、K、V

注意事项:

  • 泛型类,如果没有指定具体的数据类型,此时类型是Object
  • 泛型的类型参数只能是类类型(引用数据类型),不能是基本数据类型
  • 泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型(方法重载时如果只有泛型类型不同,此时不允许编译通过)

3.泛型类派生子类

  • 子类也是泛型类,子类和父类的泛型类型要保持一致

    class ChildGeneric<T> extends Generic<T>

  • 子类不是泛型类,父类要明确泛型的数据类型

    class ChildGeneric extends Generic<String>

4.泛型接口

interface 接口名称<泛型标识,泛型标识,......> {
  泛型标识 方法名();
  ......
}

常用的泛型标识:T、E、K、V

泛型接口使用(和泛型类派生子类用法一致):

  • 实现类不是泛型类,接口要明确数据类型
  • 实现类也是泛型类,实现类和接口的泛型类要保持一致

5.泛型方法

修饰符 <T, E, ......> 返回值类型 方法名(形参列表) {
  方法体......
}

常用的泛型标识:T、E、K、V

注意事项:

  • public与返回值中间非常重要,可以理解为声明此方法为泛型方法
  • 只有声明了的方法才是泛型方法,泛型类中使用了泛型的成员方法并不是泛型方法
  • 如果泛型方法的类型标识符和类的泛型标识符重复了,使用的一定是泛型方法的类型
  • 参数可以使用泛型可变参:public void print(T... t){}

6.类型通配符

  • 泛型通配符一般是使用"?"代替具体的类型实参

  • 泛型通配符是类型实参,而不是类型形参

  • List<?> 可以接收任意类型的集合,但是不能向其中添加数据(不能add)

7.泛型通配符上限

类/接口<? extends 实参类型>

要求该泛型的类型,只能是实参类型,或为实参类型的子类类型

注意事项:

  • 接受的话数据类型是实参类型(实参类型 item = list.get())

  • List<? extend Animal>可以接收Animal和Animal子类类型的集合。也不能添加,因为类型不确定,如果接受的是Animal的子类,添加的是Animal,这样显然是不行的,所以也就不让添加了

  • List<Integer> list = new ArrayList<>();
    
    List<? extends Cat> list = new ArrayList<>() {{
    
        add(new MiniCat());  
    
    }};
    
    左边有类型,右边无类型。经过类型推断,右边的集合数据类型和左边的保持一致(由反编译字节码得出结论)
    

8.泛型通配符下限

类/接口 <? super 实参类型>

要求改泛型的类型,只能是实参类型,或为实参类型的父类类型

注意事项:

  • 接收的话数据类型是Object类型(Object obj = list.get())
  • 可以填充类型(可以add),类型为实参类型或实参子类类型。

9.泛型擦除

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

  • T 会被擦除为 Object
  • T extends Number 会被擦除为 Number
  • 调用者知道传递的是什么类型所以调用后会做一个强转
// 源码
Map<String, String> map = new HashMap<>();
map.put("hello", "world");
map.put("你好", "世界");
System.out.println(map.get("hello"));

// 反编译class文件
Map map = new HashMap();
map.put("hello", "world");
map.put("你好", "世界");
System.out.println((String)map.get("hello"));

9.1 泛型擦除引发的问题

下面我们看一个稍微有点复杂的例子,首先声明一个接口,然后创建一个实现该接口的类:

public interface Fruit<T> {

  T get(T param);
  
}

public class Apple implements Fruit<Integer> {

  @Override
  public Integer get(Integer param) {
    return param;
  }
  
}

按照之前我们的理解,在进行类型擦除后,应该是这样的:

public interface Fruit {

  Object get(Object param);
  
}

public class Apple implements Fruit {

  @Override
  public Integer get(Integer param) {
    return param;
  }

}

但是,如果真是这样的话那么代码是无法运行的,因为虽然Apple类中也有一个get方法,但是与接口中的方法参数不一致,也就是说没有覆盖接口中的方法。针对这种情况,编译器会通过添加一个桥接方法来满足语法上的要求,同时保证了基于泛型的多态能够有效。我们反编译上面代码生成的字节码文件:

可以看到,编译后的代码中生成了两个get方法。参数为Objectget方法负责实现Fruit接口中的同名方法,然后在实现类中又额外添加了一个参数为Integerget方法,这个方法也就是理论上应该生成的带参数类型的方法。最终用接口方法调用额外添加的方法,通过这种方式构建了接口和实现类的关系,类似于起到了桥接的作用,因此也被称为桥接方法,最终,通过这种机制保证了泛型情况下的Java多态性。