1.泛型的由来
一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义的类型,如果要编写可以适用于多种类型的代码,这种刻板的限制对代码的束缚性就很大。
好的程序可以适应更多的场景,来满足我们对业务的需求。为了满足这种要求。Java给我们提供了很多方式来满足这种灵活性,扩展性,通用性的要求。
最典型的例子就是多态,在任何面向对象的编程语言中都有多态的机制。例如,我们可以在方法的形参中声明某个基类,而在实际调用的时候可以给他传递该基类的导出类作为实际参数。这样的方法就会更加通用一些。其实受限于java的单继承体制,我们更好的做法是声明一个接口(interface)作为方法的参数。将实现该接口的导出类作为实参传递进行,进而获得更大的灵活性。
但是不幸的是,有时候,即使我们依附于多态的特性,使用接口来实现更加灵活的程序。还是没法满足我们的需求。因为一旦指明了接口,那么程序就要求你必须使用某种特定的接口。而我们希望达到的目的是编写更加通用的代码。要使代码应用于"某种不具体的类型",而不是具体的接口和类。
这时候我们今天的主角“泛型”就出场了,泛型是Java SE5带来的新特性。泛型实现了参数化类型的概念,是代码可以应用于多种类型。而“泛型”这个术语的意思其实就是"可以适用于很多种类型"。
2.泛型的分类
2.1泛型类
其实有很多的原因促成了泛型的出现,其中最终要的原因是,为了创建容器类,容器就是要存放使用对象的地方。我们常见的容器类有List,Set,Map等等这些集合类都是容器类。当然数组也是,只不过数组具有固定的大小。灵活性不够高。
假设现在我们没有泛型,我们想定义一个容器,在容器中持有某种水果。(你可以把容器想象成盘子,或者碗,任何可以容纳水果的器皿),我们先让容器持有一个橘子(在一个碗里面放一个橘子)。
public class Hoder {
private Orange orange;
// 构造器,get/set方法省略
}
但是这时候我不想放橘子了,我想放置一个苹果,这时候程序是这样的
public class Hoder {
private Apple apple;
// 构造器,get/set方法省略
}
可以看到这样的程序很不灵活,扩展性,通用度也不高,我们没法通过容器持有自己想要的对象。这时候通过泛型就可以解决这样的问题。
public class Hoder<T> {
private T item;
public Hoder(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
- T 表示 类型参数,用尖括号括起来放在类型的后面。
- 这时候我们就可以暂时不指定类型,等稍后使用的时候再去决定使用什么样的类型。
- 这里的T是Type的缩写,表示类型。
2.2泛型接口
泛型不仅可以应用于类也可以应用于接口。例如生成器(Generator),实际上这是工厂方法设计模式的一种应用,不过当使用设国车给其创建新对象的时候不需要任何的参数。而工厂方法一般需要参数。也就是说无需需要额外的信息就可以创建新的对象。
定义一个Generator泛型接口。
public interface Generator<T> {
T next();
}
再定义一些其他的类。
public class Coffee {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + id;
}
}
public class Americano extends Coffee {
}
public class Breve extends Coffee {
}
public class Cappuccino extends Coffee {
}
public class Latte extends Coffee {
}
public class Mocha extends Coffee {
}
这时候我们可以编写一个类,实现Generator<Coffee>接口,他能够随机生成不同的Coffee对象
public class CoffeeGenerator implements Generator<Coffee>,Iterable<Coffee> {
private Class[] types = {Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class};
private static Random random = new Random();
private int size;
public CoffeeGenerator() {
}
public CoffeeGenerator(int size) {
this.size = size;
}
@Override
public Coffee next() {
try {
return (Coffee)types[random.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException();
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for(int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
for (Coffee c : new CoffeeGenerator(5)) {
System.out.println(c);
}
// 结果
/**
* Cappuccino0
* Breve1
* Breve2
* Breve3
* Breve4
* Latte5
* Americano6
* Mocha7
* Breve8
* Americano9
*/
}
}
2.3泛型方法
到目前为止,泛型都是应用于类上的。但是同样的在类中包含,参数化的方法。
public <T> void fn(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
gm.fn("Hello,World!");
gm.fn(1);
gm.fn(1.0);
gm.fn(1.23F);
gm.fn(gm);
// 结果
/**
* java.lang.String
* java.lang.Integer
* java.lang.Double
* java.lang.Float
* com.thikinjava.ch15generic.genericmethod.GenericMethod
*/
}
- 泛型方法和泛型类没有任何关系。泛型方法所在的类既可以是泛型类,也可以不是泛型类。 也就是说,是否拥有泛型方法,与其所在的类是否有泛型没有关系
- 无论何时,只要你能做到,尽量使用泛型方法。如果可以用泛型方法取代整个类进行泛化,那么就使用泛型方法。
- 对于static方法而言,无法访问泛型类的参数类型,如果要让其拥有访问泛型的能力,就声明其为泛型方法。
- 在使用泛型类的时候,在实例化的时候通常要指定类型参数的值。而使用泛型方法的时候不需要指名参数类型,编译器会自动找出具体的类型,这称作"类型参数判断",我们可以像调用普通方法一样,调用泛型方法,就好像fn()被重载过无数次。在方法返回值前用尖括号括起来<T>来声明一个泛型方法。
3.擦除机制
3.1 擦除的神秘面纱
观察下面这个例子。
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
// 结果 true
}
ArrayList<String>和ArrayList<Integer>很容以被认为是不同的类型,不同的类型在
行为上肯定也有所不同。
java的泛型是使用擦除来实现的。这意味着当你在使用泛型的时候,任何具体的
类型信息都被擦除了,你唯一知道的就是你在使用一个对象,因为List<String>
和List<Integer>在运行时是相同的类型,这两种形式被擦除成了"原生"的类型,即List
在泛型的内部,无法获得任何有关泛型参数类型的信息。
再看下面这个例子。
假设我们定义一个容器,持有一个对象,并且想调用这个对象的特定方法(FMethod类的f()方法)
public class FMethod {
public void f() {
System.out.println("f方法被调用");
}
}
public class Manipulator<T> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void invoke() {
obj.f(); // 编译出错 cannot resolve method f();
}
public static void main(String[] args) {
FMethod fMethod = new FMethod();
Manipulator<FMethod> manipulator = new Manipulator<>(fMethod);
manipulator.invoke();
}
}
这时候是不行的由于擦除机制,泛型T会被擦除为Object,我们无法直接调用FMethod对象的f()方法。
有没有什么办法可以让编译器知道f()是FMethod类中的方法呢??
是有的。我们在 Manipulator<T>的泛型后面加上 extend FMethod, 完整的类的声明就像这样 public class Manipulator<T extends FMethod> {...}
这段代码的含义是 T 必须具有类型FMethod 或者是 FMethod的导出类,泛型类型参数将被擦除到他的第一个边界,即FMethod,就好像T被FMethod替换了一样。
3.2擦除的前世今生
为了减少擦除的混淆,我们必须要知道的是,擦除不是一个语言特定,而是Java实现泛型的一种折中的解决方法。因为泛型不是在java语言出现时就有的组成部分。这种折中是必须的也是痛苦的。
在基于擦除的实现中,泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都会被擦除,替换为他们的非泛型上界,例如List<T>这样的类型将会被替换为List,而普通的类型变量
在未指定边界的情况下会被擦除为Object ,擦除的核心动机是它将泛化的客户端代码可以用非泛化的类库来替换,我们不仅要考虑泛化了的程序,也要考虑,Java SE5之前非泛型类库的代码兼容性问题。为了这种兼容性,Java设计者们认为擦除是唯一可行的解决办法,使得泛型代码和非泛型代码共存成为了可能。
3.3擦除的代价
擦除实现了从非泛化代码向泛化代码的过度,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用。
在他给我带来很好的编程体验的同时,随之而来也面临着一些问题。泛型不能显示的引用运行时类型的操作之中。如转型,instanceof操作符,new 表达式。因为有关参数的类型信息全部丢失了。
4.通配符
在了解通配符之前,先来了解一下数组。Java 中的数组是协变的,什么意思?看下面的例子:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
}
/* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~
main 方法中的第一行,创建了一个 Apple 数组并把它赋给 Fruit 数组的引用。这是有意义的,Apple 是 Fruit 的子类,一个 Apple 对象也是一种 Fruit 对象,所以一个 Apple 数组也是一种 Fruit 的数组。这称作数组的协变,Java 把数组设计为协变的,对此是有争议的,有人认为这是一种缺陷。
尽管 Apple[] 可以 “向上转型” 为 Fruit[],但数组元素的实际类型还是 Apple,我们只能向数组中放入 Apple或者 Apple 的子类。在上面的代码中,向数组中放入了 Fruit 对象和 Orange 对象。对于编译器来说,这是可以通过编译的,但是在运行时期,JVM 能够知道数组的实际类型是 Apple[],所以当其它对象加入数组的时候就会抛出异常。
泛型设计的目的之一是要使这种运行时期的错误在编译期就能发现,看看用泛型容器类来代替数组会发生什么:
// Compile Error: incompatible types:
ArrayList<Fruit> flist = new ArrayList<Apple>();
上面的代码根本就无法编译。当涉及到泛型时, 尽管 Apple 是 Fruit 的子类型,但是 ArrayList<Apple> 不是 ArrayList<Fruit> 的子类型,泛型不支持协变。
使用通配符从上面我们知道,List<Number> list = ArrayList<Integer> 这样的语句是无法通过编译的,尽管 Integer 是 Number的子类型。那么如果我们确实需要建立这种 “向上转型” 的关系怎么办呢?这就需要通配符来发挥作用了。
4.1上边界限定通配符
利用 <? extends Fruit> 形式的通配符,可以实现泛型的向上转型:
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
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());
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
上面的例子中, flist 的类型是 List<? extends Fruit>,我们可以把它读作:一个类型的 List, 这个类型可以是继承了 Fruit 的某种类型。注意,这并不是说这个 List 可以持有 Fruit 的任意类型。通配符代表了一种特定的类型,它表示 “某种特定的类型,但是 flist 没有指定”。这样不太好理解,具体针对这个例子解释就是,flist 引用可以指向某个类型的 List,只要这个类型继承自 Fruit,可以是 Fruit 或者 Apple,比如例子中的 new ArrayList<Apple>,但是为了向上转型给 flist,flist 并不关心这个具体类型是什么。
如上所述,通配符 List<? extends Fruit> 表示某种特定类型 ( Fruit 或者其子类 ) 的 List,但是并不关心这个实际的类型到底是什么,反正是 Fruit 的子类型,Fruit 是它的上边界。那么对这样的一个 List 我们能做什么呢?其实如果我们不知道这个 List 到底持有什么类型,怎么可能安全的添加一个对象呢?在上面的代码中,向 flist 中添加任何对象,无论是 Apple 还是 Orange 甚至是 Fruit 对象,编译器都不允许,唯一可以添加的是 null。所以如果做了泛型的向上转型 (List<? extends Fruit> flist = new ArrayList<Apple>()),那么我们也就失去了向这个 List 添加任何对象的能力,即使是 Object 也不行。
另一方面,如果调用某个返回 Fruit 的方法,这是安全的。因为我们知道,在这个 List 中,不管它实际的类型到底是什么,但肯定能转型为 Fruit,所以编译器允许返回 Fruit。
了解了通配符的作用和限制后,好像任何接受参数的方法我们都不能调用了。其实倒也不是,看下面的例子:
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is ‘Object’
flist.indexOf(new Apple()); // Argument is ‘Object’
//flist.add(new Apple()); 无法编译
}
}
在上面的例子中,flist 的类型是 List<? extends Fruit>,泛型参数使用了受限制的通配符,所以我们失去了向其中加入任何类型对象的例子,最后一行代码无法编译。
但是 flist 却可以调用 contains 和 indexOf 方法,它们都接受了一个 Apple 对象做参数。如果查看 ArrayList 的源代码,可以发现 add() 接受一个泛型类型作为参数,但是 contains 和 indexOf 接受一个 Object 类型的参数,下面是它们的方法签名:
public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)
所以如果我们指定泛型参数为 <? extends Fruit> 时,add() 方法的参数变为 ? extends Fruit,编译器无法判断这个参数接受的到底是 Fruit 的哪种类型,所以它不会接受任何类型。
然而,contains 和 indexOf 的类型是 Object,并没有涉及到通配符,所以编译器允许调用这两个方法。这意味着一切取决于泛型类的编写者来决定那些调用是 “安全” 的,并且用 Object 作为这些安全方法的参数。如果某些方法不允许类型参数是通配符时的调用,这些方法的参数应该用类型参数,比如 add(E e)。
当我们自己编写泛型类时,上面介绍的就有用了。下面编写一个 Holder 类:
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Cannot upcast
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns ‘Object’
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
在 Holer 类中,set() 方法接受类型参数 T 的对象作为参数,get() 返回一个 T 类型,而 equals() 接受一个 Object作为参数。fruit 的类型是 Holder<? extends Fruit>,所以set()方法不会接受任何对象的添加,但是 equals() 可以正常工作。
4.2下边界限定通配符
通配符的另一个方向是 “超类型的通配符“: ? super T,T 是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。还是用例子解释:
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
writeTo 方法的参数 apples 的类型是 List<? super Apple>,它表示某种类型的 List,这个类型是 Apple 的基类型。也就是说,我们不知道实际类型是什么,但是这个类型肯定是 Apple 的父类型。因此,我们可以知道向这个 List 添加一个 Apple 或者其子类型的对象是安全的,这些对象都可以向上转型为 Apple。但是我们不知道加入 Fruit 对象是否安全,因为那样会使得这个 List 添加跟 Apple 无关的类型。
在了解了子类型边界和超类型边界之后,我们就可以知道如何向泛型类型中 “写入” ( 传递对象给方法参数) 以及如何从泛型类型中 “读取” ( 从方法中返回对象 )。下面是一个例子:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src){
for (int i=0; i<src.size(); i++)
dest.set(i,src.get(i));
}
}
src 是原始数据的 List,因为要从这里面读取数据,所以用了上边界限定通配符:<? extends T>,取出的元素转型为 T。dest 是要写入的目标 List,所以用了下边界限定通配符:<? super T>,可以写入的元素类型是 T 及其子类型。
4.3无边界通配符
还有一种通配符是无边界通配符,它的使用形式是一个单独的问号:List<?>,也就是没有任何限定。不做任何限制,跟不用类型参数的 List 有什么区别呢?
List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。而单独的 List list ,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。
网友评论