跳至主要內容

泛型机制详解

Quest大约 6 分钟基础知识泛型

什么是泛型?

泛型( generics)是JDK5引入的一个新特性,定义接口、类或方法的时候,可以不预先指定具体的类型,而是使用时指定具体类型,实现参数化类型,实现多种数据类型复用相同的代码。
泛型的优点

  • 编译时更强的类型检查

Java编译器对泛型参数会执行检查,泛型参数指定传入的对象类型,只能传入对应对象类型或其衍生类型,泛型参数指定类型后不可变。

public class GenericExample {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Object> objectList = stringList; // 编译器报错
    }
}
  • 消除强制类型转换
//没有泛型强制转换的代码
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

//重写为使用泛型,代码不需要强制转换
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);

泛型的使用

泛型有三种使用方式:泛型类泛型接口泛型方法。类或接口声明了泛型类型,即引入了类型变量,该类型变量在类或接口中任何位置使用。方法声明了泛型类型,即类型变量作用域仅限于声明它的方法,运行泛型使用在静态和非静态方法,以及泛型类构造函数,类型变量命名约定(无实际意义)包括以下几类:

  • E- Element(被Java集合框架广泛使用)
  • K- Key 键
  • N- Number 数字
  • T- Type 类型
  • V- Value 值
  • S,U,V等 - 第2、3、4类

泛型类使用

简单泛型类

public class Box<T> {
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

多元泛型类

public class OrderedPair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
    this.key = key;
    this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

实例化泛型类

Box<String> box = new Box<String>(); 
OrderedPair<String,String> op = new OrderedPair<String,String>("key","value"); 

泛型接口使用

简单泛型接口:

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

不指定类型实现泛型接口:

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
    this.key = key;
    this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

指定类型实现泛型接口:

public class OrderedPairImpl<K, V> implements Pair<String, String> {

    private String key;
    private String value;

    public OrderedPair(String key, String value) {
    this.key = key;
    this.value = value;
    }

    public String getKey()  { return key; }
    public String getValue() { return value; }
}

泛型方法使用

普通泛型方法:

public class GenericsExample {
    
    public static <T> void example(T obj) {
        System.out.println(obj.getClass().toString());
    }

    public static void main(String[] args) {
        example(123);
        example("xyz");
    }
}

可变参数列表泛型方法:

public class GenericsExample {
    
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<T>();
        Collections.addAll(result, args);
        return result;
    }

    public static void main(String[] args) {
        List<String> ls = makeList("123");
        System.out.println(ls); // [123]
        ls = makeList("abc", "xyz");
        System.out.println(ls); // [abc, xyz]
    }
}

类型擦除

泛型( generics)是JDK5引入的一个新特性,为兼容之前版本,泛型实现用的是伪泛型,即语法支持泛型,执行编译时会进行类型擦除,会将所有的类型变量T表示替换为具体类型。类型擦除可以保证不会为参数化类型创建新类,可以减少虚拟机运行时的开销。类型擦除原则是:

  • 泛型中所有类型变量替换为Object,如果指定了类型边界,则用其类型边界替换,生成的字节码仅包含普通的类、接口和方法。
  • 擦除类型参数声明,删除<>的内容,比如T get()会变成Object get()List<String>会变成List如果有必要,编译时也会插入类型转换以保证类型安全
  • 生成桥方法以保留扩展泛型类型的多态性。

什么是桥方法

桥方法用于继承泛型类时保留拓展多态性,为编译器自动创建的一个方法。

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

执行类型擦除后,方法签名不匹配;Node.setData(T) 方法变为 Node.setData(Object)。作为一个结果,MyNode.setData(Integer) 方法不会覆盖 Node.setData(Object) 方法。为了解决这个问题,并在类型擦除后保留泛型类型的多态性,Java编译器生成了一个桥接 确保子类型按预期工作的方法。

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

泛型缺陷

泛型参数不能使用基本类型

class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
}
Pair<int, char> p = new Pair<>(8, 'a');  //编译错误
Pair<Integer, Character> p = new Pair<>(8, 'a'); //正确用法

无法创建泛型的实例

public static <E> void append(List<E> list) {
    E elem = new E();  // 编译错误
    list.add(elem);
}

//通过反射创建泛型的对象
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   
    list.add(elem);
}

List<String> ls = new ArrayList<>();
append(ls, String.class);

不允许使用泛型的静态字段

类的静态变量是由类的所有非静态对象共享的类级变量。因此,不允许使用参数类型的静态字段。

public class MobileDevice<T> {
    private static T os; 

}

不能使用 Instance of 和 getClass() 进行类型判断

public static <E> void rtti(List<E> list) {
  
    if (list instanceof ArrayList<Integer>) { // 编译错误
      
    }
  
}

不能重载泛型类型擦除后为相同原始类型的方法

public class Example {
  
    public void print(Set<String> strSet) { }
  
    public void print(Set<Integer> intSet) { }
  	//编译错误
}

什么是通配符

使用泛型时,问号?称为通配符,表示未知类型。

上边界通配符、下边界通配符

使用泛型时,可以对传入类型的实参进行上下边界限制,规定传入的具体类型是某种类型的子类(上边界)或某种类型的父类(下边界)。
上边界通配符
上限通配符使用通配符?,后跟extends关键字,最后跟其上限类,表示泛型传入必须是指定上限类的子类。

public static void process(List<? extends Fruit & Meat> list) { 
}

下边界通配符
下边界通配符使用通配符?,后跟super关键字,最后跟其下限类,表示泛型传入必须是指定下限类的父类。

public static void addNumbers(List<? super Integer> list) {
}

无界通配符

无界通配符类型使用通配符?指定,比如List<?>,称为未知类型的列表,无界通配符可以接受任何类型的数据,主要应用在不依赖具体类型参数的场景。

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

注意:

List<Object>List<?>并不相同,List<Object>可以添加任何Object及其子类,但List<?>只能添加null值,添加其他元素会报错。