Java 形参与实参

2023-11-10

转自:https://dailycast.github.io/Java-形参与实参/

前几天在头条上看到一道经典面试题,引发了一些思考。也是写这篇文章的导火索。

背景

请看题:

public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.println("a=" + a + ",b=" + b);
        swap(a, b);
        System.out.println("a=" + a + ",b=" + b);
    }

    private static void swap(Integer numa, Integer numb) {
        //请实现
    }
}


看到这个题后 瞬间觉得有坑。也觉得为什么要书写一个 swap方法呢?如下实现不是更简单:

public static void main(String[] args) {
        Integer a = 1; 
        Integer b = 2;
        System.out.println("a=" + a + ",b=" + b);
        Integer tmp = a;
        a = b;
        b = tmp;
        System.out.println("a=" + a + ",b=" + b);
    }


输出:

a=1,b=2
a=2,b=1

完美实现交换。但是请注意,这是一道面试题,要的就是考验一些知识点。所以还是老老实实的实现 swap方法吧。

有的同学可能会想,Integer 是一个包装类型,是对Int的装箱和拆箱操作。其实也是一个对象。既然是对象,直接更改对象的引用不就行了?
思路没问题,我们首先看看实现:

private static void swap(Integer numa, Integer numb) {
       Integer tmp = numa;
       numa = numb;
       numb = tmp;
       System.out.println("numa=" + numa + ",numb=" + numb);
}

输出:

a=1,b=2
numa=2,numb=1
a=1,b=2

不出意外,没有成功

这是什么原因呢?
技术老手一看就知道问题出在形参和实参
混淆了

JAVA的形参和实参的区别:

形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。
形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。
因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。

实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在本例中 swap 方法 的numa, numb 就是形参,传递给 swap 方法的 a,b 就是实参

注意:
值传递调用过程中,只能把实参传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变。
而在引用传递调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上。
那么问题来了,什么是值传递引用传递

值传递和引用传递

在谈值传递引用传递之前先了解下 Java的数据类型有哪些

JAVA的数据类型

Java 中的数据类型分为两大类,基本类型对象类型。相应的,变量也有两种类型:基本类型引用类型
基本类型的变量保存原始值,即它代表的值就是数值本身,原始值一般对应在内存上的栈区
引用类型的变量保存引用值引用值指向内存空间的地址。代表了某个对象的引用,而不是对象本身。对象本身存放在这个引用值所表示的地址的位置。被引用的对象对应内存上的堆内存区
基本类型包括:byte,short,int,long,char,float,double,boolean 这八大基本数据类型
引用类型包括:类类型接口类型数组

变量的基本类型和引用类型的区别

基本数据类型在声明时系统就给它分配空间

int a;//虽然没有赋值,但声明的时候虚拟机就会 分配 4字节 的内存区域,而引用数据类型不同,它声明时只给变量分配了引用空间,而不分配数据空间:	
String str;//声明的时候没有分配数据空间,只有 4byte 的引用大小,在栈区,而在堆内存区域没有任何分配
str.length(); //这个操作就会报错,因为堆内存上还没有分配内存区域,而 a = 1; 这个操作就不会报错。


好了,Java的数据类型说完了,继续我们的 值传递引用传递的话题。

先背住一个概念:基本类型的变量是值传递引用类型的变量
结合前面说的 形参实参

值传递

方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,
此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值

引用传递

也称为地址传递址传递。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址
在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象
通过例子来说话:

static class Person {
       int age;
       Person(int age) {
           this.age = age;
       }
   }
   
   private static void test() {
       int a = 100;
       testValueT(a);
       System.out.println("a=" + a);
       Person person = new Person(20);
       testReference(person);
       System.out.println("person.age=" + person.age);
   }
   
   private static void testValueT(int a) {
       a = 200;
       System.out.println("int testValueT a=" + a);
   }
   
   private static void testReference(Person person) {
       person.age = 10;
   }


输出:

int testValueT a=200
a=100
person.age=10


看见  值传递 a的值并没有改变,而  引用传递的 persion.age已经改变了

有人说

private static void testReference(Person person) {
        person = new Person(100);
}

为什么 输出的 person.age 还是20呢?

我想说 了解一下什么是引用类型吧? 方法内把 形参的地址引用换成了另一个对象,并没有改变这个对象,并不能影响 外边实参还引用原来的对象,因为 形参只在方法内有效哦。

