原题代码如下:
1 public void test1() { 2 int a = 1, b = 2; 3 System.out.println("before: a=" + a + ", b=" + b); 4 swap1(a, b); 5 System.out.println("after: a=" + a + ", b=" + b); 6 } 7 8 private void swap1(int i1, int i2) { 9 int tmp = i1; 10 i1 = i2; 11 i2 = tmp; 12 } 13 14 public void test2() { 15 Integer a = 1, b = 2; 16 System.out.println("before: a=" + a + ", b=" + b); 17 swap2(a, b); 18 System.out.println("after: a=" + a + ", b=" + b); 19 } 20 21 22 public void test3() throws NoSuchFieldException, IllegalAccessException { 23 Integer a = 1, b = 2; 24 System.out.println("before: a=" + a + ", b=" + b); 25 swap3(a, b); 26 System.out.println("after: a=" + a + ", b=" + b); 27 } 28 29 30 private void swap2(Integer i1, Integer i2) { 31 Integer tmp = i1; 32 i1 = i2; 33 i2 = tmp; 34 } 35 36 private void swap3(Integer i1, Integer i2) throws NoSuchFieldException, IllegalAccessException { 37 Field f = Integer.class.getDeclaredField("value"); 38 f.setAccessible(true); 39 int tmp = i1.intValue(); 40 f.set(i1, i2.intValue()); 41 f.set(i2, tmp); 42 }题目
上述代码中,test1、test2、test3 方法运行后 a、b 前后的值分别是多少???
思考一下......黄金100秒......
......
......
......
答案放在本篇末尾,需要你稍稍滚动一下页面,并且希望是有思考过后再来对答案。最后的目的是真正掌握其中的原理。
分析:
这道题考察的点有三个:1.Java方法传值是引用传递还是值传递 2.对 Integer Cache机制的了解 3.反射可以修改 private final 域吗?
A1:java方法传值为值传递,没有引用传递。
A2:Integer Cache机制需要查看 Integer源码,默认情况下对 [low=-128, high=127] 这些基本 int 型的 Integer 对象缓存,返回缓存好的对象。可以修改最大值 high,将参数java.lang.Integer.IntegerCache.high 传入即可。
A3:反射可以修改 private final 域。结合本题,最终 test3 输出你答对了吗?理解 test3 的输出还需要考虑到自动装箱和拆箱机制。
理解引用传递和值传递的区别:
首先直接抛结论:java中是没有引用传递的,“Java is always pass-by-value”。引用一条来自于 * 的答案,投票最多的那条就是:https://*.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
什么是引用?
Java 中所有对象类型分为引用类型和基本类型(8种)。基本类型为 char, boolean, byte, short, int, long, float, double 。除了这 8 个基本类型以外其他类型都是引用类型。
如 String s = "java"; 我们把 s 称为一个引用,类型是 String。
引用传递有什么用?没有引用传递会怎么样?
先看一下这一小段 java 代码:
POJO o1 = new POJO("zhangsan"); POJO o2 = o1;
POJO o3 = getPOJOfrom(o2);
上面是在 java 中修改引用的三种方法,一种是使用 new 开辟一个全新的空间,如第一行代码;第二种是用其他同类型的实例赋值,如第二行代码;第三种是通过方法返回值修改引用,如第三行代码。
以上三种方法的缺点就是,每次只能修改一个引用的值。
方法中引用传递的作用就是让我们可以在方法中修改实参的引用,并且由于方法可以接收多个参数,这让我们可以在方法中同时修改多个引用的值。
虽然 java 没有引用传递,看不了同时修改多个参数的例子,但是我们可以去看一下 redis 源码,找到 ziplist.c 搜索"__ziplistInsert"方法,往下找 20 行左右,看到 zipTryEncoding(s,slen,&value,&encoding) 方法。注意到方法中最后两个参数 &value, &encoding 都带有特殊的符号 &, 这是 C 语言中取地址的运算符,将对应参数的地址传进去。然后在 zipTryEncoding 方法中我们看到使用了 *val 和 *encoding,* 是取值运算符,对地址取值,对应着我们在代码的外部方法中声明的一个个原始变量(包含基本类型和引用类型)。这是我在学习 redis 内部数据结构查看源码时发现的一个特点,在源码很多地方都用到了引用传递这个技巧。(问了下搞go开发的朋友,go也有引用传递。好吧,java 就是没有)
所以,没有引用传递的 java,通过调用方法只能一次修改一个引用,这是通过方法返回值办到的。
需要强调的一点是,地址编号是一个虚拟的东西。内存有很多物理段,为了方便 CPU 使用,操作系统使用虚拟地址编号加速 CPU 查找指定位置内存的速度——这样就不需要随机查找,或者从头开始遍历。实际上物理内存中存储的是01串,这些01串在指定编码下可以表示一些特定的值。当 CPU 访问内存上某个地址时,可能直接访问到某个真实值,也有可能访问到一个指针——指向下一个内存空间,比如链表的 next 指针就是这一特性。如果举例一片连续的内存空间全都是存储同一种类型的值的话,非数组莫属了。如果数组是基本类型,那么数组那一片连续空间中存储的全是数组成员的值;如果数组是引用类型,则连续空间中存储的全是引用。例如一个数组 int[] arr = new int[]{5,4,3,2,1},在内存里的空间如下图所示:
我们在程序中声明 int[] arr 时获得 arr 的引用(地址),如上图中的 0x01011000,这个地址中存储着某个值 ,如上图中的 0x10000000,JVM在读到 0x10000000 这个值时应该可以识别它是一个地址指针还是一个具体的值(记得周志明那本《深入理解java虚拟机》提到过,应该是根据特定编码来完成识别的吧),如果是一个指针,就继续寻址,直到读到具体指为止。 上图中 0x01011000 这个地址下的值 0x10000000 是一个地址指针,也是数组第一个成员的内存空间地址,里面存储的是第一个成员的值 5。从 0x10000000~0x10000004 的连续地址是 5 个int数组成员的内存地址。当我们操作数组 arr 时,首先会拿到 0x01011000 这个地址,然后读取里面的值 0x10000000,根据 arr[0]、arr[1]、arr[2]、arr[3]、arr[4] 下标来访问不同地址空间的数组成员。
如果有这么一个函数func(address, val),参数 address 表示内存地址,val 表示你想要设置的值,暂时不考虑值宽度,那么当我们掌握内存地址的时候,就可以随意修改地址里面的值,在 java 中操作数组成员时就是这样一个操作,比如 a[1] = 6;
值传递是什么样的?
java 在方法中传递参数是值传递方式。不管是基本类型还是引用类型,都是值传递。实参如果是基本类型,值是基本类型的值的一个拷贝。实参如果是引用类型,值是该引用的内存地址的一个拷贝。
参数为引用类型时的值传递图示如下:
注:ref name 和 val 都是我对 jvm 对数据类型编码的假设字段和结构,名称也意在见名知意,方便理解。JVM 具体实现可能还有其他辅助字段,或结构更复杂,但我觉得这两个字段应该是必须的。
当一个数据对象为引用类型时,其 val 保存的是它指向的堆内存空间上真正实例的内存地址。
如上图所示,当我们将 o1 作为参数传入某个方法中,方法的形参名为 o2(也可以为 o1),两者在 JVM 内存中分别位于两块不同的函数帧上,假设其地址分别为 0x1000 和 0x2000,表示了不同的内存空间。上图可以看做是上面三行代码中第一行和第二行代码的等效图,也可以是第一行和第三行代码的等效图。
如果数据对象是基本类型,val 保存的就是基本类型的值,对于上图来讲则是少了指向 POJO 对象的指针,因为 val 已经是值了不会再指向其他地方。这个图比较简单这里省略不画。
以上,就是 java中值传递基本概念的理解分析。
到这里,test1 和 test2 的输出相信你已经会分析了。
test3 的输出如何分析?
分析 test3 就是去分析 swap3,直接贴上我在代码中的注释:
1 private void swap3(Integer i1, Integer i2) throws NoSuchFieldException, IllegalAccessException { 2 // 假设入参是 i1=1, i2=2,下面代码运行后 after 输出为 a=2,b=2 3 // 原因:IntegerCache 机制的存在,反射修改的是 IntegerCache 中数组的值。 4 // 在本例代码前提下,IntegerCache 数组中 ...-2,-1,0,1,2,3... 被修改为 ...-2,-1,0,2,2,3... 5 // 如代码 f.set(i1, i2.intValue()); 实际是将 IntegerCache 数组中 i1 对应位置的值修改为 i2.intValue() 6 // 而在代码 f.set(i2, tmp); 中,由于方法要求入参为 Object 类型,所以 tmp 会被装箱(前面 f.set(i1, i2.intValue()); 也一样), 7 // 而 tmp 被装箱之后会使用 IntegerCache 数组,你以为用的还是 1,但是 IntegerCache 数组原 1 的位置已经变成 2 了, 8 // 最终就是代码根本没有用到 int tmp 的值 9 // 解决这个问题,就是要规避 Java 对基本类型的自动装箱机制(实际调用的包装类型的 valueOf() 方法,如本例中引入了 IntegerCache),操作如下: 10 // Integer tmp = new Integer(i1.intValue()); 11 // 原因:使用 new 总是会申请新的空间,有了显式的 new 就能规避基本类型的自动装箱机制,程序运行时就不会使用 IntegerCache 中的数组缓存对象, 12 // 因此在 f.set(i2, tmp); 时就能使用我们所期望的、被提前保存的 tmp 在新内存空间的值 13 // 经过上述修改后,IntegerCache 数组中的值,从初始化的 ...-2,-1,0,1,2,3... 变为 ...-2,-1,0,2,1,3... 14 Field f = Integer.class.getDeclaredField("value"); 15 f.setAccessible(true); 16 int tmp = i1.intValue(); 17 f.set(i1, i2.intValue()); 18 f.set(i2, tmp); 19 }test3 输出分析
如何验证 test3 输出结果?
如何验证 反射修改了 IntegerCache 数组??只要将 test3 代码修改如下:
1 public void test3() throws NoSuchFieldException, IllegalAccessException { 2 Integer a = 1, b = 2; 3 System.out.println("before: a=" + a + ", b=" + b); 4 swap3(a, b); 5 System.out.println("after: a=" + a + ", b=" + b); 6 7 // 验证使用反射的方法 swap(a,b) 后 IntegerCache 数组的值 8 Integer c=1, d=2; 9 System.out.println("after reflect: c=" + c + ", d=" + d); 10 }test3 验证反射的影响
最终输出为:
before: a=1, b=2 after: a=2, b=2 after reflect: c=2, d=2test3 修改后输出验证
开篇 test1、test2、test3 output 揭晓:
test1 output:
before: a=1, b=2
after: a=1, b=2
test2 output:
before: a=1, b=2
after: a=1, b=2
test3 output:
before: a=1, b=2
after: a=2, b=2