zoukankan      html  css  js  c++  java
  • 一道 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

  • 相关阅读:
    mysql 创建数据库的基本操作
    MySQL的数据类型 及注意事项
    在执行 pip install 时遇到错误:python setup.py egg_info Check the logs for full command output
    python3.8-运行jupyter 报raise NotImplementedError
    执行python 爬虫脚本时提示bs4.FeatureNotFound: Couldn't find a tree builder with the features you requested: lxml. Do you need to install a parser library?
    Python:lambda表达式和yield关键字理解与使用讲解
    百度paddle框架学习(二):使用经典VGG网络完成人脸口罩判别
    深度学习中的梯度
    C++ OpenCV学习笔记(持续更新)
    Tensorflow常见报错
  • 原文地址:https://www.cnblogs.com/christmad/p/11589867.html
Copyright © 2011-2022 走看看