Java高级特性-泛型通配符

通配符 ?

在泛型中,问号 ? 叫做通配符,它表示了未知的类型。在使用上,通配符可以用来定义参数类,字段或本地变量,有时也可以作为方法返回类型。

有了类型T,为何要引入通配符 ?

类型 T 表示的是任意类型,表示的是某个具体的类型。通配符 ? 表示的是未知类型。我们可以从类/接口定义,变量,方法的不同角度去看下具体的区别。

类/接口

  1. 泛型类型 T 表示泛型参数的类型。下面 Box 类是一个泛型类的定义。

    public class Box<T> {
        private T t;
    
        public void set(T t) { this.t = t; }
    
        public T get() { return t; }
    }
    

    也可以使用有界的定义方式限制泛型参数的类型范围。

    // 1. 上界类型定义
    class Box<T extends Number> {
        private T t;
    
        public void set(T t) { this.t = t; }
    
        public T get() { return t; }
    }
    
    // 2. 下界类型定义,下面的代码会报编译错误
    class Box<T super Number> {
        private T t;
    
        public void set(T t) { this.t = t; }
    
        public T get() { return t; }
    }
    

    上面的代码 2 的定义会导致编译器报错,T super Number表示 T 必须是 Number 类型或是 Number 的父类型。而泛型类在 Java编译器编译期间被类型擦除 (后续会有文章说明),变成 Object 类型,这时 Box 类就变成了可以包含任意类型的类,这与泛型设计的目的相悖,因为泛型类应该限制其所能存储的对象类型。

    泛型的功能:

    • 提高代码的安全性:泛型可以限制变量或方法的类型,从而避免类型转换错误。
    • 提高代码的可读性:泛型可以使代码更加清晰和简洁。
    • 提高代码的灵活性:泛型可以提高代码的复用性。
  2. 使用通配符的类型定义。

    // 这个类的定义是错误的。Unexpected wildcard
    class Box<? extends Number> {}
    

    通配符 ? 表示了未知类型,在泛型类 Box 定义中无法表示具体的类型,不能用来表示类型进行使用。如下:

    class Box<? extends Number> {
        private ? t;
        // ...
    }
    

    这个写法肯定是错误的

方法

  1. 类型 T 在泛型方法定义中使用。

    <T> void processList(List<T> list) {
        System.out.println("list size:" + (list == null ? 0: list.size()));
    }
    
  2. 通配符 ? 在方法定义中的使用。方法涉及参数,返回值,方法体中的操作。

    • 参数类型的限制:通配符可以用于限制方法参数的类型,从而提高代码的安全性和可读性。

      以下代码定义了一个方法,参数类型是List,表示参数可以是List的子类,并且参数中的元素可以是Number的子类:

      public static <T> void printList(List<? extends T> list) {
          for (T t : list) {
              System.out.println(t);
          }
      }
      
    • 返回类型的限制:通配符可以用于限制方法返回类型的类型,从而提高代码的灵活性。

      以下代码定义了一个方法,返回类型是List,表示返回值可以是List的子类,并且返回值中的元素可以是T的子类:

      public static <T> List<? extends T> getList() {
          return new ArrayList<>();
      }
      
    • 方法体中的操作:通配符可以用于方法体中的操作,从而提高代码的可读性和复用性。

      以下代码定义了一个方法,用于比较两个对象的大小:

      public static <T extends Comparable<T>> int compare(T o1, T o2) {
          return o1.compareTo(o2);
      }
      

    通配符使用不限于泛型方法,如下的代码定义了数字类型列表的和。

        public static double sumOfList(List<? extends Number> list) { // 非泛型方法
            double s = 0.0;
            for (Number n : list)
                s += n.doubleValue();
            return s;
        }
    
        public static void main(String[] args) {
            System.out.println("sum=" + sumOfList(Arrays.asList(1,2,3)));
        }
    

变量

  1. 泛型类型 T 在定义变量的过程中,需要注意的有类型兼容性。

    T t;
    

    这样给出了声明变量的方式,但 T 并不是 Java 中的具体类型,因此在使用上一般会与类型定义一起使用。

    或者使用类型 T 定义方法的参数类型。

  2. 通配符 ? 的变量声明。

    • 无界的情况下。无界通配符?表示变量可以存储任何类型。

      List<?> list;
      

      这样的声明方式是符合 Java 语法定义的,但是在实际定义使用中,会有问题。如下声明:

      List<?> list = new ArrayList<>(); // 正确
      list.add("123"); // capture of ?
      

      这里 list的实际类型是 ArrayList,尝试向其中加入字符串 123,Java 编译器会提示 通配符捕获 的错误提示,后面将具体介绍。 因为泛型的一个作用就是限制类型范围,而列表 list中可以加入任意类型的对象,这与泛型的初衷相悖的。

    • 上界通配符。上界通配符? extends T表示变量可以存储类型 T 的子类。

      List<? extends Number> list = new ArrayList<>(); // 正确
      list.add(3);
      list.add(6.5f); // capture of ? extends Number
      

      同样,定义 list是符合 Java 语法的,但尝试向 list中添加元素时,Java 编译器就抛出了 通配符捕获 异常。

    • 下界通配符。下界通配符? super T表示变量可以存储类型 T 的父类。

      List<? super String> list = new ArrayList<>(); // 正确
      list.add("123"); // 正确
      list.add(new Object()); // capture of ? super String
      

      遇到与前两个定义方式一样的问题。

