Java泛型进阶
来源:互联网 发布:淘宝晒图返现违规吗 编辑:程序博客网 时间:2024/06/07 16:22
原文链接
1.在泛型代码内部,无法获得任何有关泛型参数类型的信息。
“在泛型代码内部,无法获得任何有关泛型参数类型的信息”,这并不是说使用泛型代码时给定的类型参数会消失(要不然泛型就没有存在的意义了,外部将T定为比如String类型后,之后的T就被定为String类型了),而是说我们只知道这是个泛型类型,可以代表多种类型,即我们不能通过T访问与某种特定类有关的方法,属性等(当然,有泛型边界的是个例外)。
2.关于擦除:擦除:List<T>被擦除为List,T被擦除为Object
//这个例子表明编译过程中并没有根据参数生成新的类型public class Main2 { public static void main(String[] args) { Class c1 = new ArrayList<Integer>().getClass(); Class c2 = new ArrayList<String>().getClass(); System.out.print(c1 == c2); }}/* outputtrue*/
在 List<String>
中添加 Integer
将不会通过编译,但是List<Sring>
与List<Integer>
在运行时的确是同一种类型。
//例子, 这个例子表明类的参数类型跟传进去的类型没有关系,泛型参数只是`占位符`public class Table {}public class Room {}public class House<Q> {}public class Particle<POSITION, MOMENTUM> {}public class Main { public static void main(String[] args) { List<Table> tableList = new ArrayList<Table>(); Map<Room, Table> maps = new HashMap<Room, Table>(); House<Room> house = new House<Room>(); Particle<Long, Double> particle = new Particle<Long, Double>(); System.out.println(Arrays.toString(tableList.getClass().getTypeParameters())); System.out.println(Arrays.toString(maps.getClass().getTypeParameters())); System.out.println(Arrays.toString(house.getClass().getTypeParameters())); System.out.println(Arrays.toString(particle.getClass().getTypeParameters())); }}/** output[E][K, V][Q][POSITION, MOMENTUM]*/
我们在运行期试图获取一个已经声明的类的类型参数,发现这些参数依旧是‘形参’,并没有随声明改变。也就是说在运行期,我们是拿不到已经声明的类型的任何信息。(只是被擦除,让你编写代码时不知道泛型T是什么,但编写完了后使用这个泛型类或泛型方法时一旦将T具体化了后,就能知道了)
只要涉及到List<T>,不管是编写泛型类,还是使用泛型类都会被擦除,所以看下面的图:List<Shape>被擦除为List,尽管知道这是Shape类型,但是就能用s.draw(不能知道详细类型信息)(加个泛型边界可以解决这个错误,因为加了后就是被擦除为泛型边界而不是Object)
编译器会虽然在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。
List<String> stringList=new ArrayList<String>();//可以通过编译stringList.add("wakaka");//编译不通过//stringList.add(new Integer(0));//List.javapublic interface List<E> extends Collection<E> {//...boolean add(E e);//...}
List
的参数类型是E
,add
方法的参数类型也是E
,他们在类的内部是一致的,所以添加Integer
类型的对象到stringList
违反了内部类型一致,不能通过编译。
重用 extends
关键字。通过它能给与参数类型添加一个边界。
泛型参数将会被擦除到它的第一个边界(边界可以有多个)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object
。下面的例子中,可以把泛型参数T当作HasF类型来使用。
// HasF.javapublic interface HasF { void f();}//Manipulator.javapublic class Manipulator<T extends HasF> { T obj; public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; }}
extend
关键字后后面的类型信息决定了泛型参数能保留的信息。补充:Java擦除的基本原理:
刚看到这里可能有些困惑,一个泛型类型没有保留具体声明的类型的信息,那它是怎么工作的呢?在把《Java编程思想》书中这里的边界与上文的边界区分开来之后,终于想通了。Java的泛型类的确只有一份字节码,但是在使用泛型类的时候编译器做了特殊的处理。
这里根据作者的思路,自己动手写了两个类SimpleHolder
和GenericHolder
,然后编译拿到两个类的字节码,直接贴在这里:
// SimpleHolder.javapublic class SimpleHolder { private Object obj; public Object getObj() { return obj; } public void setObj(Object obj) { this.obj = obj; } public static void main(String[] args) { SimpleHolder holder = new SimpleHolder(); holder.setObj("Item"); String s = (String) holder.getObj(); }}// SimpleHolder.classpublic class SimpleHolder { public SimpleHolder(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public java.lang.Object getObj(); Code: 0: aload_0 1: getfield #2 // Field obj:Ljava/lang/Object; 4: areturn public void setObj(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field obj:Ljava/lang/Object; 5: return public static void main(java.lang.String[]); Code: 0: new #3 // class SimpleHolder 3: dup 4: invokespecial #4 // Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #5 // String Item 11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V 14: aload_1 15: invokevirtual #7 // Method getObj:()Ljava/lang/Object; 18: checkcast #8 // class java/lang/String 21: astore_2 22: return }
//GenericHolder.javapublic class GenericHolder<T> { T obj; public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } public static void main(String[] args) { GenericHolder<String> holder = new GenericHolder<>(); holder.setObj("Item"); String s = holder.getObj(); }}//GenericHolder.classpublic class GenericHolder<T> { T obj; public GenericHolder(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public T getObj(); Code: 0: aload_0 1: getfield #2 // Field obj:Ljava/lang/Object; 4: areturn public void setObj(T); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field obj:Ljava/lang/Object; 5: return public static void main(java.lang.String[]); Code: 0: new #3 // class GenericHolder 3: dup 4: invokespecial #4 // Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #5 // String Item 11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V 14: aload_1 15: invokevirtual #7 // Method getObj:()Ljava/lang/Object; 18: checkcast #8 // class java/lang/String 21: astore_2 22: return }
经过一番比较之后,发现两分源码虽然不同,但是对应的字节码逻辑部分确是完全相同的。
在编译过程中,类型变量的信息是能拿到的。所以,set
方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get
方法,由于擦除机制,运行时的实际引用类型为Object
类型。为了‘还原’返回结果的类型,编译器在get
之后添加了类型转换。所以,在GenericHolder.class
文件main
方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。
所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束
3.擦除的缺陷泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof
和new
。因为在运行时,所有参数的类型信息都丢失了。(例如下面的代码中,编译不通过是因为都规定了在编写泛型代码时不让你知道T的类型信息,你还想用instanceof T去判断,所以编译会报错)
public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { //编译不通过 if (arg instanceof T) { } //编译不通过 T var = new T(); //编译不通过 T[] array = new T[SIZE]; //编译不通过 T[] array = (T) new Object[SIZE]; }}
4.擦除的补偿
①类型判断问题:
class Building {}class House extends Building {}public class ClassTypeCapture<T> { Class<T> kind; public ClassTypeCapture(Class<T> kind) { this.kind = kind; } public boolean f(Object arg) { return kind.isInstance(arg); } public static void main(String[] args) { ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class); System.out.println(ctt1.f(new Building())); System.out.println(ctt1.f(new House())); ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class); System.out.println(ctt2.f(new Building())); System.out.print(ctt2.f(new House())); }}//output//true//true//false//true
泛型参数的类型无法用instanceof
关键字来做判断。所以我们使用类类型来构造一个类型判断器,判断一个实例是否为特定的类型。
②创建类型实例:
Erased.java
中不能new T()
的原因有两个,一是因为擦除,不能确定类型;而是无法确定T
是否包含无参构造函数。
为了避免这两个问题,我们使用显式的工厂模式:
interface IFactory<T> { T create();}class Foo2<T> { private T x; public <F extends IFactory<T>> Foo2(F factory) { x = factory.create(); }}class IntegerFactory implements IFactory<Integer> { @Override public Integer create() { return new Integer(0); }}class Widget { public static class Factory implements IFactory<Widget> { @Override public Widget create() { return new Widget(); } }}public class FactoryConstraint { public static void main(String[] args) { new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); }}通过特定的工厂类实现特定的类型能够解决实例化类型参数的需求。
③创建泛型数组:
一般不建议创建泛型数组。尽量使用ArrayList
来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。
public class GenericArrayWithTypeToken<T> { private T[] array; @SuppressWarnings("unchecked") public GenericArrayWithTypeToken(Class<T> type, int sz) { array = (T[]) Array.newInstance(type, sz); } public void put(int index, T item) { array[index] = item; } public T[] rep() { return array; } public static void main(String[] args) { GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10); Integer[] ia = gai.rep(); }}
这里我们使用的还是传参数类型,利用类型的
newInstance
方法创建实例的方式。- Java泛型进阶
- 【java进阶】初探泛型
- Java进阶--Java泛型总结
- Java之泛型进阶——泛型通配符
- 【Java学习】泛型接口与Generator进阶
- Java中的泛型详解(2):高级进阶
- java 进阶
- JAVA进阶
- java进阶
- java进阶
- java进阶
- java进阶
- java进阶
- java进阶
- Java进阶
- Java 进阶
- java进阶
- java进阶
- 关于Http请求后返回json乱码的问题
- Learning Notes
- loadrunner agent process进程
- 初入JDBC
- HBase框架学习之路
- Java泛型进阶
- 移植DM9000C驱动程序之测试及内存控制器简介
- 关于浏览器缓存-javascript清除浏览器缓存的方法
- jQuery 获取文件后缀的方法
- HBase 数据模型(Data Model)
- Java多线程1:进程与线程概述
- MySQL笔记(二)
- 执行顺序
- MFC中遍历TreeControl的节点或者查找某个节点