ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2019-10-08 13:01:15  阅读:249  来源: 互联网

标签:tmp 面试题 Java java i1 i2 引用 Integer 传值


原题代码如下:

 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”。引用一条来自于 stackoverflow 的答案,投票最多的那条就是:https://stackoverflow.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=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

标签:tmp,面试题,Java,java,i1,i2,引用,Integer,传值
来源: https://www.cnblogs.com/christmad/p/11589867.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有