Java 中文官方教程 2022 版(六)

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

字符和字符串总结

原文:docs.oracle.com/javase/tutorial/java/data/stringsummary.html

大多数情况下,如果您使用单个字符值,您将使用基本的char类型。然而,有时您需要将 char 用作对象—例如,作为期望对象的方法参数。Java 编程语言为此提供了一个包装类,将char包装在Character对象中。Character类型的对象包含一个类型为char的单个字段。这个Character类还提供了许多有用的类(即静态)方法来操作字符。

字符串是字符序列,在 Java 编程中被广泛使用。在 Java 编程语言中,字符串是对象。String类有 60 多个方法和 13 个构造函数。

最常见的是,您可以使用类似于以下语句创建一个字符串

String s = "Hello world!";

而不是使用其中一个String构造函数。

String类有许多方法可以查找和检索子字符串;然后可以使用+连接运算符将它们轻松重新组装成新的字符串。

String类还包括许多实用方法,其中包括split()toLowerCase()toUpperCase()valueOf()。后者方法在将用户输入的字符串转换为数字时是不可或缺的。Number子类还有将字符串转换为数字以及反之的方法。

除了String类之外,还有一个StringBuilder类。与字符串一起工作相比,使用StringBuilder对象有时可能更有效率。StringBuilder类提供了一些对字符串有用的方法,其中包括reverse()。然而,总的来说,String类具有更广泛的方法。

可以使用StringBuilder构造函数将字符串转换为字符串构建器。可以使用toString()方法将字符串构建器转换为字符串。

自动装箱和拆箱

原文:docs.oracle.com/javase/tutorial/java/data/autoboxing.html

自动装箱是 Java 编译器在原始类型和其对应的对象包装类之间进行的自动转换。例如,将int转换为Integer,将double转换为Double等。如果转换反向进行,则称为拆箱

这是自动装箱的最简单示例:

Character ch = 'a';

本节中的其余示例使用泛型。如果您还不熟悉泛型的语法,请参阅泛型(更新)课程。

考虑以下代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    li.add(i);

尽管您将int值作为原始类型而不是Integer对象添加到li中,但代码仍然可以编译。因为liInteger对象的列表,而不是int值的列表,您可能会想知道为什么 Java 编译器没有发出编译时错误。编译器不会生成错误,因为它从i创建一个Integer对象并将该对象添加到li中。因此,编译器在运行时将前面的代码转换为以下代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    li.add(Integer.valueOf(i));

将原始值(例如int)转换为相应包装类(Integer)的对象称为自动装箱。当原始值是以下情况时,Java 编译器会应用自动装箱:

  • 作为传递给期望相应包装类对象的方法的参数。

  • 赋给相应包装类的变量。

考虑以下方法:

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i: li)
        if (i % 2 == 0)
            sum += i;
        return sum;
}

因为余数(%)和一元加号(+=)运算符不适用于Integer对象,您可能会想知道为什么 Java 编译器在不发出任何错误的情况下编译该方法。编译器不会生成错误,因为它在运行时调用intValue方法将Integer转换为int

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i : li)
        if (i.intValue() % 2 == 0)
            sum += i.intValue();
        return sum;
}

将包装类型的对象(Integer)转换为其对应的原始类型(int)值称为拆箱。当包装类的对象是以下情况时,Java 编译器会应用拆箱:

  • 作为传递给期望相应原始类型值的方法的参数。

  • 赋给相应原始类型的变量。

Unboxing示例展示了这是如何工作的:

import java.util.ArrayList;
import java.util.List;

public class Unboxing {

    public static void main(String[] args) {
        Integer i = new Integer(-8);

        // 1\. Unboxing through method invocation
        int absVal = absoluteValue(i);
        System.out.println("absolute value of " + i + " = " + absVal);

        List<Double> ld = new ArrayList<>();
        ld.add(3.1416);    // Π is autoboxed through method invocation.

        // 2\. Unboxing through assignment
        double pi = ld.get(0);
        System.out.println("pi = " + pi);
    }

    public static int absoluteValue(int i) {
        return (i < 0) ? -i : i;
    }
}

该程序打印如下内容:

absolute value of -8 = 8
pi = 3.1416

自动装箱和拆箱使开发人员编写更清晰的代码,使其更易于阅读。以下表列出了原始类型及其对应的包装类,这些包装类由 Java 编译器用于自动装箱和拆箱:

Primitive typeWrapper class
booleanBoolean
byteByte
charCharacter
floatFloat
intInteger
longLong
shortShort
doubleDouble

问题和练习:字符和字符串

原文:docs.oracle.com/javase/tutorial/java/data/QandE/characters-questions.html

