Java 中泛型 T 和 ? 的区别详解

来自:网络
时间:2022-01-09
阅读:
目录

泛型中 T 类型变量 和 ? 通配符 区别

定义不同 :T 是类型变量,? 是通配符

使用范围不同:

  • ? 通配符用作 参数类型、字段类型、局部变量类型,有时作为返回类型(但请避免这样做)
  • T 用作 声明类的类型参数、通用方法的类型参数 (这里注意 类型参数 和 参数类型 是两个概念)

通常我们使用 ? 的时候并并不知道也不关心这个时候的类型,这里只想使用其通用的方法,而且 ? 通配符是无法作用于声明类的类型参数,一般作用于方法和参数上。而 类型变量 T 在类定义时具有更广泛的应用。

在某些程度的使用上 ? 通配符与 T 参数类型是可以等效的,但是 T 参数类型并不支持下界限制 即 T super SomeTing 而 通配符支持 ? super SomeThing

如果你想写一个通用的方法且该方法的逻辑不关心类型那么就大胆的用 ? 通配符来进行适配和限制吧,如果你需要作用域类型(这可能在操作通用数组类型时更明显)或者声明类的类型参数时请使用 T 类型变量

类型参数定义了一种代表作用域类型的变量(例如,T),通配符只是定义了一组可用于泛型类型的允许类型。通配符的意思是“在这里使用任何类型”

在泛型的使用中我们经常可以看到这样的用法:

public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>
public interface GenericProgressiveFutureListener<F extends ProgressiveFuture<?>> extends GenericFutureListener<F> {
    void operationProgressed(F future, long progress, long total) throws Exception;
}

如果你其用法和概念仍有疑问,那不妨继续阅读本文

了解他们的概念:Generic Types Wildcards ,以及使用。

Generic Types 类型变量

通用类型即 T、F、K、V 这样的写法,它是一种是通过类型参数化的通用类或接口,也可以称之为 类型变量

类型变量可以是任何非原始类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量

按照惯例,类型参数名称是单个大写字母。

最常用的类型参数名称是:

  • E - 元素(被 Java 集合框架广泛使用)
  • K - 键
  • N - 数字
  • T - 类型
  • V - 值
  • S、U、V 等 - 第 2、3、4 种类型

用法

1.声明通用的类型 – 泛型类:

当我们想对通用的对象类型进行操作时我们可能想到使用 Object ,但是使用 Object 在编译时无法进行检查,因为 Object 是所有类的父类,这可能导致我们意图在传入 Integer 并可以取出 Inerger 时,在另一部分代码错误的传入了 String

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

为了避免上述的问题,我们可以选择使用 类型变量

public class Box<T> {
    // T stands for "Type"
    private T t;

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

你也可以使用多个类型参数

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; }
}

2.声明通用的方法 – 泛型方法:

泛型方法 是引入自己的类型参数的方法。这类似于声明泛型类型,但类型参数的范围仅限于声明它的方法。允许静态和非静态泛型方法,以及泛型类构造函数。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

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

一个完整的调用是

JestTestMain.<String>printListT(names);

但是通常可以省略类型,这里使用到的功能是 类型推断

JestTestMain.printListT(names);

有界类型参数

同时我们可以对类型参数进行限制通过 extends 关键字

如 <T extends Number> 这里的泛型参数就限制了必须继承于 Number 类。

public static <T extends Number> void printListT(List<T> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

同时 Java 也支持多重限定,如 <T extends CharSequence & Comparable<T> & Serializable> 但是如果其中限定包含 类 需要写在最前面

public static <T extends CharSequence & Comparable<T> & Serializable> void printListT(List<T> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

Wildcards 通配符

通配符即指 ?

在泛型代码中,? 表示未知类型。通配符可用于多种情况:

作为参数、字段或局部变量的类型,有时作为返回类型(但请避免这样做)。

通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。

用法

通配符分为 3 种:

1.上界通配符:? extend 上界类型

List public static void process(List list) { /* ... */ }

我们可以使用上界通配符来放宽对变量的限制

2.无界通配符:?

如 List<?> 这表示未知类型的列表,一般有两种情况下无界通配符是有用的:

  • 你正在编写可以使用 Object类中提供的功能实现的方法
  • 当代码使用不依赖于类型参数的泛型类中的方法时。例如,List.size或 List.clear。事实上,Class<?> 之所以如此常用,是因为 Class<T>中的大多数方法都不依赖于 T。

如何理解这句话的意思呢?来看一个例子:

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

printList 的意图是想打印任何类型的列表,但是它没有达到目标,其只打印了 Object 实例的列表。它不能打印 List<Integer>、List<String>、List<Double>等,因为它们不是 List<Object> 的子类型。

Java 中泛型 T 和 ? 的区别详解

编译时将会报错。

这里我们换成通配符将正确运行

public class JestTestMain {
    public static void main(String[] args) {
        List<String> names= Lists.newArrayList();
        names.add("张三");
        names.add("张三1");
        names.add("张三2");
        printList(names);
    }

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

打印:

张三 
张三1 
张三2 

这里需要明白的一点是,List<Object> 和 List<?> 并不相同,你可以向 List<Object> 中插入 Object 对象,或者任何其子类对象,但是你只能向 List<?> 中插入 null 值。

3.下界通配符:? super 子类

如:<? super Integer>

假设你要编写一个将 Integer 对象放入列表的方法。为了最大限度地提高灵活性,希望该方法适用于 List<Integer>、List<Number>和 List<Object> 任何可以保存 Integer 值的东西。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

类型擦除

我们要知道的一件事儿,编译器在编译时清除了所有类型参数,也就是说会将我们的类型参数进行实际的替换。

就算如此我们仍有使用泛型的理由

  • Java 编译器在编译时对泛型代码执行更严格的类型检查。
  • 泛型支持编程类型作为参数。
  • 泛型能够实现泛型算法。

Java 语言中引入了泛型以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器将类型擦除应用于:

  • 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或 Object。因此,生成的字节码仅包含普通的类、接口和方法。
  • 必要时插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保不会为参数化类型创建新类;因此,泛型不会产生运行时开销。

下面举两个例子

// 类型擦除前
public class Pair<K, V> {

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

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

    public void setKey(K key)     { this.key = key; }
    public void setValue(V value) { this.value = value; }

    private K key;
    private V value;
}
// 类型擦除后
public class Pair {

    public Pair(Object key, Object value) {
        this.key = key;
        this.value = value;
    }

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

    public void setKey(Object key)     { this.key = key; }
    public void setValue(Object value) { this.value = value; }

    private Object key;
    private Object value;
}

// 类型擦除前
public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) {
    // ...
}
// 类型擦除后
public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
    // ...
}

最后感兴趣的同学可以参考:

Java Tutorial on Generics
Generics in the Java programming language

返回顶部
顶部