Java 中文官方教程 2022 版(六)(1)https://developer.aliyun.com/article/1486307
有界类型参数
原文:
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
接口限定的类型参数:
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
的单个参数。但这意味着什么?你可以传入 Box
或 Box
吗,正如你可能期望的那样?答案是“不可以”,因为 Box
和 Box
不是 Box
的子类型。
这是在使用泛型进行编程时的一个常见误解,但这是一个重要的概念需要学习。
Box
不是 Box
的子类型,即使 Integer
是 Number
的子类型。
**注意:**给定两个具体类型 A
和 B
(例如,Number
和 Integer
),MyClass
与 MyClass
没有关系,无论 A
和 B
是否相关。MyClass
和
MyClass
的共同父类是 Object
。
有关如何在类型参数相关的情况下创建两个泛型类之间类似子类型的关系的信息,请参阅通配符和子类型。
通用类和子类型
你可以通过扩展或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extends
和 implements
子句确定。
以 Collections
类为例,ArrayList
实现了 List
,而 List
扩展了 Collection
。因此,ArrayList
是 List
的子类型,List
是 Collection
的子类型。只要不改变类型参数,类型之间的子类型关系就会保持不变。
一个示例 Collections
层次结构
现在想象一下,我们想要定义自己的列表接口,PayloadList
,它将泛型类型P
的可选值与每个元素关联起来。它的声明可能如下所示:
interface PayloadList<E,P> extends List<E> { void setPayload(int index, P val); ... }
以下对PayloadList
的参数化是List
的子类型:
PayloadList
PayloadList
PayloadList
一个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>
类型:
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
泛型和非泛型类的泛型构造函数的类型推断
请注意,构造函数可以是泛型的(换句话说,在泛型和非泛型类中声明自己的形式类型参数)。考虑以下示例:
class MyClass<X> { <T> MyClass(T t) { // ... } }
考虑类MyClass
的以下实例化:
new MyClass<Integer>("")
这个语句创建了参数化类型MyClass
的实例;语句明确为泛型类MyClass
的形式类型参数X
指定了类型Integer
。请注意,这个泛型类的构造函数包含一个形式类型参数T
。编译器为这个泛型类的构造函数的形式类型参数T
推断了类型String
(因为这个构造函数的实际参数是一个String
对象)。
Java SE 7 之前的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。然而,在 Java SE 7 及更高版本中,如果使用菱形(<>
),编译器可以推断正在实例化的泛型类的实际类型参数。考虑以下例子:
MyClass<Integer> myObject = new MyClass<>("");
在这个例子中,编译器为泛型类MyClass
的形式类型参数X
推断了类型Integer
。它为这个泛型类的构造函数的形式类型参数T
推断了类型String
。
**注意:**需要注意的是,推断算法仅使用调用参数、目标类型和可能的明显预期返回类型来推断类型。推断算法不使用程序后面的结果。
目标类型
Java 编译器利用目标类型推断泛型方法调用的类型参数。表达式的目标类型是 Java 编译器根据表达式出现的位置所期望的数据类型。考虑声明如下的方法Collections.emptyList
:
static <T> List<T> emptyList();
考虑以下赋值语句:
List<String> listOne = Collections.emptyList();
这个语句期望一个List
的实例;这个数据类型是目标类型。因为方法emptyList
返回类型为List
的值,编译器推断类型参数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的值,这与方法
processStringList
不兼容。因此,在 Java SE 7 中,您必须如下指定类型参数的值:
processStringList(Collections.<String>emptyList());
在 Java SE 8 中,这已经不再是必需的。什么是目标类型的概念已经扩展到包括方法参数,比如方法
processStringList
的参数。在这种情况下,processStringList
需要一个类型为List
的参数。方法Collections.emptyList
返回一个List
的值,因此使用List
的目标类型,编译器推断类型参数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
、List
和List
的方法;您可以通过使用上界通配符来实现这一点。
要声明上界通配符,请使用通配符字符(‘
?
’),后跟extends
关键字,再跟其上界。请注意,在此上下文中,extends
的含义是广义上的,既可以表示"extends"(如类)也可以表示"implements"(如接口)。
要编写适用于
Number
及其子类型(如Integer
、Double
和Float
)的列表的方法,您应指定List
。术语List
比List
更为严格,因为前者仅匹配类型为Number
的列表,而后者匹配类型为Number
或其任何子类的列表。
考虑以下
process
方法:
public static void process(List<? extends Foo> list) { /* ... */ } • 1 • 2
上界通配符
,其中
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
。
考虑以下方法,
printList
:
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println(); }
printList
的目标是打印任何类型的列表,但它未能实现这个目标 — 它只打印Object
实例的列表;它无法打印List
、List
、List
等,因为它们不是List的子类型。要编写一个通用的
printList
方法,使用List
:
public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + " "); System.out.println(); }
因为对于任何具体类型
A
,List
是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和
List
并不相同。你可以将Object
或Object
的任何子类型插入List中。但你只能将
null
插入List
中。通配符使用指南部分有关于如何确定在特定情况下应该使用什么类型的通配符的更多信息。
Java 中文官方教程 2022 版(六)(3)https://developer.aliyun.com/article/1486309