Java 中文官方教程 2022 版(六)
字符和字符串总结
原文:
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
中,但代码仍然可以编译。因为li
是Integer
对象的列表,而不是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 type | Wrapper class |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
问题和练习:字符和字符串
原文:
docs.oracle.com/javase/tutorial/java/data/QandE/characters-questions.html
问题
-
以下字符串构建器的初始容量是多少?
StringBuilder sb = new StringBuilder("Able was I ere I saw Elba.");
-
考虑以下字符串:
String hannah = "Did Hannah see bees? Hannah did.";
-
表达式
hannah.length()
显示的值是多少? -
方法调用
hannah.charAt(12)
返回的值是多少? -
编写一个表达式,引用
hannah
所指的字符串中的字母b
。
-
-
以下表达式返回的字符串有多长?这个字符串是什么?
"Was it a car or a cat I saw?".substring(9, 12)
-
在下面的程序中,名为
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); } }
练习
-
展示两种方法将以下两个字符串连接在一起以得到字符串
"Hi, mom."
:String hi = "Hi, "; String mom = "mom.";
-
编写一个程序,从你的全名中计算出你的缩写并显示出来。
-
一个变位词是由另一个单词或短语的字母重新排列而成的单词或短语;例如,“parliament”是“partial men”的变位词,“software”是“swear oft”的变位词。编写一个程序,判断一个字符串是否是另一个字符串的变位词。该程序应忽略空格和标点符号。
检查你的答案。
Lesson: 泛型(更新)
在任何非平凡的软件项目中,错误都是生活中不可避免的事实。仔细的规划、编程和测试可以帮助减少它们的普遍性,但不知何故,它们总会找到一种方式悄悄地潜入你的代码中。随着新功能的引入和代码库规模与复杂性的增长,这一点变得尤为明显。
幸运的是,有些错误比其他错误更容易检测。例如,编译时错误可以在早期被检测出来;你可以利用编译器的错误消息来找出问题所在并立即修复它。然而,运行时错误可能会更加棘手;它们并不总是立即显现,而且当它们出现时,可能是在程序中与问题实际原因相距甚远的地方。
泛型通过在编译时使更多的错误可检测,为你的代码增加了稳定性。完成本课程后,你可能想继续学习 Gilad Bracha 的《泛型》教程。
为什么使用泛型?
简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。就像在方法声明中使用的更熟悉的形式参数一样,类型参数提供了一种方式让您可以重复使用相同的代码以不同的输入。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。
使用泛型的代码比不使用泛型的代码有许多好处:
-
编译时进行更强的类型检查。
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
-
使程序员能够实现泛型算法。
通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以进行定制,并且是类型安全且更易阅读的。
泛型类型
泛型类型是一个参数化类型的泛型类或接口。下面的 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> { /* ... */ }
类型参数部分,由尖括号(<>
)界定,跟在类名后面。它指定了类型参数(也称为类型变量)T1
、T2
、… 和 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>
实例化K
为String
,V
为Integer
。因此,OrderedPair
的构造函数的参数类型分别为String
和Integer
。由于自动装箱,将String
和int
传递给类是有效的。
如钻石中所述,因为 Java 编译器可以从声明OrderedPair<String, Integer>
中推断出K
和V
类型,所以可以使用钻石符号缩短这些语句:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
要创建泛型接口,遵循与创建泛型类相同的约定。
参数化类型
您还可以用参数化类型(即List<String>
)替换类型参数(即K
或V
)。例如,使用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;
}
该方法的实现很简单,但它无法编译,因为大于运算符(>
)仅适用于原始类型,如short
、int
、double
、long
、float
、byte
和char
。你不能使用>
运算符来比较对象。为了解决这个问题,使用一个由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
,因为 Object
是 Integer
的超类型之一:
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<Integer>
不是 Box<Number>
的子类型,即使 Integer
是 Number
的子类型。
**注意:**给定两个具体类型 A
和 B
(例如,Number
和 Integer
),MyClass<A>
与 MyClass<B>
没有关系,无论 A
和 B
是否相关。MyClass<A>
和 MyClass<B>
的共同父类是 Object
。
有关如何在类型参数相关的情况下创建两个泛型类之间类似子类型的关系的信息,请参阅通配符和子类型。
通用类和子类型
你可以通过扩展或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extends
和 implements
子句确定。
以 Collections
类为例,ArrayList<E>
实现了 List<E>
,而 List<E>
扩展了 Collection<E>
。因此,ArrayList<String>
是 List<String>
的子类型,List<String>
是 Collection<String>
的子类型。只要不改变类型参数,类型之间的子类型关系就会保持不变。
一个示例 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
层次结构示例
类型推断
原文:
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
及其子类型(如Integer
、Double
和Float
)的列表的方法,您应指定List<? extends Number>
。术语List<Number>
比List<? extends Number>
更为严格,因为前者仅匹配类型为Number
的列表,而后者匹配类型为Number
或其任何子类的列表。
考虑以下process
方法:
public static void process(List<? extends Foo> list) { /* ... */ }
上界通配符<? extends Foo>
,其中Foo
是任何类型,匹配Foo
和Foo
的任何子类型。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.size
或List.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();
}
因为对于任何具体类型A
,List<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<?>
并不相同。你可以将Object
或Object
的任何子类型插入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
的超类型(如 Integer
、Number
和 Object
)的方法,你应该指定 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
鉴于Integer
是Number
的子类型,那么List<Integer>
和List<Number>
之间的关系是什么?
共同父类是List<?>
。
尽管Integer
是Number
的子类型,但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>
因为Integer
是Number
的子类型,而numList
是Number
对象的列表,现在intList
(Integer
对象的列表)和numList
之间存在关系。以下图表显示了使用上下界通配符声明的几个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));
}
}
多亏了辅助方法,编译器使用推断确定T
是CAP#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
。
桥接方法
当编译一个继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。通常情况下,你不需要担心桥接方法,但如果在堆栈跟踪中出现一个,你可能会感到困惑。
经过类型擦除后,Node
和 MyNode
类变成:
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
对象时,您不能用原始类型替换类型参数K
或V
:
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
您只能用非原始类型替换类型参数K
和V
:
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<>();
因为静态字段os
被phone
、pager
和pc
共享,os
的实际类型是什么?它不能同时是Smartphone
、Pager
和TabletPC
。因此,您不能创建类型参数的静态字段。
不能在参数化类型中使用强制类型转换或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) { }
}
这些重载将共享相同的类文件表示,并将生成编译时错误。
原文地址:https://blog.csdn.net/wizardforcel/article/details/137679002
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!