擦除


在泛型代码内部,无法获得任何有关泛型参数类型的信息。

例子1:

  1. //这个例子表明编译过程中并没有根据参数生成新的类型
  2. public class Main2 {
  3. public static void main(String[] args) {
  4. Class c1 = new ArrayList<Integer>().getClass();
  5. Class c2 = new ArrayList<String>().getClass();
  6. System.out.print(c1 == c2);
  7. }
  8. }
  9. /* output
  10. true
  11. */

List<String> 中添加 Integer 将不会通过编译,但是List<Sring>List<Integer>在运行时的确是同一种类型。

例子2:

  1. //例子, 这个例子表明类的参数类型跟传进去的类型没有关系,泛型参数只是`占位符`
  2. public class Table {
  3. }
  4. public class Room {
  5. }
  6. public class House<Q> {
  7. }
  8. public class Particle<POSITION, MOMENTUM> {
  9. }
  10. public class Main {
  11. public static void main(String[] args) {
  12. List<Table> tableList = new ArrayList<Table>();
  13. Map<Room, Table> maps = new HashMap<Room, Table>();
  14. House<Room> house = new House<Room>();
  15. Particle<Long, Double> particle = new Particle<Long, Double>();
  16. System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
  17. System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
  18. System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
  19. System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
  20. }
  21. }
  22. /** output
  23. [E]
  24. [K, V]
  25. [Q]
  26. [POSITION, MOMENTUM]
  27. */

我们在运行期试图获取一个已经声明的类的类型参数,发现这些参数依旧是‘形参’,并没有随声明改变。也就是说在运行期,我们是拿不到已经声明的类型的任何信息。

编译器会虽然在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

例子:

  1. List<String> stringList=new ArrayList<String>();
  2. //可以通过编译
  3. stringList.add("wakaka");
  4. //编译不通过
  5. //stringList.add(new Integer(0));
  6. //List.java
  7. public interface List<E> extends Collection<E> {
  8. //...
  9. boolean add(E e);
  10. //...
  11. }

List的参数类型是Eadd方法的参数类型也是E,他们在类的内部是一致的,所以添加Integer类型的对象到stringList违反了内部类型一致,不能通过编译。

重用 extends 关键字。通过它能给与参数类型添加一个边界。

泛型参数将会被擦除到它的第一个边界(边界可以有多个)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HasF类型来使用。

例子:

  1. /** * Created by yxf on 16-5-28. */
  2. // HasF.java
  3. public interface HasF {
  4. void f();
  5. }
  6. //Manipulator.java
  7. public class Manipulator<T extends HasF> {
  8. T obj;
  9. public T getObj() {
  10. return obj;
  11. }
  12. public void setObj(T obj) {
  13. this.obj = obj;
  14. }
  15. }

extend关键字后后面的类型信息决定了泛型参数能保留的信息。

Java中擦除的基本原理

刚看到这里可能有些困惑,一个泛型类型没有保留具体声明的类型的信息,那它是怎么工作的呢?在把《Java编程思想》书中这里的边界与上文的边界区分开来之后,终于想通了。Java的泛型类的确只有一份字节码,但是在使用泛型类的时候编译器做了特殊的处理。

