Java泛型 自限定类型(Self-Bound Types)详解
简介
java泛型里会有class SelfBounded<T extends SelfBounded<T>> { }
这种写法,泛型类有一个类型参数T,但这个T有边界SelfBounded<T>
。这边界里有两个疑问:
- SelfBounded已经在左边出现,但SelfBounded类还没定义完这里就用了;
- 同样,T也在左边出现过了,这是该泛型类的类型参数标识符。
这两点确实挺让人疑惑,思考这个类定义时容易陷入“死循环”。
注意,自限定的要点实际就是这两个“疑问”。
普通泛型类——构成自限定
class BasicHolder<T> {
T element;
void set(T arg) { element = arg; }
T get() { return element; }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
class Subtype extends BasicHolder<Subtype> {}
public class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype st1 = new Subtype(), st2 = new Subtype(), st3 = new Subtype();
st1.set(st2);
st2.set(st3);
Subtype st4 = st1.get().get();
st1.f();
}
} /* Output:
Subtype
*///:~
BasicHolder<T>
泛型类的类型参数并没有什么边界,在继承它的时候,你本可以class A extends BasicHolder<B> {}
这样普普通通的用。- 但
class Subtype extends BasicHolder<Subtype> {}
这样用,就构成自限定了。从定义上来说,它继承的父类的类型参数是它自己。从使用上来说,Subtype
对象本身的类型是Subtype
,且Subtype
对象继承而来的成员(element)、方法的形参(set方法)、方法的返回值(get方法)也是Subtype
了(这就是自限定的重要作用)。这样Subtype
对象就只允许和Subtype
对象(而不是别的类型的对象)交互了。 - 虽然
class Subtype extends BasicHolder<Subtype> {}
这样用,看起来是类定义还没有结束,就把自己的名字用到了边界的泛型类的类型参数。虽然感觉稍微有点不合理,但这里就强行理解一下吧。自限定的用法:父类作为一个泛型类或泛型接口,用子类的名字作为其类型参数。 - 以上两点,就解释了简介里的第二个“疑问”。正因为
class Subtype extends BasicHolder<Subtype>
这样用可以让Subtype
对象只允许和Subtype
对象交互,这里再把Subtype
抽象成类型参数T,不就刚好变成了T extends SelfBounded<T>
这样的写法。 - 在主函数里,根据自限定的重要作用,且由于
BasicHolder<T>
泛型类有个成员变量和set方法,所以st1.set(st2); st2.set(st3);
可以像链表一样,节点的后继指向一个节点,后者又可以指向另外的节点。
自限定类型的泛型类
下面是自限定类型的标准用法。
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> {}
public class SelfBounding {
public static void main(String[] args) {
A a = new A();//a变量只能与A类型变量交互,这就是自限定的妙处
SelfBounded<A> b = new SelfBounded<A>();
}
} ///:~
- 抛开自限定类型的知识点,观察SelfBounded泛型类,发现该泛型类的类型参数T有
SelfBounded<T>
的边界要求。根据上个章节的讲解,一个普通的泛型类我们都可以继承它来做到自限定,且因为要使用SelfBounded泛型类之前,我们必须有一个实际类型能符合SelfBounded<T>
的边界要求,所以这里就模仿上一章,创建一个新类来符合这个边界,即class A extends SelfBounded<A> {}
,这样新类A
便符合了SelfBounded<T>
的边界。 - 这时你觉得终于可以使用SelfBounded泛型类了,于是你便
SelfBounded<A> b = new SelfBounded<A>();
,但是这个b变量本身的类型是SelfBounded<A>
,成员函数的形参或返回值的类型却是A
,这个效果看起来不是我们想要的自限定的效果。(b变量不可以和别的SelfBounded<A>
对象交互,因为它继承来的成员函数的类型限定是A
,这样把别的SelfBounded<A>
对象传给成员函数会造成ClassCastException,这属于父类对象传给子类引用,肯定不可以。所以说没有达到自限定。) - 其实,这里是我们多此一举了,新类
class A extends SelfBounded<A> {}
创建的时候就已经一举三得了。1.出现SelfBounded尖括号里面的A
需要满足边界SelfBounded<T>
,它自己的类定义已经满足了。2.给了SelfBounded泛型类的定义是为了使用它,新类A
的对象也能使用到它,只不过这里是继承使用。3.根据上一章的讲解,新类A
的类定义形成了自限定。 - 可能一般我们以为要使用SelfBounded泛型类要有两步(1.创建新类型以符合边界 2.以刚创建的新类型的名字来创建SelfBounded泛型类对象),但由于
class SelfBounded<T extends SelfBounded<T>>
类定义中,SelfBounded作为了自己的泛型类型参数的边界,这样,想创建一个新类作为T类型参数以符合边界时,这个新类就必须继承到SelfBounded的所有成员(这也是我们想要的效果)。所以就可以class A extends SelfBounded<A> {}
这样一步到位。这也解释了简介里的第一个“疑问”。
对了,对于第一个疑问,你可能想看一下,如果边界里的泛型类不是自己,会是什么情况:
class testSelf<T> {
//假设这里也有一些成员变量,成员方法
}
class SelfBounded<T extends testSelf<T>> {//类型参数的边界不是自己的名字SelfBounded
T element;
SelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class testA extends testSelf<testA> {}//这个新类可作为SelfBounded的类型参数T,因为符合了边界
public class SelfBounding {
public static void main(String[] args) {
SelfBounded<testA> a = new SelfBounded<testA>();
}
} ///:~
按照一般使用SelfBounded泛型类的两个步骤,首先需要创建新类class testA extends testSelf<testA> {}
来符合边界,然后新类型作为类型参数使用来创建SelfBounded对象,即SelfBounded<testA> a = new SelfBounded<testA>()
。但a变量的效果却不是我们想要的自限定的效果,总之看起来很奇怪。
一旦你把class SelfBounded<T extends testSelf<T>>
的边界改成<T extends SelfBounded<T>>
,那么新类testA,那么它的定义就应该是class testA extends SelfBounded<testA> {}
,然后正因为testA继承了SelfBounded<testA>
,所以testA就获得了父类SelfBounded的成员方法且这些成员方法的形参或返回值都是testA。
通过这个反例便进一步解释了简介的第一个“疑问”。
对本章第一个例子作进一步的拓展吧:
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<D> {}
// Compile error: Type parameter D is not within its bound
// Alas, you can do this, so you can't 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();//最终a是null
C c = new C();
c = c.setAndGet(new C());//c换成这行新new出来的C对象了
}
} ///:~
class B extends SelfBounded<A>
这样使用也是可以的,毕竟继承SelfBounded时,给定的具体类型A确实满足了边界。不过B对象没有自限定的效果了。class C extends SelfBounded<C>
展示了:在自己新增的成员方法里,去调用继承来的成员方法。注意,继承来的方法被限定类型为C即自身了,这就是自限定的效果。class E extends SelfBounded<D>
无法通过编译,因为给定的具体类型A不符合边界。class F extends SelfBounded
,你可以继承原生类型,此时T会作为它的上限SelfBounded(边界)来执行。如下图:
也可以将自限定用于泛型方法:
//借用之前定义好的SelfBounded
class testNoBoundary<T> {}//我自己新加的
public class SelfBoundingMethods {
static <T extends SelfBounded<T>> T f(T arg) {
return arg.set(arg).get();
}
static <T extends testNoBoundary<T>> T f1(T arg) {
return arg;
}
public static void main(String[] args) {
A a = f(new A());
class selfBound extends testNoBoundary<selfBound> {}
selfBound b = f1(new selfBound());
}
} ///:~
f
静态方法要求T自限定,且边界是SelfBounded。那么之前定义的A类型就符合要求了。- 我加了个
f1
静态方法,它也要求T自限定,且边界是testNoBoundary。注意testNoBoundary泛型类对类型参数T没有边界要求。class selfBound extends testNoBoundary<selfBound> {}
这里用了局部内部类创建了一个符合边界要求的新类型。
JDK源码里自限定的应用——enum
java中使用enum
关键字来创建枚举类,实际创建出来的枚举类都继承了java.lang.Enum。也正因为这样,所以enum
不能再继承别的类了。其实enum
就是java的一个语法糖,编译器在背后帮我们继承了java.lang.Enum。
下面就是一个枚举类的使用:
public enum WeekDay {
Mon("Monday"), Tue("Tuesday"), Wed("Wednesday"), Thu("Thursday"), Fri( "Friday"), Sat("Saturday"), Sun("Sunday");
private final String day;
private WeekDay(String day) {
this.day = day;
}
public static void printDay(int i){
switch(i){
case 1: System.out.println(WeekDay.Mon); break;
case 2: System.out.println(WeekDay.Tue);break;
case 3: System.out.println(WeekDay.Wed);break;
case 4: System.out.println(WeekDay.Thu);break;
case 5: System.out.println(WeekDay.Fri);break;
case 6: System.out.println(WeekDay.Sat);break;
case 7: System.out.println(WeekDay.Sun);break;
default:System.out.println("wrong number!");
}
}
public String getDay() {
return day;
}
public static void main(String[] args) {
WeekDay a = WeekDay.Mon;
}
}
发现通过idea看WeekDay.class文件时看不出继承java.lang.Enum的。只有通过javap命令才能看出来。先看一下java.lang.Enum的定义,Enum<E extends Enum<E>>
是自限定类型的标准写法:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { }
截取部分汇编来看:
public final class WeekDay extends java.lang.Enum<WeekDay> {
public static final WeekDay Mon;
public static final WeekDay Tue;
public static final WeekDay Wed;
public static final WeekDay Thu;
public static final WeekDay Fri;
public static final WeekDay Sat;
public static final WeekDay Sun;
public static WeekDay[] values();
public static WeekDay valueOf(java.lang.String);
发现确实WeekDay做到了自限定,因为继承来的成员和方法的类型都被限定成WeekDay它自己了。
分析一下java.lang.Enum
这么设计的好处:
Enum
作为一个抽象类,我们使用enum
关键字创建出来的枚举类实际都是Enum
的子类,因为class Enum<E extends Enum<E>>
的类定义是这种标准的自限定类型,所以编译器直接生成的类必须是WeekDay extends java.lang.Enum<WeekDay>
(即本文中讲的:需先创建一个符合边界条件的实际类型,但创建的同时又继承Enum
本身,所以就一步到位了)。- 正因为编译器生成的枚举类都是
Enum
的子类,结合上条分析,每种Enum
子类的自限定类型都是Enum
子类自身。这样WeekDay的实例就只能和WeekDay的实例交互(星期几和星期几比较),Month的实例就只能和Month的实例交互(月份和月份比较)。
JDK源码里自限定的应用——Integer
Integer
的类定义是:
public interface Comparable<T> {
public int compareTo(T o);
}
public final class Integer extends Number implements Comparable<Integer> {//省略}
可以看到Integer
实现了Comparable<Integer>
,这也是自限定,这样,从Comparable接口继承来的compareTo方法的形参类型就是Integer
它自己了。和章节《普通泛型类——构成自限定》里的例子一样。
但接口Comparable的定义可没要求类型参数T必须自限定啊,它甚至连T的边界都没有,当然,这样的好处就是把决定权交给了Comparable的使用者,当使用者想要自限定时,就按照自限定的写法创建新类就好了。