解读java 泛型机制
一. 思考
1.什么是java 泛型?
2.泛型的好处?
在了解泛型之前,我们尝试着先来回答上面的几个问题
1.Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法
2.泛型的好处
- 第一是泛化 可以用T代表任意类型Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。
- 第二是类型安全泛型的一个主要目标就是提高ava程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。
- 第三是消除强制类型转换 泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
- 第四是向后兼容 支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译。
二. 不使用泛型
1. 早期缺点 :类型缺失
java 一直是承诺早期的代码可以在后续的高版本的JVM上面运行,所以所尽管现在的JDK 已经到11 了,但是还是可以运行JDK1.0 时期的代码 下面就写一点关于没有泛型时期的java 代码
public class Test1 {
public static void main(String[] args) {
fun1();
}
private static void fun1() {
List list = new ArrayList();
list.add(11);
list.add("sff");
list.add(new Animal());
list.add(new Animal());
//Object o 来接收对象
Animal o = (Animal)list.get(2);
//必须强转
o.eat();
Object o2 = list.get(3);
//o2 只能使用Object 内部的方法
}
}
class Animal{
public void eat(){
System.out.println("eating");
}
}
就是什么类型都可以添加 但是在使用的时候必须的要强转 而且强转是你知道这个list 里面存放的是什么样子的类型不然还需要添加一个instanceof 的判断.否者还会报出ClassCastException 的异常;
2.早期缺点 : 代码冗余
比如说我需要构建一个只可以存放String 类型的List 我需要写以下的代码
public class LossDeneric {
public static void main(String[] args) {
fun1();
}
private static void fun1() {
StringList list = new StringList();
list.add("sff");
System.out.println(list.get(0).equals("sff"));
}
}
class StringList {
List list = new ArrayList();
//限制传入的参数
public void add(String string){
list.add(string);
}
//强制转换返回的参数
public String get(Integer index){
return (String)list.get(index);
}
//....OTHER API
}
//代码冗余
class LongList {
List list = new ArrayList();
//限制传入的参数
public void add(Long long1){
list.add(long1);
}
//强制转换返回的参数
public Long get(Integer index){
return (Long)list.get(index);
}
//....OTHER API
}
//==每次对应一个数据类型 就要编写一个这样的list
三.使用泛型
下面这个例子是使用泛型
public class UserGeneric {
public static void main(String[] args) {
fun1();
}
public static void fun1(){
List<String> strList = new ArrayList<>();
strList.add("aaa");
String s = strList.get(0);
//complier error
//strList.add(1111);
}
}
可以看到的是在List 里面添加元素的时候,他是限定了元素添加的类型必须是和T 类型是一致的,而获取的数据类型也是直接返回的是和约束的泛型是一致的.
用法
使用super 和extends 来约束类型的下界 和上界 具体看的是编程思想第15 章节
四.问题
这个章节也是我写这一篇文章的原因,也是工作里面遇到的一个问题,困扰了我很久,话不多说,测试代码先贴上;
public class Test1 {
public static Map<String, String> testMap = new HashMap<>();
public static void main(String[] args) throws Exception {
Method put = testMap.getClass().getDeclaredMethod("put", Object.class, Object.class);
put.invoke(testMap, "sff", 111);
testMap.get("sff"); //对应L23
testMap.get("sff").toString();//error 发生 L24
Assert.isNull(null);
}
}
解读就是先使用反射获取HashMap 的put() 方法 ,然后就是put 一个不符合它得泛型约束的值,根据运行结果可以看到是可以put 数据的 .但是代码在 testMap.get(“sff”).toString() 发生了错误
Connected to the target VM, address: '127.0.0.1:62969', transport: 'socket'
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.goodgoodstudy.reflect.Test1.main(Test1.java:26)
Disconnected from the target VM, address: '127.0.0.1:62969', transport: 'socket'
Process finished with exit code 1
思考?为什么会发生这个问题,这个问题我也是纠结了很久; 下面是我的的思考步骤;
- 我先是开启了IDEA的debug 模式 L7 ,是OK 的,到L8 的时候我先调试 testMap.get(“sff”) 时获取的对象是Integer类型的数据.问题就在这里.toString()调用的是String 方法的toString() 而不是Integer的toString();这个也是我最郁闷的点,我们知道在java 运行的时候 有一个名称叫做RTTI runtime type Identification 简单的可以理解为就是运行的时候执行它的真正类型的方法(就是多态),当我们通过某个引用调用方法时,Java总能找到正确的Class类中所定义的方法
- 思考了很久,那不如看看这个类的反编译字节码文件,唉 还真看出了一点东西
L2
LINENUMBER 23 L2
GETSTATIC com/goodgoodstudy/reflect/Test1.testMap : Ljava/util/Map;
LDC "sff"
INVOKEINTERFACE java/util/Map.get (Ljava/lang/Object;)Ljava/lang/Object; (itf)
POP
L3
LINENUMBER 24 L3
GETSTATIC com/goodgoodstudy/reflect/Test1.testMap : Ljava/util/Map;
LDC "sff"
INVOKEINTERFACE java/util/Map.get (Ljava/lang/Object;)Ljava/lang/Object; (itf)
CHECKCAST java/lang/String //就是他发生的问题!!!!
INVOKEVIRTUAL java/lang/String.toString ()Ljava/lang/String;
POP
- 结合这个问题 在java 编程思想的15.7.4 边界处问题 这一章节 找到问题的根源
就是当我们对使用泛型 插入的边界:就拿List 举例来说 add()**(一般就是类似于set put 等)**的泛型方法,编译器会对输入的参数校验,对不符合的参数编译器就报错. 取出的边界方法: 一般是get() 方法 编译器会自动添加CHECKCAST指令来确保获取的参数类型和泛型一致; - 似乎问题得到了解释,但是又想为什么testMap.get(“sff”) 没有异常呢.这个我查阅资料没有查到什么有用的信息,但是我用代码测试了几下,应该是找到了规律 一下是我的个人想法:在get() 的时候后面,没有对取出的值做进一步的操作,也就是调用方法 我有做了下面测试
String.valueOf(testMap.get("sff"));
valueOf() 方法接收一个Object 对象;
在结合testMap.get(“sff”).toString() 方法 这个是获取到值之后再去调用方法, 编译器智能的理解为肯定是调用泛型类里面的泛型方法,所以把获取到的值做一个类型转换,结果就是在类型转换的时候发生了异常!!
小节总结: java 的泛型机制在运行时时擦出它的泛型信息的,但是取而代之的擦除的补偿机制就是在编译时对泛型的插入(一般就是泛型作为参数的方法)的参数做一个编译期校验; 在获取(方法的返回值是泛型)对象的方法,会在使用该对象的时候会做一次类型校验,从而确保使用泛型的方法的正确返回
问题:java 为什么要做泛型擦除呢??
五.泛型擦除
看看上面的那个例子的map 对应得字节码
public static Map<String, String> testMap = new HashMap<>();
字节码
public static Ljava/util/Map; testMap
我们知道,在JVM 运行的时候,是通过加载和运行我们的class文件也就是我们的字节码文件,我们看看这个字节码.最后在编译的时候没有泛型添加到字节码里面去,.也就是说在运行的时候是不知道或者说在运行的时候我是可以在map 里面随意添加不符合代码泛型约束的类型的数值.
这里就引出了上面的问题 java 为什么要做泛型擦除呢??
我查阅了一些资料和博客大概得出一个结论,可以再结合我们第二节的内容:java 是为了兼容早期版本的代码,应为早期JDK1.5以前的代码是没有泛型的概念的,而如果在后面有泛型的时候我们把泛型这个添加到编译的字节码里面去的话,高版本的虚拟机可能就是不支持之前低版本的代码了,这与之前java 官方的宣传不符合(基于低版本的java代码永远可以运行在高版本的JVM 上的)进而使用这个编译期检查的机制来兼容以前的老的代码.
OK 写完了,感觉自己还有好多关于泛型的内容没有写进去,后面知道的跟多的知识在更新吧!!!