捕获转换
有一种情况特别需要使用<?> 而不是原生类型。如果想一个使用 <?> 的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,是的这个方法可以回调另一个使用这个确切类型的方法。
public class CaptureConversion {
static <T> void f1(Holder<T> holder){
T holderT = holder.getT();
System.out.println(holderT.getClass().getSimpleName());
}
static void f2(Holder<?> holder){
f1(holder);
//捕获类型
}
public static void main(String[] args) {
Holder<Integer> holder=new Holder<>(1);
f1(holder);
f2(holder);
Holder holder1=new Holder();
holder1.setT(new Object());
f2(holder1);
Holder<Double> holder2=new Holder<>(1.0d);
f2(holder2);
}
}
//运行结果为
Integer
Integer
Object
Double
f1() 中的类型参数都是确切的,没有通配符或边界。在 f2() 中,Holder 参数是一个无界通配符,因为它看起来是未知的。但是, 在 f2() 中,f1() 被调用,而 f1()需要一个已知参数。这里所发生的是: 参数类型在调用 f2() 的过程中被捕获,因此它可以在对f1() 的调用中被使用。你可能想知道,这项技术是否可以用于写入,但是这要求在传递 Holder<?> 时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:
即在内部,你需要使用确切的类型。注意不能从 f2() 中返回T ,因为 T 对于 f2() 来说是未知的。捕获转换十分有趣,但是非常受限。
任何基本类型都不能作为类型参数
正如本章早先提到过的,你将在Java泛型中发现的限制之一是,不能将基本类型用作类型参数。因此,不能创建 ArrayList<int> 之类的东西。解决之道是使用基本类型的包装器以及JavaSE5的自动包装机制。如果创建一个ArrayList<Integer> ,并将基本类型 int 应用于这个容器,那么你将发现自动包装机制将自动地实现 int 到 Integer 的双向转换__因此,这几乎就像是有一个 ArrayList<int> 一样的。
public class ListOfInt {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i);
}
for (int i:list) {
System.out.println(i);
}
}
}
//运行结果为
0 1 2 3 4
注意,自动包装机制甚至允许用 foreach 语法来产生int。通常,这种解决方案工作得很好___能够成功地存储和读取int,有一些转换碰巧在发生的同时会对你屏蔽掉。但是,如果性能成为了问题,就需要使用专门适配基本类型的容器版本。
public class ByteSet {
Byte [] possible={1,2,3,4,5,6,7,8,9};
Set<Byte> byteSet =new HashSet<>(Arrays.asList(possible));
//but you can't do this 但是你做不到
//Set<Byte> bytes=new HashSet<>(Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
}
注意,自动包装机制解决了一些问型题,但并不是解决了所有问题。下面的示例展示了一个 泛型的 Generator 接口,它指定 next() 方法返回一个具有其参数类型的对象。Farray类包含一个泛型方法,它通过使用生成器在数组中填充对象(这使得类型在本例中无法工作,因为方法是静态的)。
public class Farray {
static <T> T[] fill(T[] t,Generator<T> generator){
for (int i = 0; i < t.length; i++) {
t[i]=generator.next();
}
return t;
}
}
interface RandomGenericator<T> extends Generator<T>{}
class PrimitiveGenericTest{
public static void main(String[] args) {
String[] strings = Farray.fill(new String[7], new RandomGenericator<String>() {
@Override
public String next() {
StringBuffer sb = new StringBuffer();
String[] strs = {"a", "b", "c", "d", "e", "f", "g"};
Random random = new Random();
//随机生成四位随机数
for (int i = 0; i < 4; i++) {
sb.append(strs[random.nextInt(strs.length)]);
}
return sb.toString();
}
});
//打印 数组内容
for (String s :strings) {
System.out.println(s);
}
Integer[] fill = Farray.fill(new Integer[7], new RandomGenericator<Integer>() {
@Override
public Integer next() {
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < 4; i++) {
sb.append(random.nextInt(10));
}
return Integer.valueOf(sb.toString());
}
});
for (int i:fill) {
System.out.println(i);
}
}
}
//运行结果为
fbdg
daed
bdee
eadg
bfgf
bgbc
ggdf
3456
4645
2447
8682
6451
3490
6290
由于 new RandomGenericator<T> 使用内部类的方法来生成不同的策略,
实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这俩个变体会变为相同的接口。
public interface Payable<T> {}
class Employee1 implements Payable<Employee1>{}
//class Hourly1 extends Employee1 implements Payable<Hourly1>{}
Hourly 不能编译,因为擦除会将 Payable<Employee1> 和 Payable<Hourly1> 简化为相同的类 Payable,这样,上面的代码就意味着在
重复俩次地实现相同的接口。十分有趣的是,如果从 Payable的俩种用法中都移除掉泛型参数(就像编译器在擦除阶段所作的那样)这样代码就可以编译。
转型和警告
使用带有泛型类型参数的转型或 instanceof 不会有任何效果。如下
public class FinxedSizeStack<T> {
private int index = 0;
private Object[] array;
public FinxedSizeStack(int size) {
array = new Object[size];
}
public void push(T item) {
array[index++] = item;
}
public T pop(){
return (T) array[--index];
}
}
class Demo{
public static void main(String[] args) {
FinxedSizeStack<String> stack = new FinxedSizeStack<>(5);
for (String s:"A B C D E".split(" ")) {
stack.push(s);
}
for (int i = 0; i < 5; i++) {
System.out.print(stack.pop() +" ");
}
}
}
//运行结果为
E D C B A
由于擦除的原因,编译器无法知道这个转型是否安全的,并且 pop() 方法实际上并没有执行任何转型。这是因为,T 被擦除到它的第一个边界,默认情况下是 Object ,因此 pop() 实际上只是将 Object 转型为 Object。
重载
下面的才程序时
不能编译的,即使它是一种合理的尝试:
public class UseList<W,T> {
void f(List<W> ws){}
void f(List<T> ts){}
}
由于擦除的原因,重载方法将昌盛相同的类型签名。与此不同的是,当被擦除的参数不能产生唯一的参数列表时,必须提供明显有区别的方法名。
public class UseList<W,T> {
void f1(List<W> ws){}
void f2(List<T> ts){}
}
幸运的是,这类问题可以由编译器探测到。
基类劫持了接口
假设你有一个Pet类,它可以与其他的Pet对象进行比较(实现了 Comparable接口)。
public class ComparablePet implements Comparable<ComparablePet>{
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
对可以与 ComparablePet 的子类比较的类型进行窄化是有意义的。例如,一个 Cat 对象就只能与其他Cat 对象比较。
自限定的类型
在Java 泛型中,有一个好像是经常性出现的惯用法,它相当令人费解。
class SelfBounded<T extends SelfBounded<T>>{//...}
这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种
无限反射。SelfBounded 类接受泛型参数 T ,而 T 由一个边界类限定,这个边界就是拥有 T 作为其参数的SelfBounded。当你每次看到它时,很难去解析它,它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。
古怪的循环泛型
为了理解自限定类型的含义,我们从这个惯用法的一个简单版本入手,它没有自限定的边界。不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类。也就是说,可以声明。
class GenericType<T>{}
class CuriouslyRecurringGeneric extends GenericType<CuriouslyRecurringGeneric>{}
这可以按照 Jim Coplien 在C++ 中的
古怪的循环模板模式的命名方式,称为
古怪的循环泛型(CRG)。古怪的循环 是指
类相当古怪地出现在它自己的基类中这一事实。为了理解其含义,努力大声说
我们再创建一个新类,他它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。当给出导出类的名字时,这个泛型基类能够实现什么呢? 好吧,Java中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,甚至那些将被擦除为Object 的类型。
class BasicHolder<T>{
T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
void f(){
System.out.println(t.getClass().getSimpleName());
}
}
这是一个普通的泛型类型,它的一些方法将接受和产生具有其参数类型的对象,还有一个方法将在其存储的域上执行操作(尽管只是在这个域上执行Object操作)。
class Subtype extends BasicHolder<Subtype> {}
class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype subtype = new Subtype();
Subtype subtype1 = new Subtype();
subtype.setT(subtype1);
Subtype subtype2 = subtype.getT();
subtype.f();
}
}
//运行结果为
Subtype
注意,这里有些东西很重要: 新类Subtype 接受的参数和返回的值具有 Subtype类型而不仅仅是基类 BasicHolder 类型。这就是 CRG 的本质: 基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模板,但是这些功能对于其所有参数和返回值,将使用导出类型。 也就是说,在所有的类中将使用确切类型而不是基类型。因此在 Subtype 中,传递给setT() 的参数和从 getT() 返回的类型都是确切的Subtype。
自限定
BasicHolder 可以使用任何类型作为其泛型参数,就像下面这样。
class Other{}
class BasicOther extends BasicHolder<Other>{}
class Unconstrained{
public static void main(String[] args) {
BasicOther basicOther=new BasicOther();
basicOther.setT(new Other());
BasicOther basicOther1=new BasicOther();
Other other = basicOther.getT();
basicOther.f();
}
}
//运行结果为
Other
自限定将采取额外的步骤, 强制泛型当作其自己的边界参数来使用。观察所产生的类可以如何使用以及不可以如何使用。
public class SelfBounded<T extends SelfBounded<T>> {
T t;
public T getT() {
return t;
}
public SelfBounded<T> setT(T t) {
this.t = t;
return this;
}
}
class A extends SelfBounded<A> {}
//还可以
class B extends SelfBounded<A> {}
class C extends SelfBounded<C> {
C setAndGet(C args) {
setT(args);
return getT();
}
}
class D {
}
//做不到
// !class E extends SelfBounded<D> {}
//编译错误类型参数D不在其范围内
//您可以这样做,所以您不能强迫惯用用法
class F extends SelfBounded {}
class SelfBounding {
public static void main(String[] args) {
A a = new A();
a.setT(new A());
a=a.setT(new A()).getT();
a=a.getT();
C c=new C();
c.setAndGet(new C());
}
}
自限定所做的,就是要求在继承关系中,就像下面这样使用这个类。
class A extends SelfBounded<A>{}
这会去强制要求将正在定义的类当作参数传递给基类。
自限定的参数有何意义呢? 它可以保证类型参数必须与正在被定义的类相同。正如你在 B 类的定义中所看到的的,还可以从使用了另一个 SelfBounded参数的 SelfBounded中导出 , 尽管在 A 类看到的用法看起来是主要的用法。对定义 E 的尝试说明不能使用不是 SelfBounded 的类型参数。遗憾的是 , F可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是 E 也会因此而变得可编译。
class NoSelfBounded<T>{
T t;
public T getT() {
return t;
}
public NoSelfBounded<T> setT(T t) {
this.t = t;
return this;
}
}
class A2 extends NoSelfBounded<A2>{}
class B2 extends NoSelfBounded<B2>{}
class C2 extends NoSelfBounded<C2>{
C2 setAndGet(C2 arg){
setT(arg);
return getT();
}
}
class D2 {}
//现在可以了
class E2 extends NoSelfBounded<D2>{}
因此很明显,自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。还可以将自限定用于泛型方法。
class SelfBoundingMethods{
static <T extends SelfBounded<T>> T f(T t){
return t.setT(t).getT();
}
public static void main(String[] args) {
A a=f(new A());
}
}
这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。
参数协变
自限定类型的价值在于它们可以产生协变参数类型___方法参数类型会随着子类而变化。尽管自限定类型还可以产生于子类类型相同的返回类型,但这并不重要,因为协变返回类型是在 JavaSE5中引入的。
public class Base {
}
class Derived extends Base {
}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
//重写方法的返回类型允许
@Override
Derived get();
}
class CovariantReturnTypes{
void test(DerivedGetter derivedGetter){
Derived derived = derivedGetter.get();
}
}
DerivedGetter 类中的 get() 方法覆盖了 OrdinaryGetter 类中的 get() 方法,并返回了一个从 OrdinaryGetter.get() 的返回类型中导出的类型。尽管这是完全合乎逻辑的事情(导出类方法应该能够返回比它覆盖的基类方法更具体的类型)但是这在早先的 Java 版本中是不合法的。自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 get() 中所看到的一样。
interface GenericGetter<T extends GenericGetter<T>>{
T get();
}
interface Getter extends GenericGetter<Getter>{}
class GenericsAndReturnTypes{
void test(Getter getter){
Getter result = getter.get();
//也是基本类型
GenericGetter gg = getter.get();
}
}
注意,这段代码不能编译,除非是使用了囊括了协变返回类型的JavaSE5。然而,在非泛型代码中,参数类型不能随子类型发生变化。
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");
}
}
class OrdinaryArguments{
public static void main(String[] args) {
Base base=new Base();
Derived derived=new Derived();
DerivedSetter derivedSetter=new DerivedSetter();
derivedSetter.set(derived);
derivedSetter.set(base);
//编译重载而不是覆盖
}
}
//运行结果为
DerivedSetter set Derived
OrdinarySetter set Base
set(derived) 和 set(base) 都是合法的,因此 DerivedSetter.set() 没有覆盖 OrdinarySetter.set() ,而是重载了这个方法。从输出结果中可以看到,在 DerivedSetter 中有俩个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数。
interface SelfBoundSetter<T extends SelfBoundSetter<T>>{
void set(T t);
}
interface Setter extends SelfBoundSetter<Setter>{}
class SelfBoundingAndCovariantArguments{
void testA(Setter setter,Setter setter2,SelfBoundSetter selfBoundSetter){
setter.set(setter2);
//setter.set(selfBoundSetter); error
//selfBoundSetter.set(Setter); 在 SelfBoundSetter<Setter>
//不适用于(SelfBoundSetter)
}
}
编译器不能识别将基类当做参数传递给 set() 的尝试,因为没有任何方法具有这样的签名。实际上,这个参数已经被覆盖。如果不使用自限定类型,普通的继承机制就会介入,而你将能够重载,就像在非泛型的情况下一样。
class GenericSetter<T>{
void set(T t){
System.out.println("GenericSetter set t");
}
}
class DerivedGS extends GenericSetter<Base>{
void set(Derived derived){
System.out.println("DerivedGS set Derived");
}
}
class PlainGenericInheritance{
public static void main(String[] args) {
Base base=new Base();
Derived derived=new Derived();
DerivedGS derivedGS=new DerivedGS();
derivedGS.set(derived);
derivedGS.set(base);
}
}
//运行结果为
DerivedGS set Derived
GenericSetter set t
这段代码在模仿 OrdinaryArgument.java, 在那个例子中,DerivedSetter 继承自包含一个 set(Base) 的 OrdinarySetter 。而在这里,DerivedGs 继承自泛型创建的也包含有一个 set(Base) 的 GenericSetter<Base> 。就像 OrdinaryArgument.java一样,你可以从输出结果中看到,DerivedGS 包含两个 set() 的重载版本。如果不使用自限定,将重载参数类型。如果使用了自限定,只能获得某个方法的一个版本它将确切的参数类型。