有人或许还有疑问,按照文章开头的例子,Integer也是 引用类型该当如何呢?
其实 类似的 String,Integer,Float,Double,Short,Byte,Long,Character等等基本包装类型类。因为他们本身没有提供方法去改变内部的值,例如Integer 内部有一个value 来记录int基本类型的值,但是没有提供修改它的方法,而且 也是final类型的,无法通过常规手段更改。
所以虽然他们是引用类型的,但是我们可以认为它是值传递,这个也只是认为,事实上还是引用传递,址传递


好了,基础知识补充完毕,然我们回到面试题吧


回归正题

private static void swap(Integer numa, Integer numb) {
        Integer tmp = numa;
        numa = numb;
        numb = tmp;
        System.out.println("numa=" + numa + ",numb=" + numb);
 }


那么思路来了,我们 通过特殊手段改变  Integer内部的 value属性通过补习基础知识,我们很明显知道 上面这个方法实现替换 是不可行的。因为 Interger虽然是 引用类型
但是上述操作只是改变了 形参的引用,而没有改变 实参对应的 对象

private static void swap(Integer numa, Integer numb) {
        Integer tmp = numa;
        try {
            Field field = Integer.class.getDeclaredField("value");
            field.setAccessible(true);
            field.set(numa, numb);//成功的将numa 引用的 1的对象 值改为 2
            field.set(numb, tmp); //由于 tmp 也是指向 numa 未改变前指向的堆 即对象1 ,经过前一步,已经将对象1的值改为了2,自然 numb 也是2,所以改动失效
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


输出结果:

a=1,b=2
a=2,b=2

又来疑问了?为何  a的值改变成功,而 b的改变失败呢?

见代码注释
所以其实 field.set(numb, tmp); 是更改成功的,只是 tmp 经过前一行代码的执行,已经变成了 2。
那么如何破呢?
我们有了一个思路,既然是 tmp的引用的对象值变量,那么我让tmp不引用 numa

private static void swap(Integer numa, Integer numb) {
       int tmp = numa.intValue();//tmp 定义为基本数据类型
       try {
           Field field = Integer.class.getDeclaredField("value");
           field.setAccessible(true);
           field.set(numa, numb);//这个时候并不改变 tmp 的值
           field.set(numb, tmp);
       } catch (Exception e) {
           e.printStackTrace();
       }
   }


这种情况下 对  numa 这个对象的修改就不会导致  tmp 的值变化了,看一下运行结果

a=1,b=2
a=2,b=2

这是为啥?有没有快疯啦?
难道我们的思路错了?
先别着急,我们看看这个例子:
仅仅是将前面的例子 a的值改为 129,b的值改为130

public static void main(String[] args) {
        Integer a = 129;
        Integer b = 130;

        System.out.println("a=" + a + ",b=" + b);
        swap(a, b);
        System.out.println("a=" + a + ",b=" + b);
    }

    private static void swap(Integer numa, Integer numb) {
        int tmp = numa.intValue();
        try {
            Field field = Integer.class.getDeclaredField("value");
            field.setAccessible(true);
            field.set(numa, numb);
            field.set(numb, tmp);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


a=129,b=130
a=130,b=129

运行结果:

有没有怀疑人生?我们的思路没有问题啊?为什么 换个数值就行了呢?
我们稍微修改一下程序

public static void main(String[] args) {
        Integer a = new Integer(1);
        Integer b = new Integer(2);

        System.out.println("a=" + a + ",b=" + b);
        swap(a, b);
        System.out.println("a=" + a + ",b=" + b);
    }

    private static void swap(Integer numa, Integer numb) {
        int tmp = numa.intValue();
        try {
            Field field = Integer.class.getDeclaredField("value");
            field.setAccessible(true);
            field.set(numa, numb);
            field.set(numb, tmp);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


运行结果:

a=1,b=2
a=2,b=1

哎?为啥 1 和 2 也可以了?

我们这时肯定猜想和Integer的装箱 拆箱有关

装箱,拆箱 概念

Integer的装箱操作

为什么 Integer a = 1 和 Integer a = new Integer(1) 效果不一样
那就瞅瞅源码吧?

public Integer(int value) {
    this.value = value;
}

/**
 * Returns an {@code Integer} instance representing the specified
 * {@code int} value.  If a new {@code Integer} instance is not
 * required, this method should generally be used in preference to
 * the constructor {@link #Integer(int)}, as this method is likely
 * to yield significantly better space and time performance by
 * caching frequently requested values.
 *
 * This method will always cache values in the range -128 to 127,
 * inclusive, and may cache other values outside of this range.
 *
 * @param  i an {@code int} value.
 * @return an {@code Integer} instance representing {@code i}.
 * @since  1.5
 */
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}


反射修改前:通过注释知道,java推荐  Integer.valueOf 方式初始化一个 Interger因为有 缓存了 -128 - 127的数字
我们直接定义  Integer a = 1 具有这个功能,所以 Jvm 底层实现 是通过  Integer.valueOf这个方法
再看  field.set(numb, tmp);
我们打断点,发现通过反射设置  value时 竟然走了  Integer.valueOf 方法
下面是 我们调用  swap前后的  IntegerCache.cache 值得变化

反射修改后


在反射修改前

IntegerCache.cache[128]=0
IntegerCache.cache[129]=1
IntegerCache.cache[130]=2

IntegerCache.cache[128]=0
IntegerCache.cache[129]=2
IntegerCache.cache[130]=2

通过反射修改后

再调用 field.set(numb, tmp) tmp这时等于1 对应的 角标 129 ,但是这个值已经变成了2
所以出现了刚才 奇怪的结果
原来都是缓存的锅
下面趁机再看个例子 加深理解

Integer testA = 1;
Integer testB = 1;

Integer testC = 128;
Integer testD = 128;
System.out.println("testA=testB " + (testA == testB) + ",\ntestC=testD " + (testC == testD));

testA=testB true,
testC=testD false

输出结果:

通过这小示例,在 -128 到 127的数字都走了缓存,这样 testA 和 testB引用的是同一片内存区域的同一个对象。
而 testC testD 数值大于127 所以 没有走缓存,相当于两个Integer对象,在堆内存区域有两个对象。
两个对象自如不相等。
在前面的示例中 我们 通过

Integer a = new Integer(1);
Integer b = new Integer(2);

方式初始化 a,b 我们的交换算法没有问题,也是这个原因。

那么到目前为止我们的swap 方法可以完善啦

private static void swap(Integer numa, Integer numb) {
        int tmp = numa.intValue();
        try {
            Field field = Integer.class.getDeclaredField("value");
            field.setAccessible(true);
            field.set(numa, numb);
            field.set(numb, new Integer(tmp));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

到此, 这个面试我们已经通过了,还有一个疑问我没有解答。只需将之前的 field.set(numb, tmp) 改为 field.set(numb, new Integer(tmp))

为什么 field.set(numb, tmp) 会执行 Integer.valueOf() 而 field.set(numb, new Integer(tmp)) 不会执行。
这就是Integer的装箱操作,当 给 Integer.value 赋值 int时,JVM 检测到 int不是Integer类型,需要装箱,才执行了Integer.valueOf()方法。而field.set(numb, new Integer(tmp)) 设置的 是Integer类型了,就不会再拆箱后再装箱。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 形参与实参 的相关文章

  • 为什么 Hashtable 不允许空键或空值?

    正如 JDK 文档中所指定的 Hashtable 不允许空键或空值 HashMap 允许一个空键和任意数量的空值 为什么是这样 Hashtable 是较旧的类 通常不鼓励使用它 也许他们看到了对 null 键的需要 更重要的是 null 值
  • 从txt文件中读取数据而不下载它?

    我想从提供的文本文件中解析信息 有没有一种方法可以在应用程序中执行此操作 而无需先下载文件 以某种方式传输文本内容 打开到 URL 的 Http 连接 使用内置 HttpURLConnection 或使用 commons httpclien
  • 将 WAR 部署到 Tomcat(Spring Boot + Angular)

    我正在尝试使用以下命令部署 Spring Boot 应用程序WAR包装至Tomcat 10 应用程序已成功部署 但是 当我尝试访问端点时 它会导致404 未找到 战争文件 应用程序 war http localhost 8080 appli
  • IBM Websphere MQ - 用于 Tomcat 部署的 EJB 和 MDB 迁移

    我已经为此苦苦挣扎了很长一段时间 我有一个 IBM Websphere MQ 它使用 EJB 和 MDB 以下是配置ejb mdb的地方
  • 如何将webview内容划分为多个页面

    我必须使用 Android 上的 PdfDocument 从 webView 创建 PDF https developer android com reference android graphics pdf PdfDocument htm
  • Selector.close() 是否关闭所有客户端套接字?

    我是 nio 套接字的新手 我已经使用 nio 套接字编写了一个服务器 现在我正在尝试编写关闭钩子以确保通过清理资源正常退出 我的问题是Selector close 方法关闭所有客户端套接字 如果没有 请告诉我如何访问所有客户端套接字 而无
  • 二元运算符 >=、-、* 的错误操作数类型

    我无法弄清楚如何修复代码中不断出现的这些错误 import java util Scanner public class Unit02Prog1 public static void main String args Scanner inp
  • Android Studio 与 Google Play 服务的编译问题

    我正在运行 Android Studio 0 8 4 并在 Android Studio 0 8 2 上尝试过此操作 我正在运行 Java JDK 1 8 0 11 并尝试使用 JDK 1 8 0 05 每当我尝试构建我的 android
  • Spring 从 JBoss 上下文加载 PropertySourcesPlaceholderConfigurer

    我有一个使用 PropertySourcesPlaceholderConfigurer 的 spring 3 1 应用程序加载设置 我想管理测试和生产环境 只需从服务器上下文加载设置覆盖本地文件属性中指定的设置 下一个示例在 Tomcat
  • 如何在正则表达式中编写可选单词?

    我想编写一个识别以下模式的 java 正则表达式 abc def the ghi and abc def ghi 我试过这个 abc def the ghi 但是 它没有识别第二种模式 我哪里出错了 abc def the ghi 删除多余
  • 是否可以使用 Apache Tika 提取表信息?

    我正在寻找 pdf 和 MS Office 文档格式的解析器 以从文件中提取表格信息 当我看到 Apache Tika 时 正在考虑编写单独的实现 我能够从任何这些文件格式中提取全文 但我的要求是提取表格数据 我希望有 2 列采用键值格式
  • 生成一定长度的所有排列

    假设我们有一个字母表 abcdefghiklimnop 如何以有效的方式以五个一组的形式重复该字母表来递归生成排列 几天来我一直在为此苦苦挣扎 任何反馈都会有帮助 本质上这与 生成给定字符串的所有排列 https stackoverflow
  • JFrame 在连续运行代码时冻结

    我在使用时遇到问题JFrame 它会冻结 连续运行代码 下面是我的代码 点击时btnRun 我调用了该函数MainLoop ActionListener btnRun Click new ActionListener Override pu
  • 扩展多个类

    我知道 Java 不支持多重继承 因为不允许扩展多个类 我只是想知道我的问题是否有解决方法 我有一个名为CustomAction需要扩展两个抽象类 BaseAction and QuoteBaseAction 我无法更改这些抽象类中的任何一
  • 为 REST API 生成 Swagger UI 文档

    我使用 Java 中的 JAX RS Jersey 开发了 REST API 我想为其转换 生成基于 Swagger 的 UI 文档 谁能以简单的方式告诉我如何做到这一点的精确 步骤 很抱歉 他们网站上给出的步骤对我来说有点模糊 有多种方法
  • Hybris:如何在impex中导入zip文件中的媒体?

    我知道我们可以导入未像这样压缩的图像 siteResource jar com project initialdata constants ProjectInitialDataConstants projectinitialdata imp
  • 向Java类库添加函数

    我使用的 Java 类库在很多方面都不完整 有很多类我认为应该内置其他成员函数 但是 我不确定添加这些成员函数的最佳实践 让我们调用不足的基类A class A public A long arbitrary arguments publi
  • 如何在一次操作中使用 Thymeleaf 检查 null 和空条件?

    有什么方法可以检查 Thymeleaf 中的 null 和empty 条件吗 方法一 1 variable1 variable2 variable3 2 variable null 3 variable 如果我们结合两个条件 例如 vari
  • junit4 使用特定测试方法创建测试套件

    在 junit4 中 我想执行来自不同类的特定测试方法 即想要使用来自不同类的特定测试方法创建一个测试套件 假设我有两门课 public class Test Login Test public void test Login 001 Sy
  • Axis2 的 wsdl2java 在 RPC/Encoded 样式 Web 服务上失败

    Axis2 有替代方案吗 或者让它工作的方式 例如不同的数据绑定 Retrieving document at Exception in thread main org apache axis2 wsdl codegen CodeGener

随机推荐