问题

  1. 以下字符串构建器的初始容量是多少?

    StringBuilder sb = new StringBuilder("Able was I ere I saw Elba.");
    
    
  2. 考虑以下字符串:

    String hannah = "Did Hannah see bees? Hannah did.";
    
    
    1. 表达式hannah.length()显示的值是多少?

    2. 方法调用hannah.charAt(12)返回的值是多少?

    3. 编写一个表达式,引用hannah所指的字符串中的字母b

  3. 以下表达式返回的字符串有多长?这个字符串是什么?

    "Was it a car or a cat I saw?".substring(9, 12)
    
    
  4. 在下面的程序中,名为ComputeResult,每个编号行执行后result的值是多少?

    public class ComputeResult {
        public static void main(String[] args) {
            String original = "software";
            StringBuilder result = new StringBuilder("hi");
            int index = original.indexOf('a');
    
    /*1*/   result.setCharAt(0, original.charAt(0));
    /*2*/   result.setCharAt(1, original.charAt(original.length()-1));
    /*3*/   result.insert(1, original.charAt(4));
    /*4*/   result.append(original.substring(1,4));
    /*5*/   result.insert(3, (original.substring(index, index+2) + " ")); 
    
            System.out.println(result);
        }
    }
    
    

练习

  1. 展示两种方法将以下两个字符串连接在一起以得到字符串"Hi, mom."

    String hi = "Hi, ";
    String mom = "mom.";
    
    
  2. 编写一个程序,从你的全名中计算出你的缩写并显示出来。

  3. 一个变位词是由另一个单词或短语的字母重新排列而成的单词或短语;例如,“parliament”是“partial men”的变位词,“software”是“swear oft”的变位词。编写一个程序,判断一个字符串是否是另一个字符串的变位词。该程序应忽略空格和标点符号。

检查你的答案。

Lesson: 泛型(更新)

原文:docs.oracle.com/javase/tutorial/java/generics/index.html

在任何非平凡的软件项目中,错误都是生活中不可避免的事实。仔细的规划、编程和测试可以帮助减少它们的普遍性,但不知何故,它们总会找到一种方式悄悄地潜入你的代码中。随着新功能的引入和代码库规模与复杂性的增长,这一点变得尤为明显。

幸运的是,有些错误比其他错误更容易检测。例如,编译时错误可以在早期被检测出来;你可以利用编译器的错误消息来找出问题所在并立即修复它。然而,运行时错误可能会更加棘手;它们并不总是立即显现,而且当它们出现时,可能是在程序中与问题实际原因相距甚远的地方。

泛型通过在编译时使更多的错误可检测,为你的代码增加了稳定性。完成本课程后,你可能想继续学习 Gilad Bracha 的《泛型》教程。

为什么使用泛型?

原文:docs.oracle.com/javase/tutorial/java/generics/why.html

简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。就像在方法声明中使用的更熟悉的形式参数一样,类型参数提供了一种方式让您可以重复使用相同的代码以不同的输入。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。

使用泛型的代码比不使用泛型的代码有许多好处:

  • 编译时进行更强的类型检查。

    Java 编译器对泛型代码应用强类型检查,如果代码违反类型安全性,则会发出错误。修复编译时错误比修复运行时错误更容易,后者可能很难找到。

  • 消除强制类型转换。

    不使用泛型的以下代码片段需要进行强制类型转换:

    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    
    

    当重写为使用泛型时,代码不需要进行强制类型转换:

    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0);   // no cast
    
    
  • 使程序员能够实现泛型算法。

    通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以进行定制,并且是类型安全且更易阅读的。

泛型类型

原文:docs.oracle.com/javase/tutorial/java/generics/types.html

泛型类型是一个参数化类型的泛型类或接口。下面的 Box 类将被修改以演示这个概念。

一个简单的 Box 类

首先看一下一个操作任意类型对象的非泛型 Box 类。它只需要提供两个方法:set,用于向盒子中添加对象,和 get,用于检索对象:

