一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域

原题代码如下:

一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域
 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},在内存里的空间如下图所示:

  一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域

  我们在程序中声明 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 在方法中传递参数是值传递方式。不管是基本类型还是引用类型,都是值传递。实参如果是基本类型,值是基本类型的值的一个拷贝。实参如果是引用类型,值是该引用的内存地址的一个拷贝。

  参数为引用类型时的值传递图示如下:

  一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域

  注:ref name 和 val 都是我对 jvm 对数据类型编码的假设字段和结构,名称也意在见名知意,方便理解。JVM 具体实现可能还有其他辅助字段,或结构更复杂,但我觉得这两个字段应该是必须的。

  当一个数据对象为引用类型时,其 val 保存的是它指向的堆内存空间上真正实例的内存地址。

  如上图所示,当我们将 o1 作为参数传入某个方法中,方法的形参名为 o2(也可以为 o1),两者在 JVM 内存中分别位于两块不同的函数帧上,假设其地址分别为 0x1000 和 0x2000,表示了不同的内存空间。上图可以看做是上面三行代码中第一行和第二行代码的等效图,也可以是第一行和第三行代码的等效图。

  如果数据对象是基本类型,val 保存的就是基本类型的值,对于上图来讲则是少了指向 POJO 对象的指针,因为 val 已经是值了不会再指向其他地方。这个图比较简单这里省略不画。

  以上,就是 java中值传递基本概念的理解分析。

  到这里,test1 和 test2 的输出相信你已经会分析了。

 

test3 的输出如何分析?

  分析 test3 就是去分析 swap3,直接贴上我在代码中的注释:

一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域
 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 代码修改如下:

一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域
 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 验证反射的影响

  最终输出为:

一道 Java 方法传值面试题——Java方法传值的值传递概念和效果 + Integer 缓存机制 + 反射修改 private final 域
before: a=1, b=2
after: a=2, b=2
after reflect: c=2, d=2
test3 修改后输出验证

 

 

开篇 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

上一篇:python全栈闯关--6-小知识点总结


下一篇:Leetcode之动态规划(DP)专题-264. 丑数 II(Ugly Number II)