泛型

作者: JBryan | 来源:发表于2020-03-05 10:58 被阅读0次
1.什么是泛型

泛型就是定义一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>:

ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型作检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

泛型就是编写模板代码来适应任意类型;
泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
注意泛型的继承关系:可以把ArrayList<Integer>向上转型为List<Integer>(T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>(T不能变成父类)。

2.使用泛型

除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

public interface Comparable<T> {
    /**
     * 返回-1: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回1: 当前实例比参数o大
     */
    int compareTo(T o);
}
package com.ljessie.designpattern.java;

public class Person implements Comparable<Person>{

    String name;

    public Person(String name){
        this.name = name;
    }
    @Override
    public int compareTo(Person o) {
        return this.name.compareTo(o.name);
    }

    @Override
    public String toString() {
        return name;
    }
}
package com.ljessie.designpattern.java;

import java.util.Arrays;
import java.util.StringJoiner;

public class Test {
    public static void main(String[] args) {
        Person[] person = new Person[] {
            new Person("Jessie"),
            new Person("Bob")
        };
        Arrays.sort(person);
        System.out.println(Arrays.toString(person));
    }

}

使用泛型时,把泛型参数<T>替换为需要的class类型,例如:ArrayList<String>,ArrayList<Number>等;
可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();;
不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;
可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

3.编写泛型

编写泛型类比普通类要复杂。通常来说,泛型类一般用在集合类中,例如ArrayList<T>,我们很少需要编写泛型类。
编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。例如:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }
    // 对静态方法使用<T>:
    public static Pair<T> create(T first, T last) {
        return new Pair<T>(first, last);
    }
}

上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。
泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>:

public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public K getLast() { ... }
}

使用的时候,需要指出两种类型:

Pair<String, Integer> p = new Pair<>("test", 123);

Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。
编写泛型时,需要定义泛型类型<T>;
静态方法不能引用泛型类型<T>,必须定义其他类型(例如<K>)来实现静态泛型方法;
泛型可以同时定义多种类型,例如Map<K, V>。

4.擦拭法

泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
例如,我们编写了一个泛型类Pair<T>,这是编译器看到的代码:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}

因此,Java使用擦拭法实现泛型,导致了:
编译器把类型<T>视为Object;
编译器根据<T>实现安全的强制转型。
Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。
了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:
局限一:<T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

局限二:无法取得带泛型的Class
局限三:无法判断带泛型的Class

Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}

局限四:不能实例化T类型:

public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T();
        last = new T();
    }
}

要实例化T类型,我们必须借助额外的Class<T>参数:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
}

上述代码借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>。例如:

Pair<String> pair = new Pair<>(String.class);

有些时候,一个看似正确定义的方法会无法通过编译。例如:

public class Pair<T> {
    public boolean equals(T t) {
        return this == t;
    }
}

这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。
换个方法名,避开与Object.equals(Object)的冲突就可以成功编译:

public class Pair<T> {
    public boolean same(T t) {
        return this == t;
    }
}

子类可以获取父类的泛型类型<T>。

5.extends通配符

使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。
如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:

<? extends Number> getFirst();

即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:

Number x = p.getFirst();

然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:

Integer x = p.getFirst();

这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。
方法参数签名setFirst(? extends Number)无法传递任何Number类型给setFirst(? extends Number)。
这里唯一的例外是可以给方法参数传入null:

p.setFirst(null); // ok, 但是后面会抛出NullPointerException
p.getFirst().intValue(); // NullPointerException

extends通配符的作用
extends通配符放在方法参数位置,限制此方法内部,只可读取传入参数,不可修改参数里面的内容。
如果我们考察Java标准库的java.util.List<T>接口,它实现的是一个类似“可变数组”的列表,主要功能包括:

public interface List<T> {
    int size(); // 获取个数
    T get(int index); // 根据索引获取指定元素
    void add(T t); // 添加一个新元素
    void remove(T t); // 删除一个已有元素
}

现在,让我们定义一个方法来处理列表的每个元素:

int sumOfList(List<? extends Integer> list) {
    //list.add(new Integer(5)); 此行代码放开注释,会编译错误。若方法参数是List<Integer>,则不会。
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}