public class Box {
    private Object object;

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

由于它的方法接受或返回一个 Object,你可以自由传入任何你想要的东西,只要不是原始类型之一。在编译时无法验证类的使用方式。代码的一部分可能将一个 Integer 放入盒子中,并期望从中获取 Integer,而代码的另一部分可能错误地传入一个 String,导致运行时错误。

Box 类的泛型版本

一个泛型类的定义格式如下:

class name<T1, T2, ..., Tn> { /* ... */ }

类型参数部分,由尖括号(<>)界定,跟在类名后面。它指定了类型参数(也称为类型变量T1T2、… 和 Tn

要将 Box 类更新为使用泛型,你需要通过将代码 “public class Box” 更改为 “public class Box<T>” 来创建一个泛型类型声明。这引入了类型变量 T,可以在类内部的任何地方使用。

有了这个改变,Box 类变成了:

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

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

正如你所看到的,所有 Object 的出现都被 T 替换。类型变量可以是你指定的任何非原始类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。

这种技术也可以应用于创建泛型接口。

类型参数命名约定

按照惯例,类型参数的名称是单个大写字母。这与你已经了解的变量命名约定形成鲜明对比,而且有充分的理由:没有这个约定,很难区分类型变量和普通类或接口名称。

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

  • E - Element(Java 集合框架广泛使用)

  • K - Key

  • N - Number

  • T - Type

  • V - Value

  • S,U,V 等 - 第二、第三、第四种类型

你将在 Java SE API 和本课程的其余部分中看到这些名称的使用。

调用和实例化泛型类型

要在代码中引用泛型 Box 类,你必须执行一个泛型类型调用,将 T 替换为某个具体值,比如 Integer

Box<Integer> integerBox;

你可以将泛型类型调用看作类似于普通方法调用,但是不是向方法传递参数,而是向 Box 类本身传递一个类型参数 — 在本例中是 Integer


类型参数和类型参数术语: 许多开发人员将“类型参数”和“类型参数”这两个术语互换使用,但这两个术语并不相同。在编码时,为了创建参数化类型,需要提供类型参数。因此,在Foo<T>中,T是类型参数,而在Foo<String> f中的String是类型参数。本课程在使用这些术语时遵守此定义。


与任何其他变量声明一样,此代码实际上并不创建新的Box对象。它只是声明integerBox将保存对“Box of Integer”的引用,这就是Box<Integer>的含义。

通常将泛型类型的调用称为参数化类型

要实例化此类,像往常一样使用new关键字,但是在类名和括号之间放置<Integer>

Box<Integer> integerBox = new Box<Integer>();

钻石

在 Java SE 7 及更高版本中,只要编译器可以从上下文中确定或推断出类型参数,就可以用空类型参数集(<>)替换调用泛型类构造函数所需的类型参数。这一对尖括号<>非正式地称为钻石。例如,您可以使用以下语句创建Box<Integer>的实例:

Box<Integer> integerBox = new Box<>();

有关钻石符号和类型推断的更多信息,请参见类型推断。

多个类型参数

如前所述,泛型类可以具有多个类型参数。例如,实现泛型Pair接口的泛型OrderedPair类:

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

以下语句创建了OrderedPair类的两个实例:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

代码new OrderedPair<String, Integer>实例化KStringVInteger。因此,OrderedPair的构造函数的参数类型分别为StringInteger。由于自动装箱,将Stringint传递给类是有效的。

如钻石中所述,因为 Java 编译器可以从声明OrderedPair<String, Integer>中推断出KV类型,所以可以使用钻石符号缩短这些语句:

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

要创建泛型接口,遵循与创建泛型类相同的约定。

参数化类型

您还可以用参数化类型(即List<String>)替换类型参数(即KV)。例如,使用OrderedPair<K, V>示例:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

原始类型

原文:docs.oracle.com/javase/tutorial/java/generics/rawTypes.html

原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定泛型Box类:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

要创建Box<T>的参数化类型,您需要为形式类型参数T提供实际类型参数:

Box<Integer> intBox = new Box<>();

如果省略实际类型参数,则创建Box<T>的原始类型:

Box rawBox = new Box();

因此,Box是泛型类型Box<T>的原始类型。但是,非泛型类或接口类型是原始类型。

在旧代码中会出现原始类型,因为在 JDK 5.0 之前,许多 API 类(如Collections类)都不是泛型的。使用原始类型时,实际上获得的是泛型之前的行为 —— Box会给您Object。为了向后兼容,允许将参数化类型分配给其原始类型:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

但是,如果将原始类型赋给参数化类型,则会收到警告:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

如果使用原始类型调用相应泛型类型中定义的泛型方法,也会收到警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

此警告显示原始类型绕过了泛型类型检查,将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。

类型擦除部分提供了有关 Java 编译器如何使用原始类型的更多信息。

未经检查的错误消息

如前所述,在将旧代码与泛型代码混合使用时,可能会遇到类似以下的警告消息:

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

当使用在原始类型上操作的旧 API 时,可能会出现以下示例中所示的情况:

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

“未经检查”一词表示编译器没有足够的类型信息来执行确保类型安全所需的所有类型检查。默认情况下,“未经检查”警告是禁用的,尽管编译器会给出提示。要查看所有“未经检查”警告,请使用-Xlint:unchecked重新编译。

使用-Xlint:unchecked重新编译前面的示例,会显示以下额外信息:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

要完全禁用未经检查的警告,请使用-Xlint:-unchecked标志。@SuppressWarnings("unchecked")注解可以抑制未经检查的警告。如果您不熟悉@SuppressWarnings语法,请参阅注解。

泛型方法

原文:docs.oracle.com/javase/tutorial/java/generics/methods.html

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

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

Util 类包含一个泛型方法 compare,用于比较两个 Pair 对象:

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 class Pair<K, V> {

    private K key;
    private V value;

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

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

调用这个方法的完整语法如下:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

类型已经明确提供,如粗体所示。通常情况下,这部分可以省略,编译器会推断所需的类型:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

这个特性被称为类型推断,允许您将泛型方法作为普通方法调用,而无需在尖括号之间指定类型。这个主题在下一节 类型推断 中进一步讨论。

有界类型参数

原文:docs.oracle.com/javase/tutorial/java/generics/bounded.html

有时候你可能想要限制可以用作参数化类型中类型参数的类型。例如,一个操作数字的方法可能只想接受Number或其子类的实例。这就是有界类型参数的用途。

要声明一个有界类型参数,列出类型参数的名称,后跟extends关键字,后跟其上界,在这个例子中是Number。请注意,在这个上下文中,extends的意思是"扩展"(如类)或"实现"(如接口)。

public class Box<T> {

    private T t;          

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

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

通过修改我们的通用方法以包含这个有界类型参数,编译现在会失败,因为我们对inspect的调用仍然包括一个String

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                  ^
1 error

除了限制你可以用来实例化泛型类型的类型之外,有界类型参数还允许你调用边界中定义的方法:

public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}

isEven方法通过n调用了Integer类中定义的intValue方法。

多个边界

前面的例子说明了使用具有单个边界的类型参数,但是类型参数可以有多个边界

<T extends B1 & B2 & B3>

具有多个边界的类型变量是边界中列出的所有类型的子类型。如果边界中有一个类,它必须首先指定。例如:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

如果边界A没有首先指定,你会得到一个编译时错误:

class D <T extends B & A & C> { /* ... */ }  // compile-time error

泛型方法和有界类型参数

原文:docs.oracle.com/javase/tutorial/java/generics/boundedTypeParams.html

有界类型参数是实现泛型算法的关键。考虑以下方法,该方法计算数组T[]中大于指定元素elem的元素数量。

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

该方法的实现很简单,但它无法编译,因为大于运算符(>)仅适用于原始类型,如shortintdoublelongfloatbytechar。你不能使用>运算符来比较对象。为了解决这个问题,使用一个由Comparable<T>接口限定的类型参数:

public interface Comparable<T> {
    public int compareTo(T o);
}

最终的代码将是:

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

泛型、继承和子类型

译文:docs.oracle.com/javase/tutorial/java/generics/inheritance.html

正如你已经知道的,可以将一个类型的对象赋给另一个类型的对象,前提是这两种类型是兼容的。例如,你可以将一个 Integer 赋给一个 Object,因为 ObjectInteger 的超类型之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

在面向对象的术语中,这被称为“是一个”关系。由于 Integer Object 的一种,所以赋值是允许的。但是 Integer 也是 Number 的一种,所以下面的代码也是有效的:

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

泛型也是如此。你可以执行泛型类型调用,将 Number 作为其类型参数,并且如果参数与 Number 兼容,则允许任何后续的 add 调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

现在考虑以下方法:

public void boxTest(Box<Number> n) { /* ... */ }

它接受什么类型的参数?通过查看其签名,你可以看到它接受一个类型为 Box<Number> 的单个参数。但这意味着什么?你可以传入 Box<Integer>Box<Double> 吗,正如你可能期望的那样?答案是“不可以”,因为 Box<Integer>Box<Double> 不是 Box<Number> 的子类型。

这是在使用泛型进行编程时的一个常见误解,但这是一个重要的概念需要学习。

显示 Box 不是 Box 的子类型的图表Box<Integer> 不是 Box<Number> 的子类型,即使 IntegerNumber 的子类型。


**注意:**给定两个具体类型 AB(例如,NumberInteger),MyClass<A>MyClass<B> 没有关系,无论 AB 是否相关。MyClass<A>MyClass<B> 的共同父类是 Object

有关如何在类型参数相关的情况下创建两个泛型类之间类似子类型的关系的信息,请参阅通配符和子类型。


通用类和子类型

你可以通过扩展或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extendsimplements 子句确定。

Collections 类为例,ArrayList<E> 实现了 List<E>,而 List<E> 扩展了 Collection<E>。因此,ArrayList<String>List<String> 的子类型,List<String>Collection<String> 的子类型。只要不改变类型参数,类型之间的子类型关系就会保持不变。

显示一个示例集合层次结构的图表:ArrayList 是 List 的子类型,List 是 Collection 的子类型。一个示例 Collections 层次结构

现在想象一下,我们想要定义自己的列表接口,PayloadList,它将泛型类型P的可选值与每个元素关联起来。它的声明可能如下所示:

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

以下对PayloadList的参数化是List<String>的子类型:

  • PayloadList<String,String>

  • PayloadList<String,Integer>

  • PayloadList<String,Exception>

显示一个示例 PayLoadList 层次结构的图表:PayloadList<String, String>是 List的子类型,List是 Collection的子类型。在 PayloadList<String,String>的同一级别是 PayloadList<String, Integer>和 PayloadList<String, Exceptions>。一个PayloadList层次结构示例

类型推断

原文:docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

类型推断是 Java 编译器根据每个方法调用和相应声明来确定使调用适用的类型参数(或参数)的能力。推断算法确定参数的类型,以及如果可用的话,结果被分配或返回的类型。最后,推断算法尝试找到适用于所有参数的最具体类型。

为了说明最后一点,在以下示例中,推断确定传递给pick方法的第二个参数的类型为Serializable

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

类型推断和泛型方法

泛型方法向您介绍了类型推断,使您能够调用泛型方法,就像调用普通方法一样,而无需在尖括号之间指定类型。考虑以下示例,BoxDemo,它需要Box类:

public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

以下是此示例的输出:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

泛型方法addBox定义了一个名为U的类型参数。通常,Java 编译器可以推断泛型方法调用的类型参数。因此,在大多数情况下,您不必指定它们。例如,要调用泛型方法addBox,您可以使用类型见证指定类型参数如下:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

另外,如果省略类型见证,Java 编译器会自动推断(从方法的参数中)类型参数为Integer

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

类型推断和泛型类的实例化

只要编译器可以从上下文中推断出类型参数,您可以用一组空类型参数(<>)替换调用泛型类构造函数所需的类型参数。这一对尖括号非正式地称为菱形。

例如,考虑以下变量声明:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

您可以用一组空类型参数(<>)替换构造函数的参数化类型:

Map<String, List<String>> myMap = new HashMap<>();

请注意,在泛型类实例化期间利用类型推断,必须使用菱形。在以下示例中,编译器生成了未经检查的转换警告,因为HashMap()构造函数引用了HashMap原始类型,而不是Map<String, List<String>>类型:

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

泛型和非泛型类的泛型构造函数的类型推断

请注意,构造函数可以是泛型的(换句话说,在泛型和非泛型类中声明自己的形式类型参数)。考虑以下示例:

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

考虑类MyClass的以下实例化:

new MyClass<Integer>("")

这个语句创建了参数化类型MyClass<Integer>的实例;语句明确为泛型类MyClass<X>的形式类型参数X指定了类型Integer。请注意,这个泛型类的构造函数包含一个形式类型参数T。编译器为这个泛型类的构造函数的形式类型参数T推断了类型String(因为这个构造函数的实际参数是一个String对象)。

Java SE 7 之前的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。然而,在 Java SE 7 及更高版本中,如果使用菱形(<>),编译器可以推断正在实例化的泛型类的实际类型参数。考虑以下例子:

MyClass<Integer> myObject = new MyClass<>("");

在这个例子中,编译器为泛型类MyClass<X>的形式类型参数X推断了类型Integer。它为这个泛型类的构造函数的形式类型参数T推断了类型String


**注意:**需要注意的是,推断算法仅使用调用参数、目标类型和可能的明显预期返回类型来推断类型。推断算法不使用程序后面的结果。


目标类型

Java 编译器利用目标类型推断泛型方法调用的类型参数。表达式的目标类型是 Java 编译器根据表达式出现的位置所期望的数据类型。考虑声明如下的方法Collections.emptyList

static <T> List<T> emptyList();

考虑以下赋值语句:

List<String> listOne = Collections.emptyList();

这个语句期望一个List<String>的实例;这个数据类型是目标类型。因为方法emptyList返回类型为List<T>的值,编译器推断类型参数T必须是值String。这在 Java SE 7 和 8 中都适用。或者,您可以使用类型推断并指定T的值如下:

List<String> listOne = Collections.<String>emptyList();

然而,在这种情况下并不是必需的。尽管在其他情况下是必需的。考虑以下方法:

void processStringList(List<String> stringList) {
    // process stringList
}

假设您想要使用空列表调用方法processStringList。在 Java SE 7 中,以下语句不会编译:

processStringList(Collections.emptyList());

Java SE 7 编译器生成类似以下的错误消息:

List<Object> cannot be converted to List<String>

编译器需要一个类型参数T的值,因此它从值Object开始。因此,调用Collections.emptyList返回一个类型为List<Object>的值,这与方法processStringList不兼容。因此,在 Java SE 7 中,您必须如下指定类型参数的值:

processStringList(Collections.<String>emptyList());

在 Java SE 8 中,这已经不再是必需的。什么是目标类型的概念已经扩展到包括方法参数,比如方法processStringList的参数。在这种情况下,processStringList需要一个类型为List<String>的参数。方法Collections.emptyList返回一个List<T>的值,因此使用List<String>的目标类型,编译器推断类型参数T的值为String。因此,在 Java SE 8 中,以下语句编译通过:

processStringList(Collections.emptyList());

查看目标类型在 Lambda 表达式中获取更多信息。

通配符

原文:docs.oracle.com/javase/tutorial/java/generics/wildcards.html

在泛型代码中,问号(?),称为通配符,表示未知类型。 通配符可以在各种情况下使用:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体的编程实践更好)。 通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

以下部分将更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。

上界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/upperBounded.html

您可以使用上界通配符来放宽对变量的限制。例如,假设您想编写一个适用于List<Integer>List<Double>List<Number>的方法;您可以通过使用上界通配符来实现这一点。

要声明上界通配符,请使用通配符字符(‘?’),后跟extends关键字,再跟其上界。请注意,在此上下文中,extends的含义是广义上的,既可以表示"extends"(如类)也可以表示"implements"(如接口)。

要编写适用于Number及其子类型(如IntegerDoubleFloat)的列表的方法,您应指定List<? extends Number>。术语List<Number>List<? extends Number>更为严格,因为前者仅匹配类型为Number的列表,而后者匹配类型为Number或其任何子类的列表。

考虑以下process方法:

public static void process(List<? extends Foo> list) { /* ... */ }

上界通配符<? extends Foo>,其中Foo是任何类型,匹配FooFoo的任何子类型。process方法可以将列表元素作为类型Foo访问:

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

foreach子句中,elem变量遍历列表中的每个元素。现在可以在elem上使用Foo类中定义的任何方法。

sumOfList方法返回列表中数字的总和:

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

使用Integer对象列表的以下代码打印sum = 6.0

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

一组Double值可以使用相同的sumOfList方法。以下代码打印sum = 7.0

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

无界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/unboundedWildcards.html

未限定通配符类型是使用通配符字符(?)指定的,例如,List<?>。这被称为未知类型的列表。有两种情况下未限定通配符是一个有用的方法:

  • 如果你正在编写一个可以使用Object类提供的功能来实现的方法。

  • 当代码使用泛型类中不依赖于类型参数的方法时。例如,List.sizeList.clear。事实上,Class<?>经常被使用,因为Class<T>中的大多数方法不依赖于T

考虑以下方法,printList

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>的子类型。要编写一个通用的printList方法,使用List<?>

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

因为对于任何具体类型AList<A>List<?>的子类型,所以你可以使用printList来打印任何类型的列表:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);


注意: 在本课程的示例中使用了Arrays.asList方法。这个静态工厂方法将指定的数组转换并返回一个固定大小的列表。


需要注意的是,List<Object>List<?>并不相同。你可以将ObjectObject的任何子类型插入List<Object>中。但你只能将null插入List<?>中。通配符使用指南部分有关于如何确定在特定情况下应该使用什么类型的通配符的更多信息。

下界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html

上界通配符 部分显示,上界通配符将未知类型限制为特定类型或该类型的子类型,并使用 extends 关键字表示。类似地,下界 通配符将未知类型限制为特定类型或该类型的 超类型

使用通配符字符(‘?’)表示下界通配符,后跟 super 关键字,再跟着其 下界<? super A>


注意: 你可以为通配符指定上界,也可以指定下界,但不能同时指定两者。


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

要编写适用于 Integer 列表和 Integer 的超类型(如 IntegerNumberObject)的方法,你应该指定 List<? super Integer>。术语 List<Integer>List<? super Integer> 更具限制性,因为前者仅匹配类型为 Integer 的列表,而后者匹配任何是 Integer 超类型的列表。

以下代码将数字 1 到 10 添加到列表的末尾:

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

通配符使用指南 部分提供了何时使用上界通配符和何时使用下界通配符的指导。

通配符和子类型

原文:docs.oracle.com/javase/tutorial/java/generics/subtyping.html

如泛型、继承和子类型中所述,泛型类或接口之间并不仅仅因为它们的类型之间存在关系而相关。但是,您可以使用通配符来创建泛型类或接口之间的关系。

给定以下两个常规(非泛型)类:

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

编写以下代码是合理的:

B b = new B();
A a = b;

此示例显示了常规类的继承遵循子类型规则:如果B扩展A,则类B是类A的子类型。这个规则不适用于泛型类型:

List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error

鉴于IntegerNumber的子类型,那么List<Integer>List<Number>之间的关系是什么?

显示 List和 List的共同父类是未知类型列表的图表共同父类是List<?>

尽管IntegerNumber的子类型,但List<Integer>不是List<Number>的子类型,实际上,这两种类型没有关系。List<Number>List<Integer>的共同父类是List<?>

为了创建这些类之间的关系,以便代码可以通过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>

因为IntegerNumber的子类型,而numListNumber对象的列表,现在intListInteger对象的列表)和numList之间存在关系。以下图表显示了使用上下界通配符声明的几个List类之间的关系。

