偶然机会看到一道面试题,要求交换并打印两个数,看上去挺简单的一个问题,实际上考量了很深的Java基础,特别记录下。
初始方法体是这个样子的:

Integer a=1;
Integer b=2;
System.out.println(a+" "+b);
swap(a,b);
System.out.println(a+" "+b);

要求这段代码运行之后,输出为以下结果:
1 2
2 1
请实现swap方法。

常规实现方法

    public static void swap(Integer a, Integer b) {
        Integer temp = a;
        a = b;
        b = temp;
    }

这可能是我们最先想到的方法,但是绝大多数人在写完之后可能就会怀疑到底行不行,没错,不行,输出结果仍然是
1 2
1 2
那么问题来了,为什么呢?实际上Java中方法的参数传递分为两种,一种是值传递,一种是引用传递,这里传递进来的参数是Integer类型,我们都知道它是基本数据类型包装类,传递进来的a和b自然是对象,那为啥改变不了呢?看下Integer类的声明就明白了:

public final class Integer extends Number implements Comparable<Integer> {
}

没错,这里类被声明为final类型,在方法引用传递中,它的引用值是修改不了的,那么就没有办法修改了吗?

使用反射修改值

Integer类中声明了一个成员变量value,它的声明如下:

    /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;

它维护着当前对象的值。只要我们利用反射修改改值,是不是就可以达到修改目的了呢
改版之后的代码如下:

    public static void swap(Integer a, Integer b) throws Exception {
        Field value = Integer.class.getDeclaredField("value");
        value.setAccessible(true);
        int temp= a;
        value.set(a,b);
        value.set(b,temp);
    }

这段代码运行结果会是怎样的呢,输出结果如下:
1 2
2 2
这里可能很多人已经傻眼了,为啥会是这个结果呢?只修改成功了第一个数,第二个数没有修改成功?
让我们来分下下原因,如果想知道JVM到底干了什么事情,只能去反汇编字节码文件了,目前为止,我的完整测试代码如下:

import java.lang.reflect.Field;

/**
 * Created by zhengrj on 2019/7/8.
 */
public class Main {

    public static void main(String[] args) throws Exception {
        Integer a = 1;
        Integer b = 2;
        System.out.println(a + " " + b);
        swap(a, b);
        System.out.println(a + " " + b);
    }

    public static void swap(Integer a, Integer b) throws Exception {
        Field value = Integer.class.getDeclaredField("value");
        value.setAccessible(true);
        int temp= a;
        value.set(a,b);
        value.set(b,temp);
    }
}

使用javap -c Main即可看到如下的反汇编代码

public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: iconst_1
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: iconst_2
       6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       9: astore_2
      10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      13: new           #4                  // class java/lang/StringBuilder
      16: dup
      17: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      20: aload_1
      21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      24: ldc           #7                  // String
      26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      29: aload_2
      30: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      33: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      36: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_1
      40: aload_2
      41: invokestatic  #11                 // Method swap:(Ljava/lang/Integer;Ljava/lang/Integer;)V
      44: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      47: new           #4                  // class java/lang/StringBuilder
      50: dup
      51: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      54: aload_1
      55: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      58: ldc           #7                  // String
      60: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      63: aload_2
      64: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      67: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      70: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      73: return

  public static void swap(java.lang.Integer, java.lang.Integer) throws java.lang.Exception;
    Code:
       0: ldc           #12                 // class java/lang/Integer
       2: ldc           #13                 // String value
       4: invokevirtual #14                 // Method java/lang/Class.getDeclaredField:(Ljava/lang/String;)Ljava/lang/reflect/Field;
       7: astore_2
       8: aload_2
       9: iconst_1
      10: invokevirtual #15                 // Method java/lang/reflect/Field.setAccessible:(Z)V
      13: aload_0
      14: invokevirtual #16                 // Method java/lang/Integer.intValue:()I
      17: istore_3
      18: aload_2
      19: aload_0
      20: aload_1
      21: invokevirtual #17                 // Method java/lang/reflect/Field.set:(Ljava/lang/Object;Ljava/lang/Object;)V
      24: aload_2
      25: aload_1
      26: iload_3
      27: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      30: invokevirtual #17                 // Method java/lang/reflect/Field.set:(Ljava/lang/Object;Ljava/lang/Object;)V
      33: return
}

从头开始看,可以发现虚拟机是怎么实现自动装箱的
针对Integer a=1,这一行代码到底做了什么事情呢,对应如下的反汇编代码:

0: iconst_1
1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1

大体上这段代码表达的意思是将1压入栈顶,调用Integer.valueOf方法,将1保存到堆,所以我们知道了:int类型自动装箱使用的是Integer.valueOf方法,那我们可能需要看下valueOf方法的实现:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

根据这段代码,可以看出来,Integer类型的数,针对从IntegerCache.lowIntegerCache.high之间的数做了缓存,如果在这个区间,则不在创建对象,而从常量池中获取,那么区间大小呢,通过debug,可以得到是-128到127,如果是1,则保存在数组下标为1+(- -128)=129的位置,2则保存在130的位置。然后我们debug进入swap方法,在进入该方法前IntegerCache中下标129和130的值如下:
2019-07-08_092937
value.set(a,b); 这一行代码执行之后,我们不知道该数组发生了什么事情,紧接着执行value.set(b,temp);这一行代码,temp发生了自动装箱,我们debug进去看看:
2019-07-08_093419
可以发现,下标129和130的地方保存的值都是2,这时候value.set(b,temp);这行代码还未开始执行,这时候你就明白了,实际上temp变量在IntegerCache下标129的地方的在自动装箱之后变成了2,所以,b在修改变量的时候并并非没有成功,只是将IntegerCache下标130的地方从2改成了2而已。这时候假设我们写一段代码如下:

Integer c = 1;
System.out.println(c);

你猜结果是什么呢,当然是2,这就是为什么b变量没有修改成1的原因。

解决问题的关键是避免自动装箱

上面分析了问题出现的原因,现在来看下如何解决问题,那答案就明了了,只要禁止自动装箱,自然就么有问题了,所以,修改swap方法:

public static void swap(Integer a, Integer b) throws Exception {
        Field value = Integer.class.getDeclaredField("value");
        value.setAccessible(true);
        int temp= a;
        value.set(a,b);
        value.set(b,new Integer(temp));
    }

没错,只需要修改一行代码就可以解决这个问题了,当然还有另外的解决方法:调用Field的setInt方法也能解决这个问题

思考:这样做的风险极大,平日工作一定不要这么做

类似的问题使用反射修改了Integer类的常量池中的值,导致"1不再是1",这样导致了极大的线上风险,最好的方法是使用其它工具类一个方法返回多个值,比如ImmutablePair类或者ImmutableTriple等或者干脆自定义一个类封装下返回结果,虽然稍显笨拙,但是能从根本上消除常量池被改变的风险。