本节将阐述在使用 Java 泛型时会出现的各类问题。
正如本章早先提到的,Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建 ArrayList
之类的东西。
解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList
,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 int 到 Integer 的双向转换——因此,这几乎就像是有一个 ArrayList
一样:
import java.util.*;
import java.util.stream.*;
public class ListOfInt {
public static void main(String[] args) {
List<Integer> li = IntStream.range(38, 48)
.boxed() // Converts ints to Integers
.collect(Collectors.toList());
System.out.println(li);
}
}
通常,这种解决方案工作得很好——能够成功地存储和读取 int,自动装箱隐藏了转换的过程。但是如果性能成为问题的话,就需要使用专门为基本类型适配的特殊版本的集合;一个开源版本的实现是 org.apache.commons.collections.primitives。
下面是另外一种方式,它可以创建持有 Byte 的 Set:
import java.util.*;
public class ByteSet {
Byte[] possibles = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Set<Byte> mySet = new HashSet<>(Arrays.asList(possibles));
// But you can't do this:
// Set mySet2 = new HashSet<>(
// Arrays.asList(1,2,3,4,5,6,7,8,9));
}
自动装箱机制解决了一些问题,但并没有解决所有问题。
在下面的示例中,FillArray 接口包含一些通用方法,这些方法使用 Supplier 来用对象填充数组(这使得类泛型在本例中无法工作,因为这个方法是静态的)。Supplier 实现来自 数组 一章,并且在 main()
中,可以看到 FillArray.fill()
使用对象填充了数组:
FillArray.java
import java.util.*;
import java.util.function.*;
// Fill an array using a generator:
interface FillArray {
static <T> T[] fill(T[] a, Supplier<T> gen) {
Arrays.setAll(a, n -> gen.get());
return a;
}
static int[] fill(int[] a, IntSupplier gen) {
Arrays.setAll(a, n -> gen.getAsInt());
return a;
}
static long[] fill(long[] a, LongSupplier gen) {
Arrays.setAll(a, n -> gen.getAsLong());
return a;
}
static double[] fill(double[] a, DoubleSupplier gen) {
Arrays.setAll(a, n -> gen.getAsDouble());
return a;
}
}
public class PrimitiveGenericTest {
public static void main(String[] args) {
String[] strings = FillArray.fill(
new String[5], new Rand.String(9));
System.out.println(Arrays.toString(strings));
int[] integers = FillArray.fill(
new int[9], new Rand.Pint());
System.out.println(Arrays.toString(integers));
}
}
ConvertTo.java
public interface ConvertTo {
static boolean[] primitive(Boolean[] in) {
boolean[] result = new boolean[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i]; // Autounboxing
}
return result;
}
static char[] primitive(Character[] in) {
char[] result = new char[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static byte[] primitive(Byte[] in) {
byte[] result = new byte[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static short[] primitive(Short[] in) {
short[] result = new short[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static int[] primitive(Integer[] in) {
int[] result = new int[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static long[] primitive(Long[] in) {
long[] result = new long[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static float[] primitive(Float[] in) {
float[] result = new float[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static double[] primitive(Double[] in) {
double[] result = new double[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
// Convert from primitive array to wrapped array:
static Boolean[] boxed(boolean[] in) {
Boolean[] result = new Boolean[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i]; // Autoboxing
}
return result;
}
static Character[] boxed(char[] in) {
Character[] result = new Character[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static Byte[] boxed(byte[] in) {
Byte[] result = new Byte[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static Short[] boxed(short[] in) {
Short[] result = new Short[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static Integer[] boxed(int[] in) {
Integer[] result = new Integer[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static Long[] boxed(long[] in) {
Long[] result = new Long[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static Float[] boxed(float[] in) {
Float[] result = new Float[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
static Double[] boxed(double[] in) {
Double[] result = new Double[in.length];
for (int i = 0; i < in.length; i++) {
result[i] = in[i];
}
return result;
}
}
Rand.java
import java.util.*;
import java.util.function.*;
import static com.example.test.ConvertTo.primitive;
public interface Rand {
int MOD = 10_000;
class Boolean implements Supplier<java.lang.Boolean> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Boolean get() {
return r.nextBoolean();
}
public java.lang.Boolean get(int n) {
return get();
}
public java.lang.Boolean[] array(int sz) {
java.lang.Boolean[] result =
new java.lang.Boolean[sz];
Arrays.setAll(result, n -> get());
return result;
}
}
class Pboolean {
public boolean[] array(int sz) {
return primitive(new Boolean().array(sz));
}
}
class Byte
implements Supplier<java.lang.Byte> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Byte get() {
return (byte) r.nextInt(MOD);
}
public java.lang.Byte get(int n) {
return get();
}
public java.lang.Byte[] array(int sz) {
java.lang.Byte[] result =
new java.lang.Byte[sz];
Arrays.setAll(result, n -> get());
return result;
}
}
class Pbyte {
public byte[] array(int sz) {
return primitive(new Byte().array(sz));
}
}
class Character
implements Supplier<java.lang.Character> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Character get() {
return (char) r.nextInt('a', 'z' + 1);
}
public java.lang.Character get(int n) {
return get();
}
public java.lang.Character[] array(int sz) {
java.lang.Character[] result =
new java.lang.Character[sz];
Arrays.setAll(result, n -> get());
return result;
}
}
class Pchar {
public char[] array(int sz) {
return primitive(new Character().array(sz));
}
}
class Short
implements Supplier<java.lang.Short> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Short get() {
return (short) r.nextInt(MOD);
}
public java.lang.Short get(int n) {
return get();
}
public java.lang.Short[] array(int sz) {
java.lang.Short[] result =
new java.lang.Short[sz];
Arrays.setAll(result, n -> get());
return result;
}
}
class Pshort {
public short[] array(int sz) {
return primitive(new Short().array(sz));
}
}
class Integer
implements Supplier<java.lang.Integer> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Integer get() {
return r.nextInt(MOD);
}
public java.lang.Integer get(int n) {
return get();
}
public java.lang.Integer[] array(int sz) {
int[] primitive = new Pint().array(sz);
java.lang.Integer[] result = new java.lang.Integer[sz];
for (int i = 0; i < sz; i++) {
result[i] = primitive[i];
}
return result;
}
}
class Pint implements IntSupplier {
SplittableRandom r = new SplittableRandom(47);
@Override
public int getAsInt() {
return r.nextInt(MOD);
}
public int get(int n) {
return getAsInt();
}
public int[] array(int sz) {
return r.ints(sz, 0, MOD).toArray();
}
}
class Long
implements Supplier<java.lang.Long> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Long get() {
return r.nextLong(MOD);
}
public java.lang.Long get(int n) {
return get();
}
public java.lang.Long[] array(int sz) {
long[] primitive = new Plong().array(sz);
java.lang.Long[] result =
new java.lang.Long[sz];
for (int i = 0; i < sz; i++) {
result[i] = primitive[i];
}
return result;
}
}
class Plong implements LongSupplier {
SplittableRandom r = new SplittableRandom(47);
@Override
public long getAsLong() {
return r.nextLong(MOD);
}
public long get(int n) {
return getAsLong();
}
public long[] array(int sz) {
return r.longs(sz, 0, MOD).toArray();
}
}
class Float
implements Supplier<java.lang.Float> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Float get() {
return (float) trim(r.nextDouble());
}
public java.lang.Float get(int n) {
return get();
}
public java.lang.Float[] array(int sz) {
java.lang.Float[] result =
new java.lang.Float[sz];
Arrays.setAll(result, n -> get());
return result;
}
}
class Pfloat {
public float[] array(int sz) {
return primitive(new Float().array(sz));
}
}
static double trim(double d) {
return
((double) Math.round(d * 1000.0)) / 100.0;
}
class Double
implements Supplier<java.lang.Double> {
SplittableRandom r = new SplittableRandom(47);
@Override
public java.lang.Double get() {
return trim(r.nextDouble());
}
public java.lang.Double get(int n) {
return get();
}
public java.lang.Double[] array(int sz) {
double[] primitive = new Rand.Pdouble().array(sz);
java.lang.Double[] result = new java.lang.Double[sz];
for (int i = 0; i < sz; i++) {
result[i] = primitive[i];
}
return result;
}
}
class Pdouble implements DoubleSupplier {
SplittableRandom r = new SplittableRandom(47);
@Override
public double getAsDouble() {
return trim(r.nextDouble());
}
public double get(int n) {
return getAsDouble();
}
public double[] array(int sz) {
double[] result = r.doubles(sz).toArray();
Arrays.setAll(result,
n -> result[n] = trim(result[n]));
return result;
}
}
class String
implements Supplier<java.lang.String> {
SplittableRandom r = new SplittableRandom(47);
private int strlen = 7; // Default length
public String() {
}
public String(int strLength) {
strlen = strLength;
}
@Override
public java.lang.String get() {
return r.ints(strlen, 'a', 'z' + 1)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append).toString();
}
public java.lang.String get(int n) {
return get();
}
public java.lang.String[] array(int sz) {
java.lang.String[] result =
new java.lang.String[sz];
Arrays.setAll(result, n -> get());
return result;
}
}
}
自动装箱不适用于数组,因此我们必须创建 FillArray.fill()
的重载版本,或创建产生 Wrapped 输出的生成器。 FillArray 仅比 java.util.Arrays.setAll()
有用一点,因为它返回填充的数组。
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:
interface Payable<T> {
}
class Employee implements Payable<Employee> {
}
class Hourly extends Employee implements Payable<Hourly> {
}
Hourly 不能编译,因为擦除会将 Payable
和 Payable
简化为相同的类 Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 Payable 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译。
在使用某些更基本的 Java 接口,例如 Comparable
时,这个问题可能会变得十分令人恼火,就像你在本节稍后看到的那样。
使用带有泛型类型参数的转型或 instanceof 不会有任何效果。下面的集合在内部将各个值存储为 Object,并在获取这些值时,再将它们转型回 T:
import java.util.*;
import java.util.stream.*;
class FixedSizeStack<T> {
private final int size;
private Object[] storage;
private int index = 0;
FixedSizeStack(int size) {
this.size = size;
storage = new Object[size];
}
public void push(T item) {
if (index < size) {
storage[index++] = item;
}
}
@SuppressWarnings("unchecked")
public T pop() {
return index == 0 ? null : (T) storage[--index];
}
@SuppressWarnings("unchecked")
Stream<T> stream() {
return (Stream<T>) Arrays.stream(storage);
}
}
public class GenericCast {
static String[] letters = "ABCDEFGHIJKLMNOPQRS".split("");
public static void main(String[] args) {
FixedSizeStack<String> strings = new FixedSizeStack<>(letters.length);
Arrays.stream("ABCDEFGHIJKLMNOPQRS".split("")).forEach(strings::push);
System.out.println(strings.pop());
strings.stream()
.map(s -> s + " ")
.forEach(System.out::print);
}
}
如果没有 **@SuppressWarnings ** 注解,编译器将对 pop()
产生 “unchecked cast” 警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且 pop()
方法实际上并没有执行任何转型。
这是因为,T 被擦除到它的第一个边界,默认情况下是 Object ,因此 pop()
实际上只是将 Object 转型为 Object。
有时,泛型没有消除对转型的需要,这就会由编译器产生警告,而这个警告是不恰当的。例如:
NeedCasting.java
import java.io.*;
public class NeedCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
List<Widget> shapes = (List<Widget>) in.readObject();
}
}
FactoryConstraint.java
import java.util.*;
import java.util.function.*;
class IntegerFactory implements Supplier<Integer> {
private int i = 0;
@Override
public Integer get() {
return ++i;
}
}
class Widget {
private int id;
Widget(int n) {
id = n;
}
@Override
public String toString() {
return "Widget " + id;
}
public static
class Factory implements Supplier<Widget> {
private int i = 0;
@Override
public Widget get() {
return new Widget(++i);
}
}
}
class Fudge {
private static int count = 1;
private int n = count++;
@Override
public String toString() {
return "Fudge " + n;
}
}
class Foo2<T> {
private List<T> x = new ArrayList<>();
Foo2(Supplier<T> factory) {
Suppliers.fill(x, factory, 5);
}
@Override
public String toString() {
return x.toString();
}
}
public class FactoryConstraint {
public static void main(String[] args) {
System.out.println(
new Foo2<>(new IntegerFactory()));
System.out.println(
new Foo2<>(new Widget.Factory()));
System.out.println(
new Foo2<>(Fudge::new));
}
}
Suppliers.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Suppliers {
// Create a collection and fill it:
public static <T, C extends Collection<T>> C
create(Supplier<C> factory, Supplier<T> gen, int n) {
return Stream.generate(gen)
.limit(n)
.collect(factory, C::add, C::addAll);
}
// Fill an existing collection:
public static <T, C extends Collection<T>>
C fill(C coll, Supplier<T> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(coll::add);
return coll;
}
// Use an unbound method reference to
// produce a more general method:
public static <H, A> H fill(H holder,
BiConsumer<H, A> adder, Supplier<A> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(a -> adder.accept(holder, a));
return holder;
}
}
readObject()
无法知道它正在读取的是什么,因此它返回的是必须转型的对象。但是当注释掉 **@SuppressWarnings ** 注解并编译这个程序时,就会得到下面的警告。
NeedCasting.java uses unchecked or unsafe operations.
Recompile with -Xlint:unchecked for details.
And if you follow the instructions and recompile with -
Xlint:unchecked :(如果遵循这条指示,使用-Xlint:unchecked来重新编译:)
NeedCasting.java:10: warning: [unchecked] unchecked cast
List shapes = (List)in.readObject();
required: List
found: Object
1 warning
你会被强制要求转型,但是又被告知不应该转型。为了解决这个问题,必须使用 Java 5 引入的新的转型形式,即通过泛型类来转型:
import java.io.*;
import java.util.*;
public class ClassCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
// Won't Compile:
// List lw1 =
// List<>.class.cast(in.readObject());
List<Widget> lw2 = List.class.cast(in.readObject());
}
}
但是,不能转型到实际类型( List
)。也就是说,不能声明:
List.class.cast(in.readobject())
甚至当你添加一个像下面这样的另一个转型时:
(List)List.class.cast(in.readobject())
仍旧会得到一个警告。
下面的程序是不能编译的,即使它看起来是合理的:
import java.util.*;
public class UseList<W, T> {
void f(List<T> v) {
}
void f(List<W> v) {
}
}
因为擦除,所以重载方法产生了相同的类型签名。
因而,当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名:
import java.util.*;
public class UseList2<W, T> {
void f1(List<T> v) {
}
void f2(List<W> v) {
}
}
幸运的是,编译器可以检测到这类问题。
假设你有一个实现了 Comparable 接口的 Pet 类:
public class ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
尝试缩小 ComparablePet 子类的比较类型是有意义的。例如,Cat 类可以与其他的 Cat 比较:
class Cat extends ComparablePet implements Comparable<Cat> {
// error: Comparable cannot be inherited with
// different arguments: and
// class Cat
// ^
// 1 error
public int compareTo(Cat arg) {
return 0;
}
}
不幸的是,这不能工作。一旦 Comparable 的类型参数设置为 ComparablePet,其他的实现类只能比较 ComparablePet:
public class Hamster extends ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet arg) {
return 0;
}
}
// Or just:
class Gecko extends ComparablePet {
@Override
public int compareTo(ComparablePet arg) {
return 0;
}
}
Hamster 显示了重新实现 ComparablePet 中相同的接口是可能的,只要接口完全相同,包括参数类型。然而正如 Gecko 中所示,这与直接覆写基类的方法完全相同。
在 Java 泛型中,有一个似乎经常性出现的惯用法,它相当令人费解:
class SelfBounded<T extends SelfBounded<T>> { // ...
}
这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。SelfBounded 类接受泛型参数 T,而 T 由一个边界类限定,这个边界就是拥有 T 作为其参数的 SelfBounded。
当你首次看到它时,很难去解析它,它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。
为了理解自限定类型的含义,我们从这个惯用法的一个简单版本入手,它没有自限定的边界。
不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类。也就是说,可以声明:
class GenericType<T> {
}
public class CuriouslyRecurringGeneric extends GenericType<CuriouslyRecurringGeneric> {
}
这可以按照 Jim Coplien 在 C++ 中的_古怪的循环模版模式_的命名方式,称为古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。
为了理解其含义,努力大声说:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”
当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java 中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 Object 的类型。下面是表示了这种情况的一个泛型类:
public class BasicHolder<T> {
T element;
void set(T arg) {
element = arg;
}
T get() {
return element;
}
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
这是一个普通的泛型类型,它的一些方法将接受和产生具有其参数类型的对象,还有一个方法在其存储的域上执行操作(尽管只是在这个域上执行 Object 操作)。
我们可以在一个古怪的循环泛型中使用 BasicHolder:
class Subtype extends BasicHolder<Subtype> {
}
public class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype st1 = new Subtype(), st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
}
注意,这里有些东西很重要:新类 Subtype 接受的参数和返回的值具有 Subtype 类型而不仅仅是基类 BasicHolder 类型。这就是 CRG 的本质:基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。
也就是说,在所产生的类中将使用确切类型而不是基类型。因此,在Subtype 中,传递给 set()
的参数和从 get()
返回的类型都是确切的 Subtype。
BasicHolder 可以使用任何类型作为其泛型参数,就像下面看到的那样:
class Other {
}
class BasicOther extends BasicHolder<Other> {
}
public class Unconstrained {
public static void main(String[] args) {
BasicOther b = new BasicOther();
BasicOther b2 = new BasicOther();
b.set(new Other());
Other other = b.get();
b.f();
}
}
限定将采取额外的步骤,强制泛型当作其自身的边界参数来使用。观察所产生的类可以如何使用以及不可以如何使用:
class SelfBounded<T extends SelfBounded<T>> {
T element;
SelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() {
return element;
}
}
class A extends SelfBounded<A> {
}
class B extends SelfBounded<A> {
} // Also OK
class C extends SelfBounded<C> {
C setAndGet(C arg) {
set(arg);
return get();
}
}
class D {
}
// Can't do this:
// class E extends SelfBounded {}
// Compile error:
// Type parameter D is not within its bound
// Alas, you can do this, so you cannot force the idiom:
class F extends SelfBounded {
}
public class SelfBounding {
public static void main(String[] args) {
A a = new A();
a.set(new A());
a = a.set(new A()).get();
a = a.get();
C c = new C();
c = c.setAndGet(new C());
}
}
自限定所做的,就是要求在继承关系中,像下面这样使用这个类:
class A extends SelfBounded<A>{}
这会强制要求将正在定义的类当作参数传递给基类。
自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在 B 类的定义中所看到的,还可以从使用了另一个 SelfBounded 参数的 SelfBounded 中导出,尽管在 A 类看到的用法看起来是主要的用法。对定义 E 的尝试说明不能使用不是 SelfBounded 的类型参数。
遗憾的是, F 可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。
注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是 E 也会因此而变得可编译:
public class NotSelfBounded<T> {
T element;
NotSelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() {
return element;
}
}
class A2 extends NotSelfBounded<A2> {
}
class B2 extends NotSelfBounded<A2> {
}
class C2 extends NotSelfBounded<C2> {
C2 setAndGet(C2 arg) {
set(arg);
return get();
}
}
class D2 {
}
// Now this is OK:
class E2 extends NotSelfBounded<D2> {
}
因此很明显,自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。
还可以将自限定用于泛型方法:
public class SelfBoundingMethods {
static <T extends SelfBounded<T>> T f(T arg) {
return arg.set(arg).get();
}
public static void main(String[] args) {
A a = f(new A());
}
}
这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。
自限定类型的价值在于它们可以产生_协变参数类型_——方法参数类型会随子类而变化。
尽管自限定类型还可以产生与子类类型相同的返回类型,但是这并不十分重要,因为_协变返回类型_是在 Java 5 引入:
class Base {
}
class Derived extends Base {
}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
// Overridden method return type can vary:
@Override
Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter d) {
Derived d2 = d.get();
}
}
DerivedGetter 中的 get()
方法覆盖了 OrdinaryGetter 中的 get()
,并返回了一个从 OrdinaryGetter.get()
的返回类型中导出的类型。尽管这是完全合乎逻辑的事情(导出类方法应该能够返回比它覆盖的基类方法更具体的类型)但是这在早先的 Java 版本中是不合法的。
自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 get()
中所看到的一样:
interface GenericGetter<T extends GenericGetter<T>> {
T get();
}
interface Getter extends GenericGetter<Getter> {
}
public class GenericsAndReturnTypes {
void test(Getter g) {
Getter result = g.get();
GenericGetter gg = g.get(); // Also the base type
}
}
注意,这段代码不能编译,除非是使用囊括了协变返回类型的 Java 5。
然而,在非泛型代码中,参数类型不能随子类型发生变化:
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
}
}
class DerivedSetter extends OrdinarySetter {
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}
public class OrdinaryArguments {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedSetter ds = new DerivedSetter();
ds.set(derived);
// Compiles--overloaded, not overridden!:
ds.set(base);
}
}
set(derived)
和 set(base)
都是合法的,因此 DerivedSetter.set()
没有覆盖 OrdinarySetter.set()
,而是重载了这个方法。从输出中可以看到,在 DerivedSetter 中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。
但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数:
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter> {
}
public class SelfBoundingAndCovariantArguments {
void
testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
//- s1.set(sbs);
// error: method set in interface SelfBoundSetter
// cannot be applied to given types;
// s1.set(sbs);
// ^
// required: Setter
// found: SelfBoundSetter
// reason: argument mismatch;
// SelfBoundSetter cannot be converted to Setter
// where T is a type-variable:
// T extends SelfBoundSetter declared in
// interface SelfBoundSetter
// 1 error
}
}
编译器不能识别将基类型当作参数传递给 set()
的尝试,因为没有任何方法具有这样的签名。实际上,这个参数已经被覆盖。
如果不使用自限定类型,普通的继承机制就会介入,而你将能够重载,就像在非泛型的情况下一样:
class GenericSetter<T> { // Not self-bounded
void set(T arg) {
System.out.println("GenericSetter.set(Base)");
}
}
class DerivedGS extends GenericSetter<Base> {
void set(Derived derived) {
System.out.println("DerivedGS.set(Derived)");
}
}
public class PlainGenericInheritance {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedGS dgs = new DerivedGS();
dgs.set(derived);
dgs.set(base); // Overloaded, not overridden!
}
}
这段代码在模仿 OrdinaryArguments.java;在那个示例中,DerivedSetter 继承自包含一个 set(Base)
的OrdinarySetter 。而这里,DerivedGS 继承自泛型创建的也包含有一个 set(Base)
的 GenericSetter
。
就像 OrdinaryArguments.java 一样,你可以从输出中看到, DerivedGS 包含两个 set()
的重载版本。如果不使用自限定,将重载参数类型。如果使用了自限定,只能获得方法的一个版本,它将接受确切的参数类型。