显示 List是 List的子类型。List是 List的子类型。几个泛型List类声明的层次结构。

通配符使用指南部分有关于使用上下界通配符的更多信息。

通配符捕获和辅助方法

原文:docs.oracle.com/javase/tutorial/java/generics/capture.html

在某些情况下,编译器会推断通配符的类型。例如,一个列表可能被定义为List<?>,但在评估表达式时,编译器会从代码中推断出特定的类型。这种情况被称为通配符捕获

对于大多数情况,你不需要担心通配符捕获,除非你看到一个包含短语“capture of”的错误消息。

当编译时,WildcardError示例会产生一个捕获错误:

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 语言中的原因就是为了在编译时强制执行类型安全。

WildcardError示例在 Oracle 的 JDK 7 javac实现编译时生成以下错误:

WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
    i.set(0, i.get(0));
     ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

在这个例子中,代码正在尝试执行一个安全的操作,那么你如何解决编译器错误呢?你可以通过编写一个私有辅助方法来修复它,该方法捕获通配符。在这种情况下,你可以通过创建私有辅助方法fooHelper来解决问题,如WildcardFixed中所示:

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

}

多亏了辅助方法,编译器使用推断确定TCAP#1,即捕获变量,在调用中。现在的示例成功编译。

按照惯例,辅助方法通常被命名为*originalMethodName*Helper

