[关闭]
@lemonguge 2015-06-23T02:31:44.000000Z 字数 9835 阅读 395

泛型(一)

JAVA


在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。这样做能够具备更好的灵活性,但是,考虑到除了final类不能扩展,所以这种灵活性大多时候也会有一些性能损耗。

但是,这样做拘泥于单继承体系,也会使程序受限太多。如果方法的参数是一个接口,而不是一个类,这种限制就放松了许多。可是有的时候,即便使用了接口,对程序的约束性也还是太强。因为一旦指明了接口,它就要求你实现该接口的所有方法。

泛型的目的:为了编写更通用的代码,使代码能够应用于“某种不具体的类型”,而不是一个具体的接口或类。

这就是Java SE5的重大变化之一:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。有很多原因促成了泛型的出现,而最引入注目的一个原因,就是为了创造容器类。此时泛型是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。

在Java SE5之前,我们使用Object类型来存储任何类型的对象,在使用时必须进行强制类型转换。在泛型出现之后,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型,编译器会为我们负责转型操作,并且保证类型的正确性。

Java泛型的一个局限性:基本类型无法作为类型参数,不过可以用相对应的包装类型进行转换。


泛型类

泛型类:使用类型参数,用尖括号扩住,放在类名后面。

使用这个类的时候,再用实际的类型替换此类型参数。在下面的例子中,T就是类型参数(type)。

  1. class Cellphone { }
  2. // 泛型类
  3. public class Holder<T> { // T为类型参数,不固定可自定义
  4. private T a;
  5. public Holder(T a) { this.a = a; }
  6. public void set(T a) { this.a = a; }
  7. public T get() { return a; }
  8. public static void main(String[] args) {
  9. // 创建对象的时候指明了类型参数的值
  10. Holder<Cellphone> h3 = new Holder<Cellphone>(new Cellphone());
  11. Cellphone a = h3.get(); // No cast needed
  12. // h3.set("Not an Cellphone"); // Error
  13. // h3.set(1); // Error
  14. }
  15. }

使用泛型类时,必须在创建对象的时候指定类型参数的值。


泛型接口

