泛型在java编程中有许多好处,也是我们平常经常使用的一种减少运行时错误的方式;但是在复杂的应用中,尤其是一个底层的模块中,我们不要太相信泛型给我门反回的值。

首先:

     Java中泛型是编译时检查类型, 实际在字节码的方法体中没有对泛型的描述(书上说其中通过泛型擦除),但是在类型类的声明和参数中
<https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#jls-8.1.2>
却有泛型的存在,字节码中对泛型的定义和LocalVariableTypeTable和Constant
pool这两个位置有关。如果对前面的总结有疑问还是买本《深入理解Java虚拟机》周志明著的书吧,在 6章中有解释,我这儿提炼了
一下。为什么这么设计,因为java可以通过反射获取泛型所对应的类型,字节码中类和方法的声明中保存这些数据是为了能够运行时获取泛型得类型,并不是为了让jvm做检查使用。泛型如果不了解,就会容易跳坑。

坑:

1、中间有没有用泛型的引用做桥梁,泛型将会失去效用。

2、通过运行时通过java反射来执行添加数据,依然不会被编译期发现错误,这样也会让泛型失效。

 

贴上一个LocalVariableTypeTable和Constant pool这两个位置这两个位置的代码吧
LocalVariableTypeTable: Start Length Slot Name Signature 0 6 0 this
Lflycat/AppTest$Car<TT;>; 实施上Signature是引用了常亮里面的内容的,原数据不是直接存储的。 0 6 1 param TT;
Signature: #25 // (TT;)V
先来验证 确实没有存在字节码中,我是通过javap -verbose class命令确认的
package flycat; import org.junit.Test; import java.lang.reflect.Field; import
java.lang.reflect.ParameterizedType; import java.util.ArrayList; import
java.util.List; /** * Unit test for simple App. */ public class AppTest { /** *
Rigorous Test :-) */ @Test public void test() { List<Integer> ls = new
ArrayList<>(); ls.add(4); } }
 javap -verbose AppTest.class
public void test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2,
locals=2, args_size=1 0: new #25 // class java/util/ArrayList 创建对象 3: dup 4:
invokespecial #26 // Method java/util/ArrayList."<init>":()V 初始化对象 7: astore_1
8: aload_1 9: iconst_4 10: invokestatic #27 // Method
java/lang/Integer.valueOf:(I)Ljava/lang/Integer;将4这个整型对象Integer执行static方法 13:
invokeinterface #28, 2 // InterfaceMethod
java/util/List.add:(Ljava/lang/Object;)Z 执行List的add方法 18: pop 19: return
LineNumberTable: line 81: 0 line 82: 8 line 83: 19 LocalVariableTable: Start
Length Slot Name Signature 0 20 0 this Lflycat/AppTest; 8 12 1 ls
Ljava/util/List; LocalVariableTypeTable: Start Length Slot Name Signature 8 12
1 ls Ljava/util/List<Ljava/lang/Integer;>; RuntimeVisibleAnnotations: 0: #72()
上面部分代码看出,其中没有看到泛型的影子,List<Integer> 和List是一样的。

下面是中间有没有用泛型的引用做桥梁的代码:
public void test() { List<String> strings = new ArrayList<String>();
strings.add("sdfsd"); List objects = strings; List<Integer> integers =
(List<Integer>) objects; System.out.println(integers); }
运行结果是

[sdfsd]

这儿明显是错误的,代码中泛型声明是整数类型,但是最后是字符串类型。

下面代码是一个通过运行时添加数据:
public void test() { List<Integer> strings = new ArrayList<Integer>();
strings.add(12); Class clazz = null; try { clazz =
Class.forName("java.util.ArrayList"); Method addMethod = clazz.getMethod("add",
Object.class); //必须时Object,因为E泛型在java.util.ArrayList上面的为Object
addMethod.invoke(strings, "asdfasd"); System.out.println(strings); } catch
(Exception e) { e.printStackTrace(); } }
运行结果是

[12, asdfasd]

这儿明显是错误的,代码中泛型声明是整数类型,但是最后是字符串类型。

 

总结:

使用泛型造成错误一般都是书写不规范造成的,而且都是在发生“掩盖”的过程。

比如上上面的System.out.println(strings)这个代码
隐含着把所有数据转换成String类型,原本的Integer类型转换成String去了,String也会转换成String不会发生错误,造成这种现象。

 

怎么避免:

1、强制规定所有人统一的泛型类型,写在接口文档里面,这样编译会报错。

2、如果代码乱,可以牺牲性能,在关键位置对数据的真实类型进行判断。(比如在对象转json字符串或者json字符串转java对象的时候)。


ps:想要达到运行时添加数据造成泛型约束失效,只有通过反射获取Method对象执行。为什么不能运行时通过用户输入数据添加数据对象让泛型失效,因为用户输入数据都是String类型,底层时char的,所有得到的int或者其它的时通过类型转换得到的,其中没有使用到泛型,这儿新手容易混淆。