所谓自由就是可以说二加二等于四的自由。
简单的代码
我们都知道:
2 + 2 == 5
有力的佐证如下:
public class Main {
public static void main(String[] args) throws Exception {
Class<?> cache = Integer.class.getDeclaredClasses()[0];
Field c = cache.getDeclaredField("cache");
c.setAccessible(true);
Integer[] array = (Integer[]) c.get(cache);
array[132] = array[133];
System.out.printf("%d", 2 + 2);
}
}
输出为 5
,结果与事实相符,2 + 2 可以等于 5,也可以根据需要等于任何数字。
但是这是为什么呢?
Java 的自动装箱
我们都知道,Java 中数据分为原生类型(Primitive Types)和引用类型,比如 int
就是原生类型,Integer
就是引用类型。
原生类型并不能作为泛型参数,因此以下的代码是不可以通过编译的:
List<int> list = new ArrayList<>();
这是因为,泛型在运行时会 擦除
,Java 的泛型是假的泛型,不能在运行时获得泛型的类型,除非保存 TypeToken
之类的东西。
我们如果想往 List
里放入 int
,则可以使用其引用类型:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
int i = list.get(0);
但是这样就有了一个问题,就是在 list.add(1)
中,根据泛型参数我们的 add
方法接受的是 Integer
类型,而传入的 1
实际上是 int
类型,这样类型不就冲突了吗?
因此,Java 在 1.5 的版本与泛型同时引入了自动装箱拆箱(Auto boxing/unboxing)机制,自动完成原生类型与引用类型之间的互相转换。
当需要一个引用类型而实际传入了原生类型,Java 就会为我们自动装箱,把原生类型变成引用类型,如上文的 list.add(1)
,编译完成后实际上是:
list.add(Integer.valueOf(1));
而需要原生类型而实际为引用类型时,Java 就会尝试自动拆箱,比如 int i = list.get(0)
,最后会变成:
int i = list.get(0).intValue();
而观察 Integer 类的源码,可以看出来大概长这样:
public class Integer extends Number implements Comparable<Integer> {
private final int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return value;
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
}
然后你注意到了什么。
Java 的 Integer Cache
你可能会想(不会想也得必须给我想):
如果许多数字都装箱,那不会白白消耗许多内存吗?
因此,Java 为 Integer 类加入了缓存,对于小整数会直接引用提前创建好的实例作为 valueOf
的返回值。而提前创建好的这些实例,被放在 Integer 类里的 IntegerCache 类中。
Java 默认的缓存范围是 -128 到 127,因此 4 就在 132 的位置。
如此一来,把这里本应表示 4
的数字换成 5
的实例,就可以证明 2 + 2
实际上等于 5
了,并且这个数字可以根据需要任意改变。
int i = 2 + 2; // 4
Integer boxed = Integer.valueOf(i); // 返回 IntegerCache[132],被修改为了 5
System.out.printf("%d", boxed); // 5
顺便,缓存的范围可以自行增加,通过一个 -XX:AutoBoxCacheMax
参数指定上限,不过必须大于 127
。
当然,不要把它带进生产环境。