一道 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”。引用一条来自于 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

原文地址:https://www.cnblogs.com/christmad/p/11589867.html

时间: 2024-10-14 10:23:02

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

Java反射-修改private final成员变量值,你知道多少?

大家都知道使用java反射可以在运行时动态改变对象的行为,甚至是private final的成员变量,但并不是所有情况下,都可以修改成员变量.今天就举几个小例子说明.  基本数据类型 String类型 Integer类型 总结 首先看下对基本类型的修改: /** * @author Cool-Coding 2018/5/15 */ public class ReflectionUsage {private final int age=18; public int getAge(){ return

《Java中方法的参数传递方式只有一种:值传递》

1 //方法的参数传递机制(1):基本类型做形参的传递. 2 class PrimitiveTransferTest 3 { 4 public static void swap(int a,int b) 5 { 6 //下面代码实现a和b交换 7 int temp = a; 8 a = b; 9 b = temp; 10 System.out.println("swap方法里,a的值是:"+a+",b的值是:"+b); 11 } 12 public static v

【翻译】Integer Cache In Java(在Java中的Integer缓存机制)

返回主页 回到顶端 This Java article is to introduce and discuss about Integer Cache. 这篇Java文章将介绍和讨论整数缓存. This is a feature introduced in Java 5 to save memory and improve the performance. 这是Java 5中引入的一个特性,用于节省内存和提高性能. Let us first have a look at a sample cod

我的Java开发学习之旅------>Java语言中方法的参数传递机制

实参:如果声明方法时包含来了形参声明,则调用方法时必须给这些形参指定参数值,调用方法时传给形参的参数值也被称为实参. Java的实参值是如何传入方法?这是由Java方法的参数传递机制来控制的,Java里方法的参数传递方式只有一种:值传递.所谓值传递,就是将实际参数的副本(复制品)传入方法内,而参数本身不会收到任何影响. 一.参数类型是原始类型的值传递 下面通过一个程序来演练 参数类型是原始类型的值传递的效果: public class ParamTransferTest { public sta

java方法参数传递方式只有----值传递!

在通常的说法中,方法参数的传递分为两种,值传递和引用传递,值传递是指将实际参数复制一份传递到方法中, 在方法中的改动将不会影响到实际参数本身,而引用传递则是指传递的是实际参数本身,在方法中的改动将会影响到实 际参数本身.但是,在java中只有值传递,没有引用传递!那么,为什么当方法参数是基本数据类型时表现是值传递, 而当是引用类型时表现的是引用传递形式呢? Java内存区域中含有java堆和虚拟机栈两个内存区域(并不是只是将java内存区分为这两个内存区域,此外还有程 序计数器,本地方法栈以及方

Java Object 引用传递和值传递

Java Object 引用传递和值传递 @author ixenos Java中的引用传递: 除了在将参数传递给方法(或函数)的时候是"值传递",传递对象引用的副本,在任何用"="向引用对象变量赋值的时候都是"引用传递",传递对象的引用给另一个变量. 参数传递,传递引用的副本,这看起来是引用传递,实则是传递了副本,这已经是值传递的概念了: 变量赋值,传递引用,这算引用传递 Java参数传递中没有引用传递都是值传递 1.在 Java 应用程序中永

JAVA 值传递

Java里方法的参数传递方式只有一种:值传递 值传递:当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋给方法的形参变量,方法的操作的并不是实际的实参变量 引用型变量:系统复制的是变量,就是引用地址,并没有复制对象本身 eg. 1 class Value{ 2 public int i=15; 3 } 4 public class Test{ 5 public static void main(String argv[]){ 6 Test t=new Test(); 7 t.fir

全面剖析Smarty缓存机制二[清除缓存方法]

前段时间,写了一篇 Smaryt缓存机制的几种缓存方式 ,详细介绍了三种缓存方式:全局缓存.部分缓存.局部缓存,以及通过is_cache()判断是否存在缓存来进行缓存生成.本来这篇早该完成,由于时间关系推到今天,还好思绪没有忘掉,闲话不多说,今天主要讲解Smarty缓存机制中如何清除缓存以及缓存集合的使用技巧,下面步入正题. 一.普通清除缓存方法总所周知,当你看了上一篇文章,会知道通过如下方法,对Smarty的缓存进行清除:代码示例:$smarty->clear_cache("index.

java方法参数传递面试题

传值还是传引用是Java中很基础的一个问题,也是笔试的时候经常被考察的一个问题,总结一下. 题目1: 写出以下程序的输出内容. public class Test { public static void changeValue(int value){ value = 0; } public static void main(String[] args) { int value = 2010; changeValue(value); System.out.println(value); } }