现在考虑一个更复杂的例子,WildcardErrorBad

import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                            // got a CAP#2 extends Number;
                            // same bound, but different types
      l2.set(0, temp);	    // expected a CAP#1 extends Number,
                            // got a Number
    }
}

在这个例子中,代码正在尝试一个不安全的操作。例如,考虑swapFirst方法的以下调用:

List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);

虽然List<Integer>List<Double>都满足List<? extends Number>的条件,但从Integer值列表中取一个项目并尝试将其放入Double值列表中显然是不正确的。

使用 Oracle 的 JDK javac编译器编译代码会产生以下错误:

WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
      l2.set(0, temp);      // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
        i.set(0, i.get(0));
         ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
3 errors

没有辅助方法可以解决这个问题,因为代码本质上是错误的:从Integer值列表中取一个项目并尝试将其放入Double值列表中显然是不正确的。

通配符使用准则

原文:docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html

在学习如何使用泛型编程时,确定何时使用上界通配符和何时使用下界通配符是更令人困惑的方面之一。本页面提供了一些在设计代码时遵循的准则。

在本讨论中,将变量视为提供两种功能之一是有帮助的:

一个“In”变量

一个“in”变量向代码提供数据。想象一个带有两个参数的复制方法:copy(src, dest)src参数提供要复制的数据,因此它是“in”参数。