这里根据作者的思路,自己动手写了两个类SimpleHolderGenericHolder,然后编译拿到两个类的字节码,直接贴在这里:

  1. // SimpleHolder.java
  2. public class SimpleHolder {
  3. private Object obj;
  4. public Object getObj() {
  5. return obj;
  6. }
  7. public void setObj(Object obj) {
  8. this.obj = obj;
  9. }
  10. public static void main(String[] args) {
  11. SimpleHolder holder = new SimpleHolder();
  12. holder.setObj("Item");
  13. String s = (String) holder.getObj();
  14. }
  15. }
  16. // SimpleHolder.class
  17. public class SimpleHolder {
  18. public SimpleHolder();
  19. Code:
  20. 0: aload_0
  21. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  22. 4: return
  23. public java.lang.Object getObj();
  24. Code:
  25. 0: aload_0
  26. 1: getfield #2 // Field obj:Ljava/lang/Object;
  27. 4: areturn
  28. public void setObj(java.lang.Object);
  29. Code:
  30. 0: aload_0
  31. 1: aload_1
  32. 2: putfield #2 // Field obj:Ljava/lang/Object;
  33. 5: return
  34. public static void main(java.lang.String[]);
  35. Code:
  36. 0: new #3 // class SimpleHolder
  37. 3: dup
  38. 4: invokespecial #4 // Method "<init>":()V
  39. 7: astore_1
  40. 8: aload_1
  41. 9: ldc #5 // String Item
  42. 11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
  43. 14: aload_1
  44. 15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
  45. 18: checkcast #8 // class java/lang/String
  46. 21: astore_2
  47. 22: return
  48. }
  1. //GenericHolder.java
  2. public class GenericHolder<T> {
  3. T obj;
  4. public T getObj() {
  5. return obj;
  6. }
  7. public void setObj(T obj) {
  8. this.obj = obj;
  9. }
  10. public static void main(String[] args) {
  11. GenericHolder<String> holder = new GenericHolder<>();
  12. holder.setObj("Item");
  13. String s = holder.getObj();
  14. }
  15. }
  16. //GenericHolder.class
  17. public class GenericHolder<T> {
  18. T obj;
  19. public GenericHolder();
  20. Code:
  21. 0: aload_0
  22. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  23. 4: return
  24. public T getObj();
  25. Code:
  26. 0: aload_0
  27. 1: getfield #2 // Field obj:Ljava/lang/Object;
  28. 4: areturn
  29. public void setObj(T);
  30. Code:
  31. 0: aload_0
  32. 1: aload_1
  33. 2: putfield #2 // Field obj:Ljava/lang/Object;
  34. 5: return
  35. public static void main(java.lang.String[]);
  36. Code:
  37. 0: new #3 // class GenericHolder
  38. 3: dup
  39. 4: invokespecial #4 // Method "<init>":()V
  40. 7: astore_1
  41. 8: aload_1
  42. 9: ldc #5 // String Item
  43. 11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
  44. 14: aload_1
  45. 15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
  46. 18: checkcast #8 // class java/lang/String
  47. 21: astore_2
  48. 22: return
  49. }

经过一番比较之后,发现两分源码虽然不同,但是对应的字节码逻辑部分确是完全相同的。

在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了‘还原’返回结果的类型,编译器在get之后添加了类型转换。所以,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。

所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。

擦除的缺陷

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceofnew。因为在运行时,所有参数的类型信息都丢失了。

  1. public class Erased<T> {
  2. private final int SIZE = 100;
  3. public static void f(Object arg) {
  4. //编译不通过
  5. if (arg instanceof T) {
  6. }
  7. //编译不通过
  8. T var = new T();
  9. //编译不通过
  10. T[] array = new T[SIZE];
  11. //编译不通过
  12. T[] array = (T) new Object[SIZE];
  13. }
  14. }

擦除的补偿

1. 类型判断问题

例子:

  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 f(Object arg) {
  9. return kind.isInstance(arg);
  10. }
  11. public static void main(String[] args) {
  12. ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
  13. System.out.println(ctt1.f(new Building()));
  14. System.out.println(ctt1.f(new House()));
  15. ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
  16. System.out.println(ctt2.f(new Building()));
  17. System.out.print(ctt2.f(new House()));
  18. }
  19. }
  20. //output
  21. //true
  22. //true
  23. //false
  24. //true

泛型参数的类型无法用instanceof关键字来做判断。所以我们使用类类型来构造一个类型判断器,判断一个实例是否为特定的类型。

2. 创建类型实例

Erased.java中不能new T()的原因有两个,一是因为擦除,不能确定类型;而是无法确定T是否包含无参构造函数。

为了避免这两个问题,我们使用显式的工厂模式:

例子:

  1. interface IFactory<T> {
  2. T create();
  3. }
  4. class Foo2<T> {
  5. private T x;
  6. public <F extends IFactory<T>> Foo2(F factory) {
  7. x = factory.create();
  8. }
  9. }
  10. class IntegerFactory implements IFactory<Integer> {
  11. @Override
  12. public Integer create() {
  13. return new Integer(0);
  14. }
  15. }
  16. class Widget {
  17. public static class Factory implements IFactory<Widget> {
  18. @Override
  19. public Widget create() {
  20. return new Widget();
  21. }
  22. }
  23. }
  24. public class FactoryConstraint {
  25. public static void main(String[] args) {
  26. new Foo2<Integer>(new IntegerFactory());
  27. new Foo2<Widget>(new Widget.Factory());
  28. }
  29. }

通过特定的工厂类实现特定的类型能够解决实例化类型参数的需求。

