Java 之路 (十六) -- 泛型下(通配符、类型擦除、泛型的限制)

7. 通配符

通配符,即 "?",用来表示未知类型。

通配符可用作各种情况:作为参数,字段或局部变量的类型;有时也作为返回类型;通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。

7.1 上限有界的通配符

使用上限通配符来放宽对变量的限制。

声明上限通配符的语法:<? extends 上限>

举个例子:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

这里 上线通配符为 <? extends Number>,表明可以匹配任何 Number 类及其子类型。该方法返回列表中数字的总和。

7.2 无界通配符

使用通配符 "?" 指定无界通配符类型,例如 List<?>,称为未知类型的列表。

无界通配符有两种适用场景:

  1. 当前正在编写可以借由 Object 类中的方法来实现的方法。
  2. 使用泛型类中不依赖于类型参数的方法时,如 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();
}

如果只是希望打印 Object 列表,那这个方法可行;但是如果目标是打印任何类型的列表,那这个方法就不行了,它无法输出 List<\Integer>、List<\Double> 等等,因为这些都不是 List<\Object> 的子类型。

解决上述问题,就需要使用 List<?>:

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

这样,对于任何具体类型 A,List<A> 都是 List<?> 的子类型,于是可以用该方法打印任何类型的列表。

7.3 下限有界通配符

上限有界通配符将未知类型限制为该类型的特定类型或子类型,并使用 extends 关键字表示;类似的,下限有界通配符将位置类型限制为该类型的特定类型或超类型;

下限有界通配符的语法:<? super 下限>

注意:上限有界 和 下限有界 不能同时指定。

举个例子:

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

在这个例子中,下限有界通配符是 <? super Integer>,表示可以匹配任何 Integer 的超类型列表。该方法用来将数字 1 ~ 10 添加到列表的末尾。

7.4 通配符和子类型

在 5.1 泛型类和子类型 小节中,我们讲到泛型类或接口并不仅仅因为它们的类型之间存在关系而相互关联。

比如 Integer 是 Number 的子类型,但是 List<Integer> 和 List<Number> 却没什么关系,二者不过是有一个公共符类 List<?>:

image

如果需要通过 List<Integer> 的元素访问 Number 的方法,那么就需要使用上限有界通配符:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

因为 Integer 是 Number 的子类型,并且 numList 是一个 Number 对象的列表,所以现在 intList 和 numList 之间存在关联了。下图展示了使用上限和下限有界通配符声明的多个 List 类之间的关系:(箭头指向的是父类型)

image

7.5 通配符捕获和帮助方法

某些情况下,编译期会推断出通配符的类型。例如,列表定义为 List<?>,但是检查一个表达式的时候,编译期会从代码中推断出特定类型,这就叫做通配符捕获。

多数情况下,不必担心通配符捕获的问题,除非遇到包含 "capture of"的错误消息。

如下例子会报错:

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

此例中,编译期将输入参数 i 处理为 Object 类型,当 foo 方法调用 list.set(int,E) 时,编译期无法确认插入到列表中的对象的类型,于是产生错误。发生此类错误时,通常意味着编译器认为你为变量分配了错误的类型。也是出于这个原因,Java 添加泛型机制用来保证编译时类型安全。

那么发生错误时,如果解决编译器错误呢?通常通过编写捕获通配符的私有的 Helper 方法,如下所示:

public class WildcardFixed {

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

    // Helper method created so that the wildcard can be captured
    // through type inference.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }

}

由于辅助方法,编译器使用推断来确定 T 是调用中的捕获的变量

7.6 通配符使用指南

实际开发中,通常关于何时使用上限有界通配符以及何时使用下限有界通配符存在很大疑惑。本节就介绍设计代码时要遵循的一些原则。

首先假设两种变量:

  1. "in" 变量:向代码提供数据。
  2. "out" 变量:保存数据供其他地方使用。

通配符指南

  1. 用上限通配符定义“in”变量。(使用extends关键字)
  2. 用下限通配符定义“out”变量。(使用super关键字)
  3. 在可以使用Object类中定义的方法访问“in”变量的情况下,使用无界通配符。
  4. 在代码需要通过“in”和“out”变量访问其他变量的情况下,不要使用通配符。

避免使用通配符作为返回类型。

8. 类型擦除

Java 中并没有实现真正的泛型。为了实现泛型,Java 编译器将类型擦除应用于:

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

8.1 泛型类的擦除

在类型擦除过程中,Java 编译器将擦除所有类型参数,并在类型参数有界时将其替换为第一个边界;如果类型参数无界,则替换为 Object。