一个“Out”变量

一个“out”变量保存数据以供其他地方使用。在复制示例中,copy(src, dest)dest参数接受数据,因此它是“out”参数。

当然,有些变量既用于“in”又用于“out” —— 这种情况也在准则中有所涉及。

在决定是否使用通配符以及何种类型的通配符适合时,可以使用“in”和“out”原则。以下列表提供了要遵循的准则:


通配符准则:

  • 使用extends关键字定义具有上界通配符的“in”变量。

  • 使用super关键字定义具有下界通配符的“out”变量。

  • 在“in”变量可以使用在Object类中定义的方法访问的情况下,使用无界通配符。

  • 在代码需要将变量作为“in”和“out”变量访问的情况下,不要使用通配符。


这些准则不适用于方法的返回类型。应避免将通配符用作返回类型,因为这会强迫使用代码的程序员处理通配符。

List<? extends ...>定义的列表可以非正式地被视为只读的,但这并不是一个严格的保证。假设你有以下两个类:

class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}

class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}

考虑以下代码:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35));  // compile-time error

因为List<EvenNumber>List<? extends NaturalNumber>的子类型,所以你可以将le赋给ln。但你不能使用ln向偶数列表添加自然数。列表上的以下操作是可能的:

  • 你可以添加null

  • 你可以调用clear

  • 你可以获取迭代器并调用remove

  • 你可以捕获通配符并写入你从列表中读取的元素。