3. 创建泛型数组

一般不建议创建泛型数组。尽量使用ArrayList来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。

  1. public class GenericArrayWithTypeToken<T> {
  2. private T[] array;
  3. @SuppressWarnings("unchecked")
  4. public GenericArrayWithTypeToken(Class<T> type, int sz) {
  5. array = (T[]) Array.newInstance(type, sz);
  6. }
  7. public void put(int index, T item) {
  8. array[index] = item;
  9. }
  10. public T[] rep() {
  11. return array;
  12. }
  13. public static void main(String[] args) {
  14. GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10);
  15. Integer[] ia = gai.rep();
  16. }
  17. }

这里我们使用的还是传参数类型,利用类型的newInstance方法创建实例的方式。

边界


这里Java重用了 extend关键字。边界可以将类型参数的范围限制到一个子集当中。

  1. interface HasColor {
  2. Color getColor();
  3. }
  4. class Colored<T extends HasColor> {
  5. T item;
  6. public Colored(T item) {
  7. this.item = item;
  8. }
  9. public T getItem() {
  10. return item;
  11. }
  12. public Color color() {
  13. return item.getColor();
  14. }
  15. }
  16. class Dimension {
  17. public int x, y, z;
  18. }
  19. class ColoredDemension<T extends HasColor & Dimension> {
  20. T item;
  21. public ColoredDemension(T item) {
  22. this.item = item;
  23. }
  24. public T getItem() {
  25. return item;
  26. }
  27. Color color() {
  28. return item.getColor();
  29. }
  30. int getX() {
  31. return item.x;
  32. }
  33. int getY() {
  34. return item.y;
  35. }
  36. int getZ() {
  37. return item.z;
  38. }
  39. }
  40. interface Weight {
  41. int weight();
  42. }
  43. class Solid<T extends Dimension & HasColor & Weight> {
  44. T item;
  45. public Solid(T item) {
  46. this.item = item;
  47. }
  48. public T getItem() {
  49. return item;
  50. }
  51. Color color() {
  52. return item.getColor();
  53. }
  54. int getX() {
  55. return item.x;
  56. }
  57. int getY() {
  58. return item.y;
  59. }
  60. int getZ() {
  61. return item.z;
  62. }
  63. int weight() {
  64. return item.weight();
  65. }
  66. }
  67. class Bounded extends Dimension implements HasColor, Weight {
  68. @Override
  69. public Color getColor() {
  70. return null;
  71. }
  72. @Override
  73. public int weight() {
  74. return 0;
  75. }
  76. }
  77. public class BasicBound {
  78. public static void main(String[] args) {
  79. Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
  80. solid.color();
  81. solid.weight();
  82. solid.getZ();
  83. }
  84. }

extends关键字声明中,有两个要注意的地方:

  1. 类必须要写在接口之前;
  2. 只能设置一个类做边界,其它均为接口。

通配符


协变:

  1. public class Holder<T> {
  2. private T value;
  3. public Holder(T apple) {
  4. }
  5. public T getValue() {
  6. return value;
  7. }
  8. public void setValue(T value) {
  9. this.value = value;
  10. }
  11. @Override
  12. public boolean equals(Object o) {
  13. return value != null && value.equals(o);
  14. }
  15. public static void main(String[] args) {
  16. Holder<Apple> appleHolder = new Holder<Apple>(new Apple());
  17. Apple d = new Apple();
  18. appleHolder.setValue(d);
  19. // 不能自动协变
  20. // Holder<Fruit> fruitHolder=appleHolder;
  21. // 借助 ? 通配符和 extends 关键字可以实现协变
  22. Holder<? extends Fruit> fruitHolder = appleHolder;
  23. // 返回一个Fruit,因为添加边界之后返回的对象是 ? extends Fruit,
  24. // 可以把它转型为Apple,但是在不知道具体类型的时候存在风险
  25. d = (Apple) fruitHolder.getValue();
  26. //Fruit以及Fruit的父类,就不需要转型
  27. Fruit fruit = fruitHolder.getValue();
  28. Object obj = fruitHolder.getValue();
  29. try {
  30. Orange c = (Orange) fruitHolder.getValue();
  31. } catch (Exception e) {
  32. System.out.print(e);
  33. }
  34. // 编译不通过,因为编译阶段根本不知道子类型到底是什么类型
  35. // fruitHolder.setValue(new Apple());
  36. // fruitHolder.setValue(new Orange());
  37. //这里是可以的因为equals方法接受的是Object作为参数,并不是 ? extends Fruit
  38. System.out.print(fruitHolder.equals(d));
  39. }
  40. }

