目录
泛型存在的问题
在泛型中使用基本类型
实现参数化接口
类型转换和警告
无法实现的重载
基类会劫持接口
自限定类型
奇异递归类型
自限定
自限定提供的参数协变性
本笔记参考自: 《On Java 中文版》
泛型存在的问题
接下来讨论的,是在泛型中经常可能遇到的一些问题。
在泛型中使用基本类型
Java的泛型并不支持基本类型,因此我们无法将其用作泛型的类型参数。一个替代的方法是使用基本类型的包装类:
【例子:通过包装类使用泛型】
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ListOfInt {
public static void main(String[] args) {
List<Integer> li = IntStream.range(38, 48)
.boxed() // 将基本类型转换成其对应的包装类
.collect(Collectors.toList());
System.out.println(li);
}
}
程序执行的结果是:
这足以应付大部分的情况。但如果真的需要追求性能,可以使用专门适配基本类型的集合,例如org.apache.commons.collections.primitives。
或者,可以使用泛型集合来装载基本类型:
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class ByteSet {
Byte[] possibles = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Set<Byte> mySet1 =
new HashSet<>(Arrays.asList(possibles));
// 不可行的方式:
/* Set<Byte> mySet2 =
new HashSet<>(Arrays.<Byte>asList(
1, 2, 3, 4, 5, 6, 7, 8, 9)); */
}
在这里,自动装箱机制为我们解决了转换问题。但它不会总是有效,例如:
【例子:向数组中填充对象】
import java.util.*;
import java.util.function.*;
interface FillArray {
static <T> T[] fill(T[] a, Supplier<T> gen) {
// 使用get()填充数组a
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;
}
}
interface Rand {
// SplittableRandom也是用于生成随机数的类
SplittableRandom r = new SplittableRandom(47);
class StringGenerator implements Supplier<String> {
int strlen;
StringGenerator(int strlen) {
this.strlen = strlen;
}
@Override
public String get() {
return r.ints(strlen, 'a', 'z' + 1)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append).toString();
}
}
class IntegerGenerator implements IntSupplier {
@Override
public int getAsInt() {
return r.nextInt(10_000);
}
}
}
public class PrimitiveGenericTest {
public static void main(String[] args) {
String[] strings = FillArray.fill(
new String[5], new Rand.StringGenerator(7));
System.out.println(Arrays.toString(strings));
int[] integers = FillArray.fill(
new int[9], new Rand.IntegerGenerator());
System.out.println(Arrays.toString(integers));
}
}
程序执行的结果是:
由于自动装箱对数组无效,因此需要我们手动重载FillArray.fill()方法,或者通过一个生成器来包装输出结果。
实现参数化接口
一个类无法实现同一个泛型接口的两种变体:
因为类型擦除,这两个变体实际上都表示着原生的Payable。换言之,上述代码中Hourly将同一个接口实现了两次。
类型转换和警告
因为类型擦除,我们无法对类型参数使用类型转换或instanceof。因此,有时会需要在边界处进行类型转换:
【例子:在泛型边界处进行类型转换】
import java.util.Arrays;
import java.util.stream.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 =
"ABCDEFGHIJKLMNOPQRST".split("");
public static void main(String[] args) {
FixedSizeStack<String> strings =
new FixedSizeStack<>(letters.length);
Arrays.stream(letters)
.forEach(strings::push);
System.out.println(strings.pop());
strings.stream()
.map(s -> s + " ")
.forEach(System.out::print);
}
}
程序执行的结果是:
pop()和stram()会产生警告,因为编译器无法知道这种类型转换是否安全。在本例中,类型参数T会被擦除成Object。
虽然在泛型的边界处,类型转换会自动发生。但有时我们仍然需要手动进行类型转换,此时编译器会发出警告:
【例子:对泛型进行转型】
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;
public class NeedCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
List<Integer> shapes = (List<Integer>) in.readObject();
}
}
实际上,readObject()不会知道它正在读取什么,因此它会返回Object。
现在注释掉@SuppressWarnings("unchecked"),并且使用参数-Xlint:unchecked进行编译:
警告清楚地告诉了我们,readObject()会返回一个未经检查的Object。
Java 5还引入了一个转型方法,通过Class.cast(),可以将对象强制转换成目标类型。这个方法也适用于泛型:
【例子:尝试强制转换泛型】
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;
public class ClassCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
// 无法编译的代码:
// List<Integer> lw1 =
// List<>.class.cast(in.readObject()); // 使用cast()进行强制类型转换
// 会引发警告:
List<Integer> lw2 = List.class.cast(in.readObject());
// 无法编译:
// List<Integer> lw3 = List<Integer>.class.cast(in.readObject());
// 会引发警告
List<Integer> lw4 = (List<Integer>) List.class.cast(in.readObject());
}
}
然而,如代码所示。这些做法都会存在着这样那样的限制。
无法实现的重载
由于类型擦除,下面的这种写法是不被允许的:
【例子:无法实现的重载】
import java.util.List;
public class UseList<W, T> {
void f(List<T> v) {
}
void f(List<W> v) {
}
}
因为被擦除的参数无法作为单独的参数列表,所以我们还需要为每一个相似的方法提高不同的方法名。
基类会劫持接口
假设我们想要创建一个类,这个类实现了Comparable接口,这样这个类的不同对象就能进行互相的比较:
【例子:实现了Comparable的父类】
public class ComparablePet
implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet arg) {
return 0;
}
}
一个好的想法是,任何继承了这个类的子类,其对象之间应该也能进行比较(在这个例子中,父类是Pet,子类就是Cat)。然而事实并不会如我们所愿:
遗憾的是,若继承了父类的泛型接口,编译器不会再允许我们添加另一个Comparable接口。在这里,我们只能遵循父类的比较方式。
我们还可以在子类中重写compareTo()的行为,但这种行为是面向ComparablePet的(而不是限定在这个子类中)。
自限定类型
自限定类型来自于Java早期的泛型使用习惯:
class SelfBounded<T extends SelfBounded<T>> { // ...
在这里,类型参数的边界就是类本身:SelfBounded有一个类型参数T,而参数T的边界却又是SelfBounded。
这种写法更加强调extends在泛型参数中使用时的含义。
奇异递归类型
先看一个自限定类型的简化版本。尽管无法直接继承泛型参数,但我们可以继承一个使用了泛型参数的类。
【例子:继承泛型类】
class GenericType<T> {
}
public class CuriouslyRecurringGeneric
extends GenericType<CuriouslyRecurringGeneric> {
}
这种方式被称为奇异递归泛型。其中,“奇异递归”是指子类奇怪地出现在了其基类中的现象、
要理解这一点,首先需要明确: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());
}
}
在这个基类中,所有方法的接收或返回值(若有)都是T。接下来尝试使用这个类:
class Subtype extends BasicHolder<Subtype> {
}
public class CRGWithBasicHolder {
public static void main(String[] args) {
// Subtype中的所有方法,其接收和返回的都是Subtype:
Subtype st1 = new Subtype(),
st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
}
程序执行的结果是:
需要注意的是,Subtype类中,所有方法的接收和返回值都已经变成了Subtype。这就是一个奇异递归泛型:基类用子类替换了其参数。在这里,基类用于提供通用的方法模板,而子类使用的方法都会具有一个具体的类型,即子类自身。
自限定
上述的BasicHolder可以将任何类型作为其泛型参数:
【例子:BasicHolder的广泛应用】
class Other {
}
// 将不相关的Other作为参数
class BasicOther extends BasicHolder<Other> {
}
自限定在这种操作的基础上更进一步,它强制地把泛型作为自身的边界参数进行使用:
// 自限定类型:
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> {
}
// 属于SelfBounding<>的类型也可以这样使用:
class B extends SelfBounded<A> {
}
class C extends SelfBounded<C> {
C setAndGet(C arg) {
set(arg);
return get();
}
}
class D {
}
// 但这种做法是不被允许的:
// class E extends SelfBounding<D> {
// }
// 这样的可以(自限定的语法并非强制性的):
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());
}
}
需要注意的是,自限定类型会要求类处于继承关系中。因此像E这种并不处于继承关系中的类无法使用自限定。
除此之外,可以看到编译器并没有对F这种写法发出警告:
class F extends SelfBounded {}
由此可知,编译器对自限定的语法并不做强制要求,这需要程序员自己注意(或使用工具保证不会使用原生类型)。
注意:自限定类型只服务于强制继承关系。若使用自限定,这意味着该类使用的类型参数和使用该参数的类属于同一个基类。
对于普通的泛型类而言,像上例中的E这样的类型是可以作为泛型参数的。这种泛型类就没有对继承关系的强制性要求。
除此之外,自限定还可用于泛型方法:
【例子:使用了自限定的泛型方法】
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());
}
}
这种做法的特点是,方法f()无法应用于自限定参数规定范围之外的对象。
自限定提供的参数协变性
自限定类型的价值在于它可以生成协变参数类型,即方法参数的类型会随着子类而变化。现在先来看一个协变参数类型的例子,这种写法是Java 5引入的:
【例子:Java中的协变参数类型】
class Base {
}
class Derived extends Base {
}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
@Override
Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter d) {
Derived d2 = d.get();
}
}
这种做法有着自洽的逻辑:子类方法可以返回比其基类方法更加具体的类型(但这种写法在Java 5之前是行不通的)。
而自限定方法则可以直接返回精确的派生类型:
【例子:自限定的返回值】
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();
}
}
不过,这种做法只在引入了协变类型的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);
// 编译通过,但这里发生的不是重写,是重载:
ds.set(base);
}
}
程序执行的结果是:
尽管在main()中,ds.set(derived)和ds.set(base)都是合法的,但发生的并不是重写,而是重载。从输出可以看出,在子类DerivedSetter中存在着两个set()方法,一个参数是Base,另一个的是Derived。
若对DerivedSetter的set()方法使用@Override注释,就可以看出问题。
当使用自限定类型时,子类中来自基类的方法的参数会发生改变,因此会出现下面这种情况:
【例子:子类方法的参数会被重写】
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter> {
// 未进行任何改动,但实际上set()已经被重写
}
public class SelfBoundingAndCovariantArguments {
void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
// 不允许这么做:
// s1.set(sbs);
}
}
s1.set(sbs)存在问题:
编译器认为基类无法匹配当前set()的类型,尽管上述代码中并没有在Setter中显式地重写set()方法,但set()的参数确实已经被重写了。
若不使用自限定,那么普通的继承机制就会启动:
【例子:普通的继承机制】
// 非自限定的类型:
class OtherGenericSetter<T> {
void set(T arg) {
System.out.println("GenericSetter.set(Base)");
}
}
class DerivedGS extends OtherGenericSetter<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);
}
}
程序执行的结果是:
显然,这里发生的还是重载。若使用的是自限定,最后只会有一个接收确切类型参数的方法版本。