你可以看到由List<? extends NaturalNumber>定义的列表在严格意义上并不是只读的,但你可能会这样认为,因为你不能在列表中存储新元素或更改现有元素。

类型擦除

原文:docs.oracle.com/javase/tutorial/java/generics/erasure.html

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

  • 将泛型类型中的所有类型参数替换为它们的边界或Object(如果类型参数是无界的)。因此生成的字节码只包含普通类、接口和方法。

  • 如有必要,插入类型转换以保持类型安全。

  • 生成桥接方法以保留扩展泛型类型中的多态性。

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

泛型类型的擦除

原文:docs.oracle.com/javase/tutorial/java/generics/genTypes.html

在类型擦除过程中,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; }
    // ...
}

在下面的示例中,泛型Node类使用了有界类型参数:

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 编译器将有界类型参数T替换为第一个边界类Comparable

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

擦除通用方法

原文:docs.oracle.com/javase/tutorial/java/generics/genMethods.html

Java 编译器还会擦除通用方法参数中的类型参数。考虑以下通用方法:

// Counts the number of occurrences of elem in anArray.
//
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;
}

假设以下类已被定义:

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) { /* ... */ }

类型擦除和桥接方法的影响

原文:docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html

有时类型擦除会导致一个你可能没有预料到的情况。以下示例展示了这种情况是如何发生的。有时编译器会在类型擦除过程中创建一个合成方法,称为桥接方法

给定以下两个类:

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

考虑以下代码:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = mn.data;    

经过类型擦除后,这段代码变成:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
                        // Note: This statement could instead be the following:
                        //     Node n = (Node)mn;
                        // However, the compiler doesn't generate a cast because
                        // it isn't required.
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data; 

下一节将解释为什么在 n.setData("Hello"); 语句处抛出 ClassCastException

桥接方法

当编译一个继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。通常情况下,你不需要担心桥接方法,但如果在堆栈跟踪中出现一个,你可能会感到困惑。

经过类型擦除后,NodeMyNode 类变成:

public class Node {

    public Object data;

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

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

经过类型擦除后,方法签名不匹配;Node.setData(T) 方法变成了 Node.setData(Object)。因此,MyNode.setData(Integer) 方法不会覆盖 Node.setData(Object) 方法。

为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥接方法来确保子类型化按预期工作。

对于 MyNode 类,编译器为 setData 生成了以下桥接方法:

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

桥接方法 MyNode.setData(object) 委托给原始的 MyNode.setData(Integer) 方法。因此,n.setData("Hello"); 语句调用了 MyNode.setData(Object) 方法,由于 "Hello" 无法转换为 Integer,导致抛出 ClassCastException

非可实例化类型

原文:docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html

章节 类型擦除 讨论了编译器删除与类型参数和类型参数相关的信息的过程。类型擦除对于具有非可实例化类型的可变参数(也称为 varargs)方法有相关后果。有关可变参数方法的更多信息,请参见 传递信息给方法或构造函数 中的 任意数量的参数 章节。

本页涵盖以下主题:

  • 非可实例化类型

  • 堆污染

  • 具有非可实例化形式参数的可变参数方法的潜在漏洞

  • 防止具有非可实例化形式参数的可变参数方法产生警告

非可实例化类型

可实例化 类型是一种在运行时完全可用的类型信息的类型。这包括原始类型、非泛型类型、原始类型和未绑定通配符的调用。

非可实例化类型 是在编译时通过类型擦除删除了信息的类型 —— 未定义为未限定通配符的泛型类型的调用。非可实例化类型在运行时不具备所有信息。非可实例化类型的示例包括 List<String>List<Number>;JVM 无法在运行时区分这些类型。如 泛型的限制 所示,有一些情况下不能使用非可实例化类型:例如,在 instanceof 表达式中,或作为数组中的元素。

堆污染

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

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

具有非可实例化形式参数的可变参数方法的潜在漏洞

包含可变参数输入参数的泛型方法可能导致堆污染。

考虑以下ArrayBuilder类:

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
  }

}

