欢迎加入真理部。

简单的代码

我们都知道:

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

当然,不要把它带进生产环境。