1. 泛型与类型擦除
- 泛型是 JDK 1.5 的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、 接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
- 泛型技术在 C# 和 Java 之中的使用方式看似相同,但实现上却有着根本性的分歧
- C# 里面泛型无论在程序源码中、编译后的 IL 中(Intermediate Language,中间语言,这时候泛型是一个占位符)还是运行期的 CLR 中都是切实存在的
- List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。
- Java 语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中, 就已经被替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码
- 对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一个类,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。
1.1 类型擦除
- 是一段简单的Java泛型例子,我们可以看一下它编译后的结果是怎样的?
Map<String, String>map=new HashMap<String, String>();
map.put("hello","你好");
System.out.println(map.get("hello"));
把这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译:
Map map=new HashMap();
map.put("hello","你好");
System.out.println((String)map.get("hello"));
类型擦除!!!
1.2 当泛型遇见重载
public class GenericTypes{
public static void method(List<String>list){
System.out.println("invoke method(List<String>list)");
}
public static void method(List<Integer>list){
System.out.println("invoke method(List<Integer>list)");
}
}
- 这段代码是不能被编译的,是因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两个方法的特征签名变得一模一样
- 无法重载的原因已经找到了,但是真的就是如此吗?事实上,只能说,泛型擦除成相同的原生类型只是无法重载的一部分原因,请再接着看一看代码
- 补充:Java 语法层面和 JVM 层面上方法特征签名
- Java 语法层面的方法特征签名:特征签名 = 方法名 + 参数类型 + 参数顺序;
- JVM 层面的方法特征签名:特征签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型;
public class GenericTypes{
public static String method(List<String>list){
System.out.println("invoke method(List<String>list)");
return"";
}
public static int method(List<Integer>list){
System.out.println("invoke method(List<Integer>list)");
return 1;
}
public static void main(String[]args){
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}
执行结果:
invoke method(List<String>list)
invoke method(List<Integer>list)
- 两段代码的差别是两个 method 方法添加了不同的返回值,由于这两个返回值的加入,方法重载居然成功了,即这段代码可以被编译和执行了。 这是我们对 Java 语言中返回值不参与重载选择的基本认知的挑战吗?
- 方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择
- 但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class文件中的
2. 自动装箱、拆箱
List<Integer>list=Arrays.asList(1,2,3,4);
//如果在JDK 1.7中,还有另外一颗语法糖,
//能让上面这句代码进一步简写成List<Integer>list=[1,2,3,4];
int sum=0;
for(int i:list){
sum+=i;
}
System.out.println(sum);
编译之后
List list=Arrays.asList(new Integer[]{
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4)});
int sum=0;
for(Iterator localIterator=list.iterator(); localIterator.hasNext();){
int i=((Integer)localIterator.next()).intValue();
sum+=i;
}
System.out.println(sum);
自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,对应 Integer.valueOf()与Integer.intValue()方法,很简单
- 一些注意的地方:
public static void main(String[]args){
Integer a=1;
Integer b=2;
Integer c=3;
Integer d=3;
Integer e=321;
Integer f=321;
Long g=3L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
}
输出为:
true
false
true
true
true
false
true
简单解释下:
- java中会有一个 Integer 缓存池,缓存的大小是:-128~127
- 使用
==
的情况:- 如果比较的是 Integer变量,默认比较的是地址值
- Java 的 Integer 维护了从 -128~127 的缓存池
- 如果比较的某一边有操作表达式(如 a+b),则比较的是具体数值
- 使用
equals()
的情况:- 无论是 Integer 还是 Long 的
equals()
默认比较的是数值 - Long 的
equals()
方法,会先判断是否是 Long 类型(因此上面g==(a+b)
返回的是 false)
- 无论是 Integer 还是 Long 的