以下示例HeapPollutionExample使用了ArrayBuiler类:

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!"));
  }
}

编译时,ArrayBuilder.addToList方法的定义会产生以下警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

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

以下语句将可变参数形式参数l赋给Object数组objectArgs

Object[] objectArray = l;

这个语句可能会引入堆污染。一个与可变参数形式参数l的参数化类型不匹配的值可以赋给变量objectArray,从而可以赋给l。然而,在这个语句中,编译器并不生成未经检查的警告。编译器在将可变参数形式参数List<String>... l翻译为形式参数List[] l时已经生成了警告。这个语句是有效的;变量l的类型是List[],它是Object[]的子类型。

因此,如果您将任何类型的List对象分配给objectArray数组的任何数组组件,编译器不会发出警告或错误,如下所示:

objectArray[0] = Arrays.asList(42);

这个语句将包含一个类型为Integer的对象的List对象分配给objectArray数组的第一个数组组件。

假设您使用以下语句调用ArrayBuilder.faultyMethod

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

在运行时,JVM 在以下语句处抛出ClassCastException

// ClassCastException thrown here
String s = l[0].get(0);

存储在变量l的第一个数组组件中的对象的类型是List<Integer>,但这个语句期望的是类型为List<String>的对象。

防止具有非可重复形式参数的可变参数方法产生警告

如果您声明一个具有参数化类型参数的可变参数方法,并确保方法体不会因为对可变参数形式参数的不当处理而抛出ClassCastException或其他类似异常,您可以通过在静态和非构造方法声明中添加以下注解来防止编译器为这些类型的可变参数方法生成警告:

@SafeVarargs

@SafeVarargs注解是方法契约的一部分;此注解断言方法的实现不会不当处理可变参数形式参数。

也可以通过在方法声明中添加以下内容来抑制此类警告,尽管这种做法不太理想:

@SuppressWarnings({"unchecked", "varargs"})

然而,这种方法并不会抑制从方法调用点生成的警告。如果你对@SuppressWarnings语法不熟悉,请参见 Annotations。

泛型的限制。

原文:docs.oracle.com/javase/tutorial/java/generics/restrictions.html

要有效地使用 Java 泛型,您必须考虑以下限制:

  • 不能用原始类型实例化泛型类型。

  • 不能创建类型参数的实例。

  • 不能声明其类型为类型参数的静态字段。

  • 不能在参数化类型中使用强制类型转换或instanceof

  • 不能创建参数化类型的数组。

  • 不能创建、捕获或抛出参数化类型的对象。

  • 不能重载形式参数类型擦除为相同原始类型的方法。

不能用原始类型实例化泛型类型。

考虑以下参数化类型:

class Pair<K, V> {

    private K key;
    private V value;

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

    // ...
}

在创建Pair对象时,您不能用原始类型替换类型参数KV

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

您只能用非原始类型替换类型参数KV

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

请注意,Java 编译器会将8自动装箱为Integer.valueOf(8),将'a'自动装箱为Character('a')

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

有关自动装箱的更多信息,请参见自动装箱和拆箱中的数字和字符串课程。

不能创建类型参数的实例。

你不能创建一个类型参数的实例。例如,以下代码会导致编译时错误:

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

不能声明其类型为类型参数的静态字段。

类的静态字段是所有非静态对象共享的类级变量。因此,不允许类型参数的静态字段。考虑以下类:

public class MobileDevice<T> {
    private static T os;

    // ...
}

如果允许类型参数的静态字段,则以下代码将会混淆:

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

因为静态字段osphonepagerpc共享,os的实际类型是什么?它不能同时是SmartphonePagerTabletPC。因此,您不能创建类型参数的静态字段。

不能在参数化类型中使用强制类型转换或instanceof

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

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

传递给rtti方法的参数化类型集合为:

S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }

运行时不会跟踪类型参数,因此无法区分ArrayList<Integer>ArrayList<String>之间的区别。您最多可以使用无界通配符来验证列表是否是ArrayList

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

通常情况下,除非使用无界通配符进行参数化,否则不能进行参数化类型的强制转换。例如:

List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error

但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:

List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1;  // OK

不能创建参数化类型的数组。

你不能创建参数化类型的数组。例如,以下代码无法编译:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

以下代码说明了当不同类型插入数组时会发生什么:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

如果你尝试对一个泛型列表做同样的事情,会出现问题:

Object[] stringLists = new List<String>[2];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

如果允许参数化列表的数组,上述代码将无法抛出期望的ArrayStoreException

无法创建、捕获或抛出参数化类型的对象

一个泛型类不能直接或间接地扩展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
        // ...
    }
}

但是,你可以在throws子句中使用类型参数:

class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}

无法重载形式参数类型擦除为相同原始类型的方法

一个类不能有两个在类型擦除后具有相同签名的重载方法。

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

这些重载将共享相同的类文件表示,并将生成编译时错误。

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值