总结:

  • 使用范围不同T 用来声明类、接口、方法的类型参数。通配符 ? 用来声明方法的参数类型、字段类型、局部变量类型等。
  • 使用限制不同T 表示为某个特定的类型。通配符可以被限定为某个特定类型 T 的子类型或父类型。
  • 使用场景不同:T通常用于表示泛型类型的实际类型。通配符通常用于表示泛型类型的任意类型。

通配符的边界

通配符也可以分为 有界通配符无界通配符 两种类型。

  • 有界通配符,同样使用到关键字 extendssuper,写法上是 ? extends T? super T ,其中 T 表示某个类型。
  • 无界通配符,独立使用 ? 表示泛型类型。

有界通配符

上界通配符(Upper Bounded Wildcards)使用 ? extends T表示 T 的子类。上界通配符主要使用在方法定义中限制参数类型,返回值类型及变量声明中。

  • 限制方法的参数类型。

    下列代码中参数中可以传入任意 Number 类型及其子类型的列表。

    // 方法参数类型
    public static void printList(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }
    
  • 限制方法的返回类型。

    // 方法定义的返回类型被限制成 CharSequence 及其父类型的列表。
    List<? super CharSequence> processStringList(List<?> list) {
        String elem1 = "789";
        List<? super CharSequence> v = new ArrayList<>();
        System.out.println("list size:" + (list == null ? 0: list.size()));
        return v;
    }
    
  • 声明变量。

    List<? extends Number> list = new ArrayList<>();
    

无界通配符

适用无界通配符的两个场景:

  • 使用 Object中的定义功能可以实现的方法定义。
  • 使用泛型类中的不依赖类型参数的方法。例如:List.size()方法等。

假设要定义一个打印列表大小的方法,不管类型参数。若使用 List 作为方法的参数,即代码:

static void printListSize(List<Object> list) {
    System.out.println("size: " + list.size());
}

这个方法定义,只能传入 List 类型的列表,不能用于打印 List List 等类型列表的大小。要适用于所有类型列表大小的打印需求,可以将参数类型定义为 List ,使用无界通配符作为参数 List 的类型参数。

static void printListSize(List<?> list) {
    System.out.println("size: " + list.size());
}

上述两个方法定义的类型 ListList 类型不同。List 列表中可以放入任意 Object 及其子类型对象。而List 列表中只能放入 null

通配符和子类型

在前面的文章中有写过,有继承关系的两个类,在作为类型参数用以定义列表类型时,两个列表没有任何关系。

class A { // ... }
class B extends A { // ... }

上述代码,B 继承自 A,在若将这两个类型作为 List 的类型参数,ListList 确是没有任何的关系。

Java高级特性-泛型通配符_第1张图片

使用通配符定义,可以将 List 定义修改为 List ,这样 ListList 存在继承关系。

Java高级特性-泛型通配符_第2张图片

通配符捕获

使用通配符定义泛型类型时,在编译器推导实际类型时,会遇到通配符捕捉问题。

例如,定义一个 List 类型类型变量,但实际操作时,添加如 String 类型或其他引用类型对象是,Java 编译器会报编译错误,即通配符捕捉提示,通常含提示文字 “capture of”。

看下列一个代码:

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0)); // Java编译器会提示 "capture of ?"
    }
}

这段代码中,Java 编译器会提示 通配符捕捉错误,进而编译失败。编译时 i.get(0) 获取到的结果被 Java 编译器认为是 Object 类型的,使用 List.set() 方法将获取的对象设置到列表中,但是 Java 编译器无法判断要添加到 i 的对象是什么具体类型,对于可能由于类型不同而导致的类型安全问题,Java 编译器给出错误提示,禁止编译通过。

要解决这种编译错误,需要定一个帮助方法,让编译器任务对象的类型时确定的。

public class WildcardError {
	void foo(List<?> i) {
        fooHelper(i);
    }

    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }
}

泛型类型T与通配符的区别

  • 泛型类型参数 T 是一种确定性的类型,表示在使用泛型类或方法时,指定了具体的类型参数。例如,List 中的 T 被明确地指定为 String 类型,因此在编译时可以知道使用的是具体的类型。

    通配符表示不确定的类型,用于在泛型类型中接受多种可能的类型。通配符使用?表示,例如List,表示可以接受任意类型的List。

  • 泛型类型参数 T 可以用于读取和写入操作。可以根据具体的类型参数执行各种操作,例如读取、修改和写入元素。

    通配符通常用于读取操作,可以从具有通配符类型的对象中读取元素,但无法添加或修改元素。这是因为编译器无法确定通配符的具体类型,无法保证类型安全性。

通配符使用准则

通配符在泛型的使用过程中有时会让人觉得困惑,下面可以看下几个通配符使用中的准则。

先了解下什么时"in","out"变量。

  • in 变量主要用于数据的输入,只能被读取,不能被修改。
  • out 变量可用于他处,数据被修改。
  • 既是 in,又是 out 变量。

使用的注意:

  • 一个 “in” 变量的定义使用上界通配符,即使用 ? extends T
  • 一个 “out” 变量的定义使用下界通配符,即使用 ?super T
  • 可使用 Object 类中方法访问的 “in” 变量的情况下,使用无界通配符。
  • 代码中需要访问的变量既作 “in” 又作 “out” 变量,不使用通配符。

上述几条使用注意项不适用于方法返回值。假设方法的返回类型定义使用了通配符,那么程序中的其他位置需要手动处理通配符的问题。

你可能感兴趣的:(Java高级特性,java)