15.1 泛型的出现
15.1.1 为什么要用泛型
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果需要编写可以应用于多种类型的代码。这种刻板的限制对代码的束缚就很大。所以从Java SE5开始,泛型出现了,泛型实现了参数化类型的概念,使代码可以应用于多种类型。
15.1.2 什么是泛型
泛型这个术语的意思是:“适用于许多许多的类型”,就是一种宽泛的类型,适用于很多的类型。
泛型可以用于类、接口、方法等
15.2 泛型类
有时候,我们希望一个类可以用于不同类型的对象,通常而言,在使用时用来表示一种类型的对象,泛型就可以用来指定这个类用于什么类型的对象,而且由编译器来保证类型的正确性。
遇到上面的需求时,通常我们会暂时不指定类型,而是稍后使用的时候再决定使用什么类型,要达到这个目的就需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此参数类型,下面的例子中,T就是类型参数:
/**
* 泛型类
*
* @author huxiong
* @date 2016/07/07 15:17
*/
public class GenericClass<T> {
private T field;
public T getField() {
return field;
}
public void setField(T field) {
this.field = field;
}
@Override
public String toString() {
return "GenericClass{" +
"field=" + field +
'}';
}
public static void main(String[] args) {
GenericClass<String> s = new GenericClass<>();
s.setField("gaga");
System.out.println("s:" + s);
GenericClass<Integer> i = new GenericClass<>();
i.setField(100);
System.out.println("i:" + i);
}
}
输出:
s:GenericClass{field=gaga}
i:GenericClass{field=100}
GenericClass类可以用于任意类型的对象,它是一个泛型类,我们只要在使用的时候指定其具体类型就可以了。
15.3 泛型接口
同样,泛型也可以用于接口,例如生成器
,这是一种专门负责创建对象的类,这也是工厂方法设计模式
的一种应用,不过在使用时,不需要任何参数,下面是一个制造玩具的程序:
/**
* 生产接口
*
* @param <T>
*/
interface Builder<T> {
T next();
}
/**
* 玩具
*/
class Toy {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + id;
}
}
/**
* 玩具狗
*/
class DogToy extends Toy {
}
/**
* 玩具猫
*/
class CatToy extends Toy {
}
/**
* 玩具熊
*/
class BearToy extends Toy {
}
/**
* 玩具制造器
*/
class ToyBuilder implements Builder<Toy>, Iterable<Toy> {
private Class[] types = {DogToy.class, CatToy.class, BearToy.class};
private int size = 0;//for iteration
private final Random rand = new Random(47);
public ToyBuilder() {
}
public ToyBuilder(int size) {
this.size = size;
}
@Override
public Toy next() {
try {
// make a toy randomly
return (Toy) types[rand.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 自定义迭代器
*/
class ToyIterator implements Iterator<Toy> {
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Toy next() {
count--;
return ToyBuilder.this.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Toy> iterator() {
return new ToyIterator();
}
public static void main(String[] args) {
ToyBuilder tb = new ToyBuilder();
for (int i = 0; i < 3; i++) {
System.out.println(tb.next());
}
System.out.println("------------------------");
for (Toy t : new ToyBuilder(3)) {
System.out.println(t);
}
}
}
输出:
BearToy0
BearToy1
CatToy2
------------------------
BearToy3
BearToy4
CatToy5
15.4 泛型方法
泛型方法所在的类可以是泛型类也可以不是泛型类,也就是说是否拥有泛型方法,与其所在的类是否是泛型没有关系。
泛型方法的定义就是将泛型参数列表置于返回值之前,像这样:
/**
* 泛型方法
* @author huxiong
* @date 2016/07/07 15:00
*/
public class GenericMethod {
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
gm.f("baby");
gm.f(1);
gm.f(3.0f);
gm.f(5.0d);
gm.f(gm);
}
}
输出:
java.lang.String
java.lang.Integer
java.lang.Float
java.lang.Double
com.mrdios.competencymatrix.java.readingnotes.ThinkingInJava.chapter15.GenericMethod
通过程序我们看到,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法时通常不需要,因为编译器会为我们找出正确的类型。这成为类型参数推断
15.5 泛型擦除
在泛型代码中,无法获得任何有关泛型类型参数的信息。
因此,我们可以知道如类型参数标识符和泛型类型边界这类的信息,但是却无法知道用来创建某个特定实例的实际的类型参数。之所以会这样,是因为java泛型是使用擦除来实现的,当使用泛型时,任何具体的类型信息都被擦除了,唯一知道的就是在使用一个对象,因此List<String>
和List<Integer>
在运行时事实上是相同的类型,这两种形式都被擦除成它们的“原生”类型List
class SuperMan {
public void fly() {
System.out.println("超人在飞...");
}
}
class Hero<T> {
private T someone;
public Hero(T someone) {
this.someone = someone;
}
public void savePeople() {
// someone.fly(); // 此句不能通过编译
}
}
class Hero2<T extends SuperMan> {
private T someone;
public Hero2(T someone) {
this.someone = someone;
}
public void savePeople() {
someone.fly(); // 此句通过编译
}
}
public class GenericWipe {
public static void main(String[] args) {
SuperMan sm = new SuperMan();
Hero2<SuperMan> hero = new Hero2<>(sm);
hero.savePeople();
}
}
就以上程序而言,由于有了擦除,java编译器无法将savePeople()
必须能够在someone
上调用fly()
这一需求映射到SuperMan
拥有fly()
这一事实上。所以Hero
中的savePeople()
将不能通过编译,为了调用fly()
,我们必须协助泛型类,给定泛型的边界,以告知编译器只能接收遵循这个边界的类型,所以Hero2
中相应的代码编译通过,
编译器实际上会把类型参数替换为它的擦除,就像以上程序中一样,T
擦除到了SuperMan
,就好像在类的声明中用SuperMan
替换了T
一样。
15.5.1 擦除与迁移兼容性
擦除并不是一个语言特性,它是java的泛型实现中的一种折中,因为java刚出现时并没有这个组成部分,所以这种折中也是必须的。
在基于擦除的实现中,泛型类型被当做第二类类型处理,就是不能再某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后程序中所有的泛型类型都将被擦除,替换为他们的泛型上界,如List<T>
被替换成List
擦除也是java语言的一种“迁移兼容性”,如果某个类库要迁移到泛型上的话,为了实现迁移兼容性,这个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。它们必须不具备探测其它类库是否使用了泛型的能力,所以必须将这些证据“擦除”。
15.6 使用泛型的问题
- 任何基本类型都不能作为泛型参数,诸如
List<int>
,可以使用包装类如List<Integer>
。 - 一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口,如
interface A<T>
,class B implements A<B>
,class C extends B implements A<C>
,此时C
将不能编译,因为擦除会将A<B>
和A<C>
简化为相同的类A
。 - 在泛型中,拥有方法
void f(List<T> v)
和void f(List<W> v)
的一个类UserList<T,W>
是不能编译通过的,因为由于擦除的原因,这个两个重载方法将产生相同的类型签名,所以这时候必须提供有明显区别的方法名。