在Java中父类型可以持有子类型。如果一个父类的容器可以持有子类的容器,那么我们就可以称为发生了协变。在java中,数组是自带协变的,但是泛型的容器没有自带协变。我们可以根据利用边界和通配符?来实现近似的协变。

Holder<? extends Fruit>就是一种协变的写法。它表示一个列表,列表持有的类型是Fruit或其子类。

这个Holder<? extends Fruit>运行时持有的类型是未知的,我们只知道它一定是Fruit的子类。正因为如此,所以我们无法向这个holder中放入任何类型的对象,Object类型的对象也不可以。但是,调用它的返回方法却是可以的。因为边界明确定义了它是Fruit类型的子类。

逆变:

  1. package wildcard;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public class GenericWriting {
  5. static <T> void writeExact(List<T> list, T item) {
  6. list.add(item);
  7. }
  8. static List<Apple> apples = new ArrayList<Apple>();
  9. static List<Fruit> fruits = new ArrayList<Fruit>();
  10. static void f1() {
  11. writeExact(apples, new Apple());
  12. //this cannot be compile,said in Thinking in Java
  13. writeExact(fruits, new Apple());
  14. }
  15. static <T> void writeWithWildcard(List<? super T> list, T item) {
  16. list.add(item);
  17. }
  18. static void f2() {
  19. writeWithWildcard(apples, new Apple());
  20. writeWithWildcard(fruits, new Apple());
  21. }
  22. static <T> readWithWildcard(List<? super T> list, int index) {
  23. //Compile Error, required T but found Object
  24. return list.get(index);
  25. }
  26. public static void main(String[] args) {
  27. f1();
  28. f2();
  29. }
  30. }

如果一个类的父类型容器可以持有该类的子类型的容器,我们称这种关系为逆变。声明方式List<? super Integer>, List<? super T> list

不能给泛型参数给出一个超类型边界;即不能声明List<T super MyClass>

上面的例子中,writeExact(fruits,new Apple());在《Java编程思想》中说是不能通过编译的,但我试了一下,在Java1.6,Java1.7中是可以编译的。不知道是不是编译器比1.5版本升级了。

由于给出了参数类型的‘下界’,所以我们可以在列表中添加数据而不会出现类型错误。但是使用get方法获取返回类型的时候要注意,由于声明的类型区间是Object到T具有继承关系的类。所以返回的类型为了确保没有问题,都是以Object类型返回回来的。比如过例子中list.get(index)的返回类型就是Object

无界通配符

无界通配符<?> 意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。举个例子,在List<?>类型的引用中,不能向其中添加Object, 而List类型的引用就可以添加Object类型的变量。

一些需要注意的问题


1. 任何基本类型都不能作为类型参数

2. 实现参数化接口

例子:

  1. interface Payable<T>{}
  2. class Employee implements Payable<Employee> {}
  3. //Compile Error
  4. class Hourly extends Employee implements Payable<Hourly> {}

因为擦除的原因,Payable<Employee>Payable<Hourly>简化为相同的Payable<Object>,例子中的代码意味着重复两次实现相同的接口。但他们的参数类型却是不相同的。

3. 转型和警告

使用带有泛型类型参数的转型或者instanceof不会有任何效果。因为他们在运行时都会被擦除到上边界上。所以转型的时候用的类型实际上是上边解对应的类型。

4. 重载

  1. //Compile Error. 编译不能通过
  2. public class UseList<W,T>{
  3. void f(List<T> v){}
  4. void f(List<W> v){}
  5. }

由于擦除的原因,重载方法将产生相同的类型签名。避免这种问题的方法就是换个方法名。

5. 基类劫持接口

例子:

  1. public class ComparablePet implements Comparable<ComparablePet>{
  2. public int compareTo(ComparablePet arg) {return 0;}
  3. }
  4. class Cat extends ComparablePet implements Comparable<Cat>{
  5. // Error: Comparable connot be inherited with
  6. // different arguments: <Cat> and <ComparablePet>
  7. public int compareTo(Cat arg);
  8. }

父类中我们为Comparable确定了ComparablePet参数,那么其它任何类型都不能再与ComparablePet之外的对象再比较。子类中不能对同一个接口用不同的参数实现两次。这有点类似于第四点中的重载。

