java基础加强--泛型(Generic)的应用

    技术2026-08-30  3

    泛型是JAVA 1.5的新特性,引入泛型主要是为了解决数据的类型安全问题。在类的声明时通过使用一个标识符号(如<T>、<K,V>、<?>等)添加在类名,属性,方法(方法名,参数以及返回值)上。这样用户在声明或实例化时只要指定好需要的具体类型即可。1.5后集合类和反射都增加了泛型的支持,先从集合类的一个例子来直观了解泛型的作用和使用方法。例:

    package cn.hxq.generic; import java.lang.reflect.*; import java.util.ArrayList; public class GenericTest { @SuppressWarnings("unchecked") public static void main(String[] args) throws Exception{ //1.5以前的集合使用:集合中可以加入多种类型,导致使用时类型出错 ArrayList collection1 = new ArrayList(); collection1.add(1); collection1.add(1L); collection1.add("abc"); //如果这样拿出来用就出错了: //int i = (Integer)collection1.get(1); //使用泛型改写上面的集合 ArrayList<String> collection2 = new ArrayList<String>(); //此时下面两个内容将不能加入到集合中,保证了集合只能存储String类型的对象 //collection2.add(1); //collection1.add(1L); collection2.add("abc"); String element = collection2.get(0); //反射中也可以使用泛型: Constructor<StringBuffer> construtor = StringBuffer.class.getConstructor(String.class); StringBuffer sbr = construtor.newInstance(element); Method method = sbr.getClass().getMethod("append", String.class); System.out.println(method.invoke(sbr, element)); } }

     

    通过上面的例子可以看出,使用泛型后,编译器会通过声明的泛型来限定集合中的元素类型,用户如果加入错误的类型,就会报错。通过这样的方式就可以保证集合中的类型一致。当用户在集合中取出一个元素时,也不需要进行强制类型转换了,因为编译器已经知道了集合中的元素只能是某个用户指定的类型。

    泛型的内部作用:泛型其实是提供给JAVAC编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带类型说明的集合时会去掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getclass()方法的返回值类型和原始类型一样。由于编译器编译器产生的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据。例如,用反射得到集合,再调用其ADD方法。

    例2:

    import java.util.*; import java.lang.reflect.*; public class ReflyectGeneric { public static void main(String[] args) throws Exception { List<String> list1 = new ArrayList<String>(); List<Integer> list2 = new ArrayList<Integer>(); //使用反射得到ArrayList集合的字节码,看是否是同一份 System.out.println(list1.getClass() == list2.getClass()); //通过反射调用集合的add方法来往集合中加入不同类型的元素 Method list1Method = list1.getClass().getMethod("add",Object.class); list1Method.invoke(list2, "abc"); System.out.println(list2.get(0)); } }

     

    输出结果为abc

     

     

    但是写成这样就报错了!例3:

    package cn.hxq.generic; import java.util.*; import java.lang.reflect.*; public class ReflyectGeneric { public static void main(String[] args) throws Exception { List<String> list1 = new ArrayList<String>(); List<Integer> list2 = new ArrayList<Integer>(); //使用反射得到ArrayList集合的字节码,看是否是同一份 System.out.println(list1.getClass() == list2.getClass()); //通过反射调用集合的add方法来往集合中加入不同类型的元素 Method list1Method = list1.getClass().getMethod("add",Object.class); list1Method.invoke(list1, 1); System.out.println(list1.get(0)); } }

     

    将会报出:Exception in thread "main" java.lang.ClassCastException:  java.lang.Integer cannot be cast to java.lang.String at cn.hxq.generic.ReflyectGeneric.main(ReflyectGeneric.java:16)

     

     

    我的猜想:System.out.println(list1.get(0));在list1.get(0)这里,由于编译器认为返回的对象类型应该为String,但我因为使用了反射在集合中添加了Integer类型的对象,list1.get(0)给出的其实是Integer。于是我再想:System.out.println不是会自动调用toString()方法吗?后来发现,我的声明是List<String> list1里面装的肯定是String,get()方法返回的肯定是String,那编译器肯定不能对一个String类型的对象去调用toString()方法。于是编译器就相当于去做了这么一件事: Integer i = 1; String str = i;那样就肯定报错了,改成以下两句就可以了。Object obj = list1.get(0);System.out.println(obj);我想原来错误就是由于System.out.println语句没法调用toString方法的缘故。查阅JDK文档我却赫然发现:String类竟然也有toString()方法!那么前面的推断是错的吗?(String 的toString()方法的解释很搞笑!)

     

    我于是再试了一下这样的方式:

    package cn.hxq.generic; import java.util.*; import java.lang.reflect.*; public class ReflyectGeneric { public static void main(String[] args) throws Exception { List<String> list1 = new ArrayList<String>(); List<Integer> list2 = new ArrayList<Integer>(); //使用反射得到ArrayList集合的字节码,看是否是同一份 System.out.println(list1.getClass() == list2.getClass()); //通过反射调用集合的add方法来往集合中加入不同类型的元素 Method list1Method = list1.getClass().getMethod("add",Object.class); list1Method.invoke(list2, "abc"); System.out.println(list2.get(0).toString()); } }

     

    这时我使用了list2,我指定的集合类型是ArrayList<Integer>,通过反射加入了"abc"这个String对象,从例2来看,直接System.out.println(list2.get(0));这样可以很好的打印出abc。但这次我显示地加入了toString()方法。让人吃惊的是,报错了:Exception in thread "main" java.lang.ClassCastException:  java.lang.Integer cannot be cast to java.lang.String at cn.hxq.generic.ReflyectGeneric.main(ReflyectGeneric.java:16)。

    还是和以前一样的异常类型。这样可以证明我前面的推论其实是错的。想了好久也没想出来,后来请教了群里的同学,同学认为:问题不是出在toString()是否被调用的前提下,而是在get()这个方法中。

    同学的观点:

    看看java源代码关于get()方法的定义:public E get(int index) { RangeCheck(index);

     return (E) elementData[index];    }这是一个返回值是泛型的方法,编译器在编译的list1.get(0)的时候,因为前面我声明了List<String> list1,由于去泛型化的原因那么通过list1调用的get()方法自然就被编译成:public String get (int index){ return (String) elmentData[index];}在运行时,就需要返回一个String类型的值.而且在return 的时候,会将我加入的Integer强制转换成String类型,于是就报了java.lang.ClassCastException的错。其实就是类型转换的错误。

    新衍生的问题:那为什么System.out.println(list2.get(0))是正确的,System.out.println(list2.get(0).toString())却又报错了呢?如果按照上面的推论:我声明了List<String> list2,我使用list2调用的get()方法应该会成为这样的形式:public Integer (int index){ return (Integer) elmentData[index];}那么此时将String类型的元素强制转换成Integer的时候却又不报错了呢?再者,为什么一旦显式调用了toString(),却马上报错了?这些问题实在思考不出来,先丢在一边··。

    昨晚没有思考出来,今天早上突然有了灵感。其实问题是出在get方法的return之后,get()方法本身只是通过return将集合中的引用传递出来,println()方法接到引用之后,会对接到的引用根据类型去调用编译器在class文件中说明的toString()方法,因为这个方法是被不同的子类覆写过的。

    问题1的解答:list1.get(0)编译器编译的时候认为它返回的是String值,于是在Class文件中标明了一旦需要调用toString(),则会使用String类的toString()(实际上如果真是String类,JVM将不会调用)运行时传递来了一个Integer类的对象,那么就需要调用toString()方法了。此时JVM就去用一个Integer的对象调用了String类的toString()方法,因此报出类型转换的错误。Object obj = list.get(0);这一句很好地说明了get()方法本身内部在进行引用传递时并没有出错。错在于接受引用的方法或者引用变量。System.out.println(obj);此时会调用obj的toString方法肯定是没错的,于是正确打印了。

    问题2的解答:System.out.println(list2.get(0));这样为什么正确呢?因为println()实际上接到的引用是String类的,JVM没必要再调用编译器在class文件中声明的Integer类的toString()方法,所以直接就能打印了。System.out.println(list2.get(0).toString());这样的显式调用为什么错误?这也是编译器在编译成class文件时造成的,因为我显式调用了toString(),因为编译器认为我传递出来的是个Integer,所以在class文件中声明的是Integer类的toString()方法。所以我显示调用时,实际上是用String()类的对象去调用了Integer类的toString()方法。这样就出错了。

    总结如下:一、list1Method.invoke(list1, 1);System.out.println(list1.get(0));

    错误:1)、get()方法运行时返回了一个Integer对象,println()方法无法打印。   2)、JVM对Integer对象的处理:去调用toString()方法。   3)、class文件中的信息让它去调用了一个String类的toString()方法。   4)、使用Integer对象调用了String类的toString()方法,出错了。

    二、list1Method.invoke(list2, "abc");System.out.println(list2.get(0));

    正确:1)、get()方法运行时返回了一个String对象,println()方法          接到String类对象。   2)、JVM对String对象的处理:不调用toString()方法。正确了。 三、list1Method.invoke(list2, "abc");System.out.println(list2.get(0).toString());

    错误:1)、get()方法运行时返回了一个String对象,println()方法          接到String类对象。   2)、此时toString()方法被显式调用,由于class文件得到的类型信息,调用    的是Integer类的toString()方法。   3)、使用String对象去调用Integer类的toString()方法,出错了。

    结论是:  使用一个类的对象 去调用了 另一个非父类的toString()方法,导致出错。  如果使用String类,默认是不会调用toString()方法,如果显式调用,会重复上面的  错误。  泛型的应用很广泛,应该牢牢掌握,但如果要深入了解泛型的实现,我想可不是一朝一夕的事··。

     

    最新回复(0)