《Thinking in Java》学习——15章泛型(二)
通配符
1.数组具有协变性:可以向导出类型的数组赋予基类型的数组引用:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
class CovariantArrays {
public static void main(String... args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); //OK
fruit[1] = new Jonathan(); //OK
fruit[2] = new Orange(); //ArrayStoreException
}
}
上面的代码不会出现编译问题,因为Apple、Orange、Jonathan都是Fruit的子类型,Fruit类型的引用持有它们并没有任何问题,是有意义的。但是fruit[2] = new Orange();这一句在运行时会抛出ArrayStoreException异常,因为数组fruit在运行时的实际类型为Apple。
2.数组的协变性对List并不起作用:
List<? extends Fruit> flist = new ArrayList<Apple>();
//Compile Error: can't add any type of object
//flist.add(new Apple());
//flist.add(new Fruit);
//flist.add(new Object());
上面代码中唯一的限制就是这个List要持有某种具体的Fruit或Fruit的子类型,但是编译器实际上并不知道LIst持有什么类型,那么也就不能安全地向其中添加对象,因此会出现编译时错误。
一.编译器有多聪明
1.虽然在上面的程序中List的add()方法不可用,但是并不是所有的方法都是不可用的:
public class CompilerIntelligence {
public static main(String... args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0);
flist.contains(new Apple());
flist.indexOf(new Apple());
}
}
这两段程序的区别在于add()方法将接受一个具有泛型参数类型的参数,但是contains()和indexOf()将返回或者接受Object类型的参数。因此,在指定一个ArrayList<? extends Fruit>时,add()的参数就变成了"? extends Fruit",此时,编译器并不能了解这里需要Fruit的哪个具体子类型,因此它不会接受任何类型的Fruit。而另外的方法使用了Object,并不涉及通配符,因此编译器也将允许这个调用。而上面的get()方法只会也只能返回Fruit对象,这是在该泛型参数所给定了边界——“任何扩展自Fruit的对象”之后所能做的唯一的事情了。
二.逆变
1.超类通配符:使用方法是由某个特定类的任何基类来界定的即<? super MyClass>。
2.有了超类通配符之后,程序可以这样写:
List<? super Apple> apples = Arrays.asList(new Apple());
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); //Error
这里Apple是下接,那么向其中添加Apple或Apple的子类型是安全的,但是添加Fruit是不安全的。
三.无界通配符
1.第一种情况下无界通配符意味着“任何事物”,即编译器很少关心使用的是原生类型还是<?>。因此,<?>是在声明:我是想用Java的泛型来编写代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。
2.泛型的另一种应用是:当你在处理多个参数时,优势允许一个参数可以时任喝类型,同时为其他参数确定某种特定类型,如Map<String, ?> map = new HashMap<String, Integer>
。
3.List实际表示“持有任何Object类型的List”,而List<?>表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么。”
4.使用确切类型来代替通配符,可以使用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。
一.捕获转换
1.捕捉转换:如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并使用另一个使用这个确切类型的方法。
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
f1(holder);
}
@SupressWarnings("unchecked")
public static void main(String... args) {
Holder raw = new Holder<Integer>(1);
//f1(raw); //warnings
f2(raw); //no warnings
}
上面代码的执行过程是:参数类型在调用f2()的过程中被捕获,因此它可以在对f1()对调用中被使用。
问题
一.任何基本类型都不能作为类型参数
1.不能将基本类型用作类型参数,因此不能创建ArrayList<int>之类的东西。
2.上一条的解决方法是使用基本类型的包装器以及自动包装机制。比如,创建一个ArrayList<Integer>,并将基本类型int应用于这个容器。
3.但是,自动包装机制不能应用于数组,因此当需要使用数组的时候需要注意。
二.实现参数化接口
1.一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口:
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Hourly> {}
由于擦除会将Payable<Employee>和Payable<Hourly>简化为相同的类Payable,也就意味着上面的代码重复两次地实现了相同的接口,因此不能编译。但是,如果将泛型参数全都移除,上述代码是可以编译的。
三.转型和警告
1.使用带有泛型参数类型的转型或instanceof不会带有任何效果:
class Stack<T> {
private int index = 0;
private Object[] storage;
......
@SupressWarnings("unchecked")
public T pop() {
return (T) storage[--index];
}
}
在pop()中,由于擦除的原因,编译器无法知道这个转型是否是安全的,并且由于T被擦除到它的第一个边界,此处即Object,因此pop()实际上只是将Object转型为Object,相当于没有执行任何转型。
四.重载
public class UseList<W, T> {
void f(List<W> w) {}
void f(List<T> t) {}
}
上面的代码由于擦除的原因,重载的方法将产生相同的签名,因此是不能编译的。
五.基类劫持了接口
public class ComparablePet implements Comparable<ComparablePet> {
public int compareTo(ComparablePet arg) {
return 0;
}
}
class Cat extends ComparablePet implements Comparable<Cat> {
public int compareTo(Cat arg) {
return 0;
}
}
这里有一个可以进行比较的Pet类,这里我们的想法是对它的子类Cat的比较尽兴窄化。但是会出现问题,因为一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象进行比较。
自限定的类型
一.古怪的循环泛型
1.不能直接集成一个泛型参数,但是,可以继承在自己的定义中使用这个泛型参数的类,如:
class GenericType<T> {}
public class CuriouslyRecurringGener extends GenericType<CuriousRecurringGener> {}
这就是古怪的循环泛型(CRG):类相当古怪地出现在它自己基类中这一事实。
2.CRG的本质:基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型:
class BasicHolder<T> {
T element;
void set(T arg) {
this.element = arg;
}
T get() {
return element;
}
}
class SubType extends BasicHolder<SubType> {
public static void main(String... args) {
SubType st1 = new SubType(), st2 = new SubType();
st1.set(st2);
SubType st2 = st1.get();
}
}
二.自限定
1.自限定:
class SelfBounded<T extends SelfBounded<T>> {}
SelfBounded类接受泛型参数T,而T由一个边界限定,这个边界就是拥有T作为其参数的SelfBounded。
2.自限定强制泛型当作自己的边界参数来使用。如:
class A extends SelfBounded<A> {}
3.自限定限制只能强制作用于继承关系。如果食用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。
三.参数协变
1.自限定类型的价值在于它们可以产生协变参数类型——方法参数类型后随子类而变化:
interface Generic<T extends Generic<T>> {
T get();
void set(T t);
}
interface GetterAndSetter extends Generic<GetterAndSetter> {}
public class Generics {
void test(Getter g) {
GetterAndSetter result = g.get();
Generic g = g.get();
GetterAndSetter gns = new GetterAndSetter();
gns.set(result);
gns.set(g); // Can't compile
}
}
上面代码中result和gg的实际类型都是GetterAndSetter,同时,set()方法只能接受GetterAndSetter类型的参数,如果传入基类参数,编译不会通过。但是注意,这段代码由于使用了自限定类型产生子类类型相同的换回类型,只有在囊括了协变返回类型的Java SE5的环境下才能编译。