但是我们可以在子类中覆写父类中的方法。

关于泛型问题就先了解这么多,有什么不对的地方还请大家指正。也欢迎小伙伴们一起交流。

Java 泛型进阶的更多相关文章

  1. 18.Java泛型

    1.为什么需要泛型 List list1=new ArrayList(Arrays.asList(new String("string"),new Integer(20))); S ...

  2. Java泛型:类型擦除

    类型擦除 代码片段一 Class c1 = new ArrayList<Integer>().getClass(); Class c2 = new ArrayList<String& ...

  3. Java基础系列二:Java泛型

    该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 一.泛型概述 1.定 ...

  4. Java 干货之深入理解Java泛型

    一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类.如果要编写可以应用多中类型的代码,这种刻板的限制对代码得束缚会就会很大. ---<Thinking in Java> ...

  5. Java——Java泛型

    该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 一.泛型概述 1.定 ...

  6. Java基础进阶

    Java基础进阶J Object类 hashcode() toString() clone() getClass() notify() wait() equals() Random类 生成 随机数 U ...

  7. Java泛型中的细节

    Java泛型中的细节 如果没有泛型 学习Java,必不可少的一个过程就是需要掌握泛型.泛型起源于JDK1.5,为什么我们要使用泛型呢?泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一 ...

  8. Java泛型的历史

    为什么Java泛型会有当前的缺陷? 之前的章节里已经说明了Java泛型擦除会导致的问题,C++和C#的泛型都是在运行时存在的,难道Java天然不支持“真正的泛型”吗? 事实上,在Java1.5在200 ...

  9. 浅析Java 泛型

    泛型是JavaSE5引入的一个新概念,但是这个概念在编程语言中却是很普遍的一个概念.下面,根据以下内容,我们总结下在Java中使用泛型. 泛型使用的意义 什么是泛型 泛型类 泛型方法 泛型接口 泛型擦 ...

随机推荐

  1. dojo实现省份地市级联报错(一)

  2. PHP学习笔记--Php预定义超全局变量$_GET

    Php中的许多预定义变量都是超全局的,无需使用global声明 超全局变量 不需要声明就可以直接使用 提供超全局变量(数组)为了让程序员更快捷的编程 $GLOBALS— 引用全局作用域中可用的全部变量 ...

  3. 图像处理------泛洪填充算法(Flood Fill Algorithm) 油漆桶功能

    泛洪填充算法(Flood Fill Algorithm) 泛洪填充算法又称洪水填充算法是在很多图形绘制软件中常用的填充算法,最熟悉不过就是 windows paint的油漆桶功能.算法的原理很简单,就 ...

  4. Unhandled event loop exception Java heap space

    1 错误描述 2 错误原因 3 解决办法

  5. 权限的分类(shiro项目中来的五)

    第一种权限:菜单栏展示还是不展示的权限(粗颗粒) 实现方法,在SYS_ROLE表中添加一个字段rights,通过 public static BigInteger sumRights(String[] ...

  6. 异常-----freemarker.template.TemplateException: The only legal comparisons are between two numbers, two strings, or two dates

    1.错误描述 六月 26, 2014 10:44:49 下午 freemarker.log.JDK14LoggerFactory$JDK14Logger error 严重: Template proc ...

  7. tomcat原理(一)server.xml中的host虚拟主机的理解

    一.Tomcat服务器端口的配置 Tomcat的所有配置都放在conf文件夹之中,里面的server.xml文件是配置的核心文件. 如果想修改Tomcat服务器的启动端口,则可以在server.xml ...

  8. 内置函数:filter函数

    功能: filter函数用于过滤序列,将满足条件的元素取出来构成新的序列. 用法: filter(function, iterable) 接受两个参数,第一个函数为过滤函数(返回True后者False ...

  9. Web 小案例 -- 网上书城(三)

    内容有点乱,有兴趣的同伙可依照后面的案例结构结合文章进行阅读 和网上购买东西一样,你可以在不登录的状态下去浏览商品,但是当你想把自己中意的东西加入购物车或是收藏起来就需要你拥有自己的账号然后登录后才可 ...

  10. Linux进行AES加密每次结果都不一致并且解密失败报错

    1. 现象 windows操作系统下进行"123456"的AES加密 encrypted message is below : QLNYZyjRnKF/zxAjzDt/lw== d ...