泛型也可以用于接口。例如生成器(generator,这是一种专门负责创建对象的类,生成器无需额外的信息就知道如何创建新对象。

  1. interface Generator<T> { T next(); } // 生成器,方法的返回类型是参数化的T
  2. // 向上抽取固定电话和移动电话而产生的基类
  3. class Phone {
  4. private static int counter = 0;
  5. private final int id = counter++;
  6. @Override
  7. public String toString() { return getClass().getSimpleName() + " " + id; }
  8. }
  9. class Telephone extends Phone { } // 固定电话
  10. class Cellphone extends Phone { } // 移动电话
  11. // 实现了生成器接口的类,用于生成电话
  12. public class Generate implements Generator<Phone>{
  13. private Class[] phones = { Telephone.class, Cellphone.class };
  14. private static Random rand = new Random(47);
  15. private int size ;
  16. public Generate(int size) {
  17. this.size = size;
  18. }
  19. @Override
  20. public Phone next() { // 实现接口中的方法,随机产生电话的导出类
  21. try {
  22. if(size-->0)
  23. return (Phone) phones[rand.nextInt(phones.length)].newInstance();
  24. return null;
  25. } catch (Exception e) {
  26. throw new RuntimeException(e);
  27. }
  28. }
  29. public static void main(String[] args) {
  30. Generate gen = new Generate(3);
  31. for (int i = 0; i < 5; i++)
  32. System.out.println(gen.next());
  33. }
  34. } /* Output:
  35. Cellphone 0
  36. Telephone 1
  37. Cellphone 2
  38. null
  39. null
  40. *///:~

泛型方法

泛型方法的定义:只需将泛型参数列表置于返回值之前。

  1. public class GenericMethods {
  2. public <T> void f(T x) {
  3. System.out.println(x.getClass().getName());
  4. }
  5. public static void main(String[] args) {
  6. GenericMethods gm = new GenericMethods();
  7. gm.f("");
  8. gm.f(1); // 自动装箱
  9. gm.f(1.0);
  10. gm.f(1.0F);
  11. gm.f('c'); // 等价于gm.<java.lang.Character>f('c'),显式指明类型
  12. gm.f(gm);
  13. }
  14. } /* Output:
  15. java.lang.String
  16. java.lang.Integer
  17. java.lang.Double
  18. java.lang.Float
  19. java.lang.Character
  20. generics.GenericMethods
  21. *///:~

是否拥有泛型方法,与其所在的类是否是泛型没有关系。泛型方法使得该方法能独立于类而产生变化,无论何时,只要你能做到,你就应该尽量使用泛型方法。

类型参数推断:使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。

编译器要么通过传入参数判断,要么通过返回类型来判断。

当然,在泛型方法中,我们也可以显式地指明类型。显式类型参数说明:必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。在定义该方法的类的内部,如果是static方法,点操作符之前加上类名;否则在点操作符之前加上this


泛型的擦除与补偿

擦除的出现是:为了原来的类库兼容才使用了擦除,将会把类型参数T换成Object

擦除

我们可以声明ArrayList.class,但是不可以声明ArrayList<String>.class

  1. public class ErasedTypeEquivalence {
  2. public static void main(String[] args) {
  3. Class c1 = new ArrayList<String>().getClass();
  4. Class c2 = new ArrayList<Integer>().getClass();
  5. System.out.println(c1 == c2);
  6. }
  7. } /* Output:
  8. true
  9. *///:~

ArrayList<String>ArrayList<Integer>很容易认为是不同的类型,实际上却是同样的类型,为什么会这样呢?

  1. import java.util.*;
  2. class Frob {}
  3. class Fnorkle {}
  4. class Quark<Q> {}
  5. class Particle<POSITION,MOMENTUM> {}
  6. public class LostInformation {
  7. // 泛型声明所声明的类型参数
  8. public static void show(Class clazz) {
  9. System.out.println(Arrays.toString(clazz.getTypeParameters()));
  10. }
  11. public static void main(String[] args) {
  12. List<Frob> list = new ArrayList<Frob>();
  13. Map<Frob, Fnorkle> map = new HashMap<Frob, Fnorkle>();
  14. Quark<Fnorkle> quark = new Quark<Fnorkle>();
  15. Particle<Long, Double> p = new Particle<Long, Double>();
  16. show(list.getClass());
  17. show(map.getClass());
  18. show(quark.getClass());
  19. show(p.getClass());
  20. }
  21. } /* Output:
  22. [E]
  23. [K, V]
  24. [Q]
  25. [POSITION, MOMENTUM]
  26. *///:~

从输出中看到,只是用作参数占位符的标识符,并非有用的信息。ArrayList<String>ArrayList<Integer>都被擦除成它们的“原生类型”,即ArrayList

在泛型代码内部,无法获得任何有关泛型参数类型的信息,把类型参数当作一个Object来使用。

  1. class Foo<T> {
  2. T var; // 把这个参数化类型当作一个Object
  3. }

在泛型代码内部,不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息已经被擦除了,它只是一个Object

  1. public class Erased {
  2. public static <T> void f(Object arg) {
  3. // Cannot perform instanceof check against type parameter T.
  4. // Use its erasure Object instead since further generic type information will be erased at runtime
  5. // ! if (arg instanceof T) { }
  6. // ! T var = new T(); // Cannot instantiate the type T
  7. // ! T[] array = new T[5]; // Cannot create a generic array of T
  8. T[] array = (T[])new Object[5]; // Unchecked cast from Object[] to T[]
  9. }
  10. }

擦除的补偿

在泛型代码内部,任何在运行时需要知道确切类型信息的操作都将无法工作,因为其类型信息已经被擦除了。不过我们可以类型标记Class<T>传入到泛型代码中,以便从擦除中恢复。因为类型标记是创建该类任何对象的蓝图,包含该类所有的信息,所以类型标记Class对象又称为最便利的工厂对象。

在编译时,会存储元素会进行类型检查;在运行时,通过获取元素的类型进行自动转型动作。

创建参数化类型实例

由于获取不到类型信息,所以不能通过new T()来实现。可以通过使用类型标记的newInstance()调用默认的构造器来创建这个类型的新对象。

  1. class Employee {}
  2. public class Erased {
  3. public static <T> T create(Class<T> kind) {
  4. // ! T var = new T();
  5. try {
  6. T var = kind.newInstance();
  7. return var;
  8. } catch (Exception e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. public static void main(String[] args) {
  13. Employee e = create(Employee.class); // OK
  14. // ! Integer i = create(Integer.class); // RuntimeException
  15. }
  16. } /* Output:
  17. Exception in thread "main" java.lang.RuntimeException: java.lang.InstantiationException: java.lang.Integer
  18. at Erased.create(Erased.java:9)
  19. at Erased.main(Erased.java:14)
  20. Caused by: java.lang.InstantiationException: java.lang.Integer
  21. at java.lang.Class.newInstance(Class.java:359)
  22. at Erased.create(Erased.java:6)
  23. ... 1 more
  24. *///:~

上面这段代码可以通过编译,但是无法创建Integer的实例,因为Integer没有任何默认的构造器。由于这个错误是运行期异常,所以Sun的伙伴们对这种方法并不赞成,他们建议使用显式的工厂,并将限制其类型,使得只能接受实现了这个工厂的类:

  1. interface FactoryI<T> { T create(); } // 一个额外的功能,工厂用于获取实例对象
  2. class IntegerFactory implements FactoryI<Integer>{
  3. @Override
  4. public Integer create() {
  5. return 0; // 自动包装
  6. }
  7. }
  8. class Widget{}
  9. class WidgetFactory implements FactoryI<Widget>{
  10. @Override
  11. public Widget create() {
  12. return new Widget();
  13. }
  14. }
  15. class Foo<T>{
  16. private T t;
  17. public <F extends FactoryI<T>> Foo(F factory){ // extends用来确定上限
  18. t = factory.create(); // 在泛型代码内,使用工厂来获取参数化类型的对象
  19. }
  20. }
  21. public class FactoryConstraint {
  22. public static void main(String[] args) {
  23. new Foo<>(new IntegerFactory()); // OK
  24. new Foo<>(new WidgetFactory()); // OK
  25. }
  26. } ///:~

当然,我们也可以使用模版方法设计模式。在泛型类中,我们仅仅定义好一个基本框架,将具体的实现延迟到子类。

  1. abstract class TemplateCreate<T>{
  2. final T element;
  3. public TemplateCreate() {
  4. this.element = create();
  5. }
  6. public abstract T create(); // 后期绑定的方法
  7. void show(){ System.out.println(element.getClass().getSimpleName()); }
  8. }
  9. class X { }
  10. class XCreate extends TemplateCreate<X>{
  11. @Override
  12. public X create() { return new X(); } // 实现模版方法
  13. }
  14. class IntegerCreate extends TemplateCreate<Integer>{
  15. @Override
  16. public Integer create() { return 1; }
  17. }
  18. public class Template {
  19. public static void main(String[] args) {
  20. new XCreate().show();
  21. new IntegerCreate().show();
  22. }
  23. } /* Output:
  24. X
  25. Integer
  26. *///:~

动态的isInstance()

在泛型代码内部,可以使用类型标记的isInstance()来代替instanceof

  1. class Building {}
  2. class House extends Building {}
  3. public class ClassTypeCapture<T> {
  4. Class<T> kind;
  5. public ClassTypeCapture(Class<T> kind) {
  6. this.kind = kind;
  7. }
  8. public boolean check(Object arg) {
  9. return kind.isInstance(arg);
  10. }
  11. public static void main(String[] args) {
  12. ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(
  13. Building.class);
  14. System.out.println(ctt1.check(new Building()));
  15. System.out.println(ctt1.check(new House()));
  16. ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
  17. System.out.println(ctt2.check(new Building()));
  18. System.out.println(ctt2.check(new House()));
  19. }
  20. } /* Output:
  21. true
  22. true
  23. false
  24. true
  25. *///:~

泛型数组

正如之前所看到的// ! T var = new T();代码,我们无法创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList

创建泛型类数组

我们知道数组也是一个对象,数组被创建时就已经确定了它们的实际类型。强制将Object[]向下转型会出现ClassCastException,这是一个运行期异常。

  1. class Generic<T> { } // 泛型类
  2. public class ArrayOfGeneric {
  3. static final int SIZE = 100;
  4. static Generic<Integer>[] gia;
  5. @SuppressWarnings("unchecked")
  6. public static void main(String[] args) {
  7. // ClassCastException: [Ljava.lang.Object; cannot be cast to [Lgenerics.Generic;
  8. // ! gia = (Generic<Integer>[])new Object[SIZE];
  9. gia = (Generic<Integer>[]) new Generic[SIZE];
  10. System.out.println(gia.getClass().getSimpleName());
  11. gia[0] = new Generic<Integer>();
  12. // ! gia[1] = new Object(); // Compile-time error
  13. // Discovers type mismatch at compile time:
  14. // ! gia[2] = new Generic<Double>();
  15. }
  16. } /* Output:
  17. Generic[]
  18. *///:~

成功创建泛型类数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。

在泛型代码内创建参数化类型数组

正如之前所看到的T[] array = (T[])new Object[5];代码,我们能够在泛型代码内创建一个对象数组,然后将其转型,这样做仅会获得编译期警告。

  1. public class GenericArray<T> {
  2. private T[] array;
  3. @SuppressWarnings("unchecked")
  4. public GenericArray(int sz) {
  5. array = (T[]) new Object[sz];
  6. }
  7. public T[] rep() { return array; }
  8. public static void main(String[] args) {
  9. GenericArray<Integer> gai = new GenericArray<Integer>(10);
  10. // This causes a ClassCastException:
  11. // [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
  12. // ! Integer[] ia = gai.rep(); // 实际的运行类型是Object[]
  13. Object[] oa = gai.rep(); // OK
  14. }
  15. }

当我们在泛型代码内部,将Object[]转型为T[],那么在编译期该数组的实际类型就将丢失,导致编译器可能会错过某些潜在的错误检查。为了解决这个问题,我们泛型代码内就使用Object[],然后当我们使用数组元素时,添加一个对T的转型。

  1. public class GenericArray2<T> {
  2. private Object[] array;
  3. public GenericArray2(int sz) {
  4. array = new Object[sz];
  5. }
  6. public void put(int index, T item) {
  7. array[index] = item;
  8. }
  9. @SuppressWarnings("unchecked")
  10. // 使用数组元素时进行转型
  11. // 也许有人会想,T被擦除了为Object了,那Object转Object还有意义吗?这样转型的目的是为了编译期不报错。
  12. public T get(int index) {
  13. return (T) array[index];
  14. }
  15. @SuppressWarnings("unchecked")
  16. public T[] rep() {
  17. return (T[]) array; // Warning: Unchecked cast
  18. }
  19. public static void main(String[] args) {
  20. GenericArray2<Integer> gai = new GenericArray2<Integer>(10);
  21. for (int i = 0; i < 10; i++)
  22. gai.put(i, i);
  23. for (int i = 0; i < 10; i++)
  24. System.out.print(gai.get(i) + " ");
  25. System.out.println();
  26. try {
  27. Integer[] ia = gai.rep(); // 还是产生了异常
  28. } catch (Exception e) {
  29. System.out.println(e);
  30. }
  31. }
  32. } /* Output:
  33. 0 1 2 3 4 5 6 7 8 9
  34. java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
  35. *///:~

直到现在,我们仍然不能获取到参数化类型数组,它们始终是Object[]因此,没有任何一种方式可以推翻底层的数组类型!所以我们应该传递一个类型标记创建参数化类型数组。

java.lang.reflect.Array.newInstance(Class<?> componentType, int length)

  1. import java.lang.reflect.Array;
  2. import java.util.Arrays;
  3. public class GenericArrayWithTypeToken<T> {
  4. private T[] array;
  5. @SuppressWarnings("unchecked")
  6. public GenericArrayWithTypeToken(Class<T> type, int sz) {
  7. array = (T[]) Array.newInstance(type, sz);
  8. }
  9. public void put(int index, T item) {
  10. array[index] = item;
  11. }
  12. public T get(int index) { return array[index]; }
  13. public T[] rep() { return array; }
  14. public static void main(String[] args) {
  15. GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10);
  16. gai.put(0, 1); // 自动装箱
  17. Integer[] ia = gai.rep(); // OK
  18. System.out.println(Arrays.toString(ia));
  19. }
  20. } /* Output:
  21. [1, null, null, null, null, null, null, null, null, null]
  22. *///:~
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注