如下:

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

因为 T 无界,所以Java 编译器会用 Object 替换它:

public class Node {

    private Object data;
    private Node next;

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

    public Object getData() { return data; }
    // ...
}

再举个上限有界通配符的例子:

public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

Java 编译器会用第一个边界 Comparable 替换有界类型参数 T:

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

8.2 泛型方法的擦除

Java 编译器还会擦除泛型方法形参中的类型参数

1.举一个无界类型参数的例子

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

由于T无界,因此 Java 编译器会用 Object 替换之:

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

2.再举一个有界类型参数的例子:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

public static <T extends Shape> void draw(T shape) { /* ... */ }

上面编写了一个绘制不同形状的泛型方法,Java 编译器会将 T 替换为 Shape:

public static void draw(Shape shape) { /* ... */ }

8.3 不可具体化类型

8.3.1 什么是不可具体化类型

可具体化类型是指:类型信息在运行时完全可用,这v澳阔基元、非泛型类、原生类型和为绑定通配符的引用。

不可具体化的类型是指:运行时没有提供所有信息,会通过类型擦除删除部分编译时的信息。不可具体化类型的示例是 List<String> 和 List<Number>,运行时 JVM 无法区分这两个类型,它们都会编程 List 原生类型。

8.3.2 堆污染

当参数化类型的变量引用不是该参数化类型的对象时,会发生堆污染 。 如果程序执行某些操作,在编译时产生未经检查的警告,则会出现这种情况。 如果在编译时(在编译时类型检查规则的限制内)或在运行时,生成涉及参数化类型(例如,强制转换或方法调用)的操作的正确性,则会生成未经检查的警告。验证。 例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。

在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起您对潜在堆污染的注意。 如果单独编译代码的各个部分,则很难检测到堆污染的潜在风险。 如果确保代码在没有警告的情况下编译,则不会发生堆污染。

8.3.3 可变参数的潜在漏洞

带有可变参数的泛型方法可能导致堆污染。

考虑以下例子:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

下面的类使用 ArrayBuilder:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

当编译器遇到可变参数的方法时,它会将可变参数转换为数组。 但是,Java编程语言不允许创建参数化类型的数组。在方法ArrayBuilder.addToList ,编译器将可变参数T... elements转换为形式参数T[] elements ,即数组。 但是,由于类型擦除,编译器会将可变参数转换为Object[] elements 。 因此,存在堆污染的可能性。


9. 泛型的限制

  1. 无法使用基本数据类型实例化泛型类

    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');  // compile-time error
    

    应该如下调用:

    Pair<Integer, Character> p = new Pair<>(8, 'a');
    

    传入的基本类型参数如8 会自动装箱。

  2. *无法创建类型参数的实例

    //以下代码错误 
    public static <E> void append(List<E> list) {
        E elem = new E();  // compile-time error
        list.add(elem);
    }
    
    //解决方法:反射
    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
        E elem = cls.newInstance();   // OK
        list.add(elem);
    }
    
    
    //通过以下方式调用 append方法
     List <String> ls = new ArrayList <>();
     append(ls,String.class);
    
  1. 无法声明类型为类型参数的静态字段

    //不允许将 静态字段类型设置为类型参数的类型
    public class MobileDevice<T> {
        private static T os;
    
        // ...
    }
    
  2. 无法强制转换或使用 instanceof

    1. 因为Java编译器会擦除通用代码中的所有类型参数,所以无法验证在运行时使用泛型类型的参数化类型。

    2. 特殊的,可以通过无界通配符验证是否属于某个基类型

      public static void rtti(List<?> list) {
          if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
              // ...
          }
      }
      
    3. 更特殊的,某些情况下,编译器知道类型参数始终有效并允许强制转换:

      List<String> l1 = ...;
      ArrayList<String> l2 = (ArrayList<String>)l1;  // OK
      
  3. 无法创建参数化类型的数组

  4. 无法直接或间接扩展 Throwable 类,无法捕获类型参数的实例

    // Extends Throwable indirectly
    class MathException<T> extends Exception { /* ... */ }    // compile-time error
    
    // Extends Throwable directly
    class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
        
        public static <T extends Exception, J> void execute(List<J> jobs) {
        try {
            for (J job : jobs)
                // ...
        } catch (T e) {   // compile-time error
            // ...
        }
    }
    
  1. 类型擦除到原生类型的方法无法重载

    public class Example {
        public void print(Set<String> strSet) { }
        public void print(Set<Integer> intSet) { }
    }
    

    以上方法在类型擦除后具有相同的签名,会在编译时报错。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容