指定 T 的具体类型,该类型可以是类、接口、数组等,但不能是基本类型。

泛型类

我们知道使用泛型可以使类型错误在编译时就被检测到,从而能够增加程序的健壮性。

定义泛型类

public class Generic {

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

实例化泛型类

Generic genericString = new Generic<>("abc");
Generic genericInteger = new Generic<>(123);
System.out.println(genericString.getKey());
System.out.println(genericInteger.getKey());

需要注意的是泛型的类型参数必须是引用类型(类、接口、数组等都是引用类型)而不能是简单类型,如Generic generic = new Generic<>(123);是不允许的。

当然和 List 等一样,实例化泛型类可以传入任意类型而并不一定非要传入泛型类实参,只不过既然我们将类定义为泛型类,其目的就是希望开发者们能够传入确定的类型实参,以增加程序健壮性:

Generic generic = new Generic("abc");
Generic generic2 = new Generic(123);
Generic generic3 = new Generic(true);

泛型接口

定义泛型接口

public interface Generator {
    public T fun();
}

实现泛型接口

public class PersonGenerator implements Generator {
    @Override
    public T fun() {
        return null;
    }
}

可见当类实现泛型接口时若没有传入泛型实参,则需要将泛型也加到类的定义中,否则像下面的代码将会出现编译错误:

public class PersonGenerator implements Generator {
    @Override
    public T fun() {
        return null;
    }
}

如果实现泛型接口时传入了泛型实参,则该类中所有使用泛型的地方都要替换成传入的泛型实参:

public class PersonGenerator implements Generator {
    @Override
    public String fun() {
        return null;
    }
}

泛型方法

为了判断数组中是否包含某值写了如下两个重载方法:

public static void main(String[] args) {
    Integer[] integers = new Integer[]{1, 2, 3};
    String[] strings = new String[]{"a", "b", "c"};
    System.out.println(contains(2, integers));
    System.out.println(contains("b", strings));
}
public static boolean contains(Integer integer, Integer[] integers) {
    return Arrays.asList(integers).contains(integer);
}
public static boolean contains(String s, String[] strings) {
    return Arrays.asList(strings).contains(s);
}

如果还想要判断 Float 类型的数组中是否包含某个值就有需要编写一个重载方法,好在我们可以通过泛型方法有效的避免这些冗余的方法:

public static void main(String[] args) {
    Integer[] integers = new Integer[]{1, 2, 3};
    String[] strings = new String[]{"a", "b", "c"};
    Float[] floats = new Float[]{0.1f, 0.2f, 0.3f};
    System.out.println(contains(2, integers));
    System.out.println(contains("b", strings));
    System.out.println(contains(0.2f, floats));
}
public static  boolean contains(T t, T[] ts) {
    return Arrays.asList(ts).contains(t);
}

需要注意的是方法返回值前需要包含形式参数,如否则该方法不能被称为泛型方法,编译也将出错。
值得一提的是,如果同时保留以下两个方法:

public static  boolean contains(T t, T[] ts) {
    return Arrays.asList(ts).contains(t);
}
public static boolean contains(Integer integer, Integer[] integers) {
    return Arrays.asList(integers).contains(integer);
}

contains("b", strings)会自动匹配泛型方法,而contains(2, integers)匹配的是普通方法而不是泛型方法。

泛型通配符

无限定通配符-Unbounded Wildcard

我们知道 Integer、Double、Float 等都是 Number 类的子类,所以下面的代码完全没问题:

public static void main(String[] args) {
    printMsg(123);
}
public static void printMsg(Number number) {
    System.out.println(number);
}

基本类型 123 被自动装箱成 Integer 类型。而 Integer 又是 Number 类的子类,所以可以作为 printMsg 方法的实参。

泛型类GenericGeneric可以认为是两个完全没有关联的新类型,两者之间不具有任何的继承关系,所以下面的代码会出现编译错误:

public static void main(String[] args) {
    Generic genericNumber = new Generic<>(123);
    Generic genericInteger = new Generic<>(123);
    printMsg(genericNumber);  // 编译通过
    printMsg(genericInteger); // 编译出错,因为 Generic 和 Generic 二者之间没有任何继承关系
}
public static void printMsg(Generic generic) {
    System.out.println(generic.getKey());
}

而如果就是希望 printMsg 方法既能接收Generic又能够接收Generic类型,甚至是能够接收传入了任意实参类型的Generic泛型类(如GenericGeneric等),则需要用到泛型通配符?了:

public static void main(String[] args) {
    Generic genericNumber = new Generic<>(123);
    Generic genericInteger = new Generic<>(123);
    printMsg(genericNumber);  // 编译通过
    printMsg(genericInteger); // 编译通过
}
public static void printMsg(Generic generic) {
    System.out.println(generic.getKey());
}

上限通配符-Upper Bounded Wildcard

为泛型添加上边界,即传入的类型实参必须是指定类型或指定类型的子类。使用extends指定上限通配符

public static void main(String[] args) {
    Generic genericNumber = new Generic<>(123);
    Generic genericInteger = new Generic<>(123);
    Generic genericFloat = new Generic<>(0.5f);
    Generic genericString = new Generic<>("Hello");
    printMsg(genericNumber);  // 编译通过
    printMsg(genericInteger); // 编译通过
    printMsg(genericFloat);   // 编译通过
    printMsg(genericString);  // 编译出错
}
public static void printMsg(Generic generic) {
    System.out.println(generic.getKey());
}

因为Generic generic指定了传入的类型实参必须是 Number 类或 Number 类的子类,所以printMsg(genericString);出错,因为 String 不是 Number 的子类。

下限通配符-Lower Bounded Wildcard

和上限通配符类似,下限通配符使用super关键字实现:

public static void main(String[] args) {
    Generic genericNumber = new Generic<>(123);
    Generic genericInteger = new Generic<>(123);
    Generic genericFloat = new Generic<>(0.5f);
    Generic genericString = new Generic<>("Hello");
    printMsg(genericNumber);  // 编译通过
    printMsg(genericInteger); // 编译通过
    printMsg(genericFloat);   // 编译出错
    printMsg(genericString);  // 编译出错
}
public static void printMsg(Generic generic) {
    System.out.println(generic.getKey());
}

因为Generic generic指定了传入的类型实参必须是 Integer 类或 Integer 类的父类,所以printMsg(genericFloat);printMsg(genericString);出现编译错误,因为 Float 和 String 都不是 Integer 类的父类。

类型擦除-Type Erasure

Java 的泛型只在编译阶段有效,编译过程中正确检验泛型结果后,会将泛型相关信息擦除。并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法,即泛型信息不回进入运行时阶段:

public static void main(String[] args) {
    Generic genericInteger = new Generic<>(123);
    Generic genericString= new Generic<>("Hello");
    System.out.println(genericInteger.getClass() == genericString.getClass());  // 返回 true
}

结果返回true,说明虽然编译时GenericGeneric是不同的类型。但因为泛型的类型擦除,所以编译后genericIntegergenericString为相同的类型

命名规则

JDK 中文档经常能看到TKVEN等类型参数。而我们在编写泛型相关代码时,这些符号都可以随意使用。实际上还可以使用 JDK 文档中从来没用过的符号,如ABC等,但却极力不推荐这样做。

JDK 文档中各符号的含义:

  • T:类型
  • K:键
  • V:值
  • E:元素(如集合类中的元素全部用该符号表示)
  • N:Number

我们应该遵循 JDK 中已有的规范