为什么我们定义的方法参数类型是List<? extends Integer>而不是List<Integer>?
从方法内部代码看,传入List<? extends Integer>或者List<Integer>是完全一样的,但是,注意到List<? extends Integer>的限制:
允许调用get()方法获取Integer的引用;
不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。
因此,方法参数类型List<? extends Integer>表明了该方法内部只会读取List的元素,不会修改List的元素(因为无法调用add(? extends Integer)、remove(? extends Integer)这些方法。换句话说,这是一个对参数List<? extends Integer>进行只读的方法(恶意调用set(null)除外)。
使用extends限定T类型
在定义泛型类型Pair<T>的时候,也可以使用extends通配符来限定T的类型:

public class Pair<T extends Number> { ... }

现在,我们只能定义:

Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;

因为Number、Integer和Double都符合<T extends Number>。
非Number类型将无法通过编译:

Pair<String> p1 = null; // compile error!
Pair<Object> p2 = null; // compile error!

因为String、Object都不符合<T extends Number>,因为它们不是Number类型或Number的子类。

6.Super通配符

和extends通配符相反,这次,我们希望接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。
我们使用super通配符来改写这个方法:

void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
因此,使用<? super Integer>通配符表示:
允许调用set(? super Integer)方法传入Integer的引用;
不允许调用get()方法获得Integer的引用。
唯一例外是可以获取Object的引用:Object o = p.getFirst()。
换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。
对比extends和super通配符
我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:
<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。
这个copy()方法的定义就完美地展示了extends和super的意图:
copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
copy()方法内部也不会修改src,因为不能调用src.add(T)。
这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

而这些都是通过super和extends通配符,并由编译器强制检查来实现的。
无限定通配符
我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:

void sample(Pair<?> p) {
}

因为<?>通配符既没有extends,也没有super,因此:
不允许调用set(T)方法并传入引用(null除外);
不允许调用T get()方法并获取T引用(只能获取Object引用)。
换句话说,既不能读,也不能写,那只能做一些null判断:

static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

大多数情况下,可以引入泛型参数<T>消除<?>通配符:

static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}

<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类。无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

7.泛型和反射

Java的部分反射API也是泛型。例如:Class<T>就是泛型:

// compile warning:
Class clazz = String.class;
String str = (String) clazz.newInstance();
// no warning:
Class<String> clazz = String.class;
String str = clazz.newInstance();

调用Class的getSuperclass()方法返回的Class类型是Class<? super T>:

Class<? super String> sup = String.class.getSuperclass();

构造方法Constructor<T>也是泛型:

Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);

我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组:

Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!

必须通过强制转型实现带泛型的数组:

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

使用泛型数组要特别小心,因为数组实际上在运行期没有泛型,编译器可以强制检查变量ps,因为它的类型是泛型数组。但是,编译器不会检查变量arr,因为它不是泛型数组。因为这两个变量实际上指向同一个数组,所以,操作arr可能导致从ps获取元素时报错,例如,以下代码演示了不安全地使用带泛型的数组:

Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;
ps[0] = new Pair<String>("a", "b");
arr[1] = new Pair<Integer>(1, 2);
// ClassCastException:
Pair<String> p = ps[1];
String s = p.getFirst();

要安全地使用泛型数组,必须扔掉arr的引用:

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

相关文章

  • 泛型 & 注解 & Log4J日志组件

    掌握的知识 : 基本用法、泛型擦除、泛型类/泛型方法/泛型接口、泛型关键字、反射泛型(案例) 泛型 概述 : 泛型...

  • 【泛型】通配符与嵌套

    上一篇 【泛型】泛型的作用与定义 1 泛型分类 泛型可以分成泛型类、泛型方法和泛型接口 1.1 泛型类 一个泛型类...

  • 泛型的使用

    泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法 泛型类 泛型接口 泛型通配符 泛型方法 静态方法与...

  • Java 泛型

    泛型类 例如 泛型接口 例如 泛型通配符 泛型方法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型上下边...

  • 探秘 Java 中的泛型(Generic)

    本文包括:JDK5之前集合对象使用问题泛型的出现泛型应用泛型典型应用自定义泛型——泛型方法自定义泛型——泛型类泛型...

  • Web笔记-基础加强

    泛型高级应用 自定义泛型方法 自定义泛型类 泛型通配符? 泛型的上下限 泛型的定义者和泛型的使用者 泛型的定义者:...

  • 重走安卓进阶路——泛型

    ps.原来的标题 为什么我们需要泛型? 泛型类、泛型接口和泛型方法(泛型类和泛型接口的定义与泛型方法辨析); 如何...

  • Kotlin泛型的高级特性(六)

    泛型的高级特性1、泛型实化2、泛型协变3、泛型逆变 泛型实化 在Java中(JDK1.5之后),泛型功能是通过泛型...

  • Java 19-5.1泛型

    泛型类定义泛型类可以规定传入对象 泛型类 和泛型方法 泛型接口 如果实现类也无法确定泛型 可以在继承类中确定泛型:

  • 【Swift】泛型常见使用

    1、Swift泛型4种 泛型函数泛型类型泛型协议泛型约束 2、泛型约束3种 继承约束:泛型类型 必须 是某个类的子...

网友评论

      本文标题:泛型

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