面试题系列第7篇:Java方法到底是值传递还是引用传递?

简介: 面试题系列第7篇:Java方法到底是值传递还是引用传递?

《Java面试题系列》:一个长知识又很有意思的专栏。深入挖掘、分析源码、汇总原理、图文结合,打造公众号系列文章,面试与否均可提升Level。欢迎持续关注【程序新视界】。本篇为第7篇。


要点:Java方法到底是值传递还是引用传递?


这个问题一直在技术讨论区争论不休。对于初级人员来说很具有迷惑性,即便对于很多高级开发来说,也搞不清楚。本篇文章就带大家探究一下底层的原理,最终化繁为简,让大家通过一两句话就明白到底是值传递还是引用传递。


面试题

常见面试题,执行以下代码,查看对应的打印结果:


@Test
public void test1() {
  String[] arr = {"关注", "程序", "新视界"};
  String name = "二师兄";
  System.out.println("before change : arr=" + Arrays.toString(arr));
  System.out.println("before change : name=" + name);
  change(arr, name);
  System.out.println("after change : arr=" + Arrays.toString(arr));
  System.out.println("after change : name=" + name);
}
public void change(String[] arr, String name) {
  arr[0] = "公众号";
  name = new String("Steven");
  System.out.println("in change method : arr=" + Arrays.toString(arr));
  System.out.println("in change method : name=" + name);
}

先想一下三个地方打印的结果有什么不同,再对照一下执行程序之后的结果:

before change : arr=[关注, 程序, 新视界]
before change : name=二师兄
in change method : arr=[公众号, 程序, 新视界]
in change method : name=Steven
after change : arr=[公众号, 程序, 新视界]
after change : name=二师兄

是不是有疑问,为什么在change方法内修改了name,但最终结果却并没有修改?而arr修改了第0个位置的内容却成功的修改了?难道第一个参数是引用传递,第二个参数是值传递?


下面我们来逐一解决上面的疑问,并汇总最终的结论。


值传递与引用传递

首先来了解一下值传递和引用传递的概念:


值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,在函数内对参数进行修改,不会影响到实际参数。


引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数内,在函数内对参数所进行的修改,将影响到实际参数。


通过概念我们可以得出区分值传递和引用传递的本质区别就是:实际参数的值是复制了一份,还是直接拿去用了。


基础类型和String的传递过程

在此问题上经常有一个理解误区:值传递和引用传递区分的条件是传递的内容,如果是个值就是值传递。如果是个引用,就是引用传递。


注意,上面的理解是错误的,值传递和引用传递的表现形式貌似与对象类型有关,但本质上是无关的。


先来看一下基础类型和String类型通常在内存中存在的形式。更多展现形式我们在本系列文章的前几篇文章中已经多次提到。


image.png总结一下就是方法内基础类型的引用和值分配在栈中,String类型引用在栈中,值在堆(堆或字符串常量池)中。

先来看基础类型(以int为例)作为参数传递给方法时的内存结构:

@Test
public void test2(){
  int a = 10;
  change(a);
}
public void change(int b) {
  b = 2;
}

首先在未调用方法时,变量a的内存分布如上图,当调用change方法时,int类型的a和b变量在内存中如下:

image.png很明显针对方法的形参b,在栈中重新复制了一份a的数据。此时,再对b重新赋值,内存会发生如下变化:

image.png而我们知道,栈中的内容随着(change)方法的执行完成,便随之消失。也就说在整个过程中change方法只是复制了a的值,在方法内对变量b的操作并没有影响到a原本的值。

对照上面的概念,很轻易发现这个过程中就是值传递。下面再来看看针对String对象的内存结构变化。

@Test
public void test3(){
  String name = "Tom";
  change(name);
}
public void change(String str) {
  str = new String("Steven");
}

起初时内存结构与第一张图一样,name在栈中存储,值Tom在字符串常量池中存储。当调用change方法时,内存结构如下:image.pngimage.png此时可以看到方法内执行的赋值操作只是改变了str变量中存储的字符串的地址,并未影响到原有的name变量所指向的字符串。


以上是关于基础类型和String类的修改都符合值传递的要求:将实际参数复制一份,修改复制的参数值并不影响原有值。只不过针对String类型复制的实际值是变量中存储的引用地址。


有朋友说:不对啊,我传递进来一个对象,然后调用其set方法也可以修改对象的内容啊!


那么,我们再来看看引用类型的内存结构图。


引用类型的值传递

我们都知道数组也属于引用类型,但为了更加明确,我们这里新创建User对象来进行验证。


@Test
public void test4() {
  User user = new User();
  user.setName("Tom");
  change(user);
}
public void change(User paramUser) {
  paramUser.setName("Steven");
}
class User{
  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
}

image.pngimage.png可以看出paramUser参数同样复制了user参数中的地址,因此也指向了堆中的User对象。那么,有朋友说User对象的确被改了啊!


需要注意的是,我们关注的不是对象内部的结构是否被改变,而是复制的原有的user参数的值是否被改变。从上图可以明显看出,原有的user中的值(引用地址),并没有发生任何改变。


那直接对paramUser赋值新对象会不会改变原有user变量的值?为了进一步验证,我们修改一下change方法中的内容:


@Test
public void test4() {
  User user = new User();
  user.setName("Tom");
  change(user);
}
public void change(User paramUser) {
  paramUser = new User();
  paramUser.setName("Steven");
}

在change方法执行之前,内存结构与上面的结构一样,但执行之后,内存结构便发生了新的变化。image.png如果是引用传递的话,那么对paramUser进行重新赋值,肯定会改变原有user对应的值。但这里很显然,赋值之后paramUser只是指向了一个新的堆中的对象,并未影响到原有的user值。


小结

通过上面的一步步分析和演示,我们可以很明显的看到出:如果是基础类型,那么在方法传递的时候复制的是(栈中)基础类型的引用和值,如果是引用类型复制的是(栈中)引用地址。


也就是说无论通过什么类型,最终都是进行了一份复制操作,而并不是直接传递实际值的引用传递。所以,在Java中本质上只有值传递,也就说Java的传参只会传递它的副本,并不会传递参数本身。


最后,通过上面的分析过程,大家是不是发现当了解了JVM的内存结构之后,很多令人疑惑的问题便迎刃而解了?所以,赶紧深入学习一下吧。



目录
相关文章
|
12天前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
2月前
|
算法 Java 开发者
Java 项目实战数字华容道与石头迷阵游戏开发详解及实战方法
本文介绍了使用Java实现数字华容道和石头迷阵游戏的技术方案与应用实例,涵盖GUI界面设计、二维数组操作、游戏逻辑控制及自动解法算法(如A*),适合Java开发者学习游戏开发技巧。
198 46
|
2月前
|
算法 Java
Java语言实现链表反转的方法
这种反转方法不需要使用额外的存储空间,因此空间复杂度为,它只需要遍历一次链表,所以时间复杂度为,其中为链表的长度。这使得这种反转链表的方法既高效又实用。
194 0
|
2月前
|
存储 Java 数据处理
Java映射操作:深入Map.getOrDefault与MapUtils方法
结合 `getOrDefault`方法的简洁性及 `MapUtils`的丰富功能,Java的映射操作变得既灵活又高效。合理地使用这些工具能够显著提高数据处理的速度和质量。开发人员可以根据具体的应用场景选择适宜的方法,以求在性能和可读性之间找到最佳平衡。
103 0
|
2月前
|
缓存 人工智能 NoSQL
Java中实现Token设置过期时间的方法
本文介绍了在Java应用中实现Token设置过期时间的多种方法,包括使用JWT和Redis缓存,并结合定时任务清理过期Token,以提升系统安全性与用户隐私保护。
269 0
|
3月前
|
安全 Java API
Java 集合高级应用与实战技巧之高效运用方法及实战案例解析
本课程深入讲解Java集合的高级应用与实战技巧,涵盖Stream API、并行处理、Optional类、现代化Map操作、不可变集合、异步处理及高级排序等核心内容,结合丰富示例,助你掌握Java集合的高效运用,提升代码质量与开发效率。
209 0
|
3月前
|
算法 搜索推荐 Java
Java中的Collections.shuffle()方法及示例
`Collections.shuffle()` 是 Java 中用于随机打乱列表顺序的方法,基于 Fisher-Yates 算法实现,支持原地修改。可选传入自定义 `Random` 对象以实现结果可重复,适用于抽奖、游戏、随机抽样等场景。
128 0
|
3月前
|
安全 Java
JAVA:Collections类的shuffle()方法
`Collections.shuffle()` 是 Java 中用于随机打乱列表顺序的工具方法,适用于洗牌、抽奖等场景。该方法直接修改原列表,支持自定义随机数生成器以实现可重现的打乱顺序。使用时需注意其原地修改特性及非线程安全性。
141 0
|
3月前
|
算法 安全 Java
java中Collections.shuffle方法的功能说明
`Collections.shuffle()` 是 Java 中用于随机打乱列表顺序的方法,基于 Fisher-Yates 算法实现,常用于洗牌、抽奖等场景。可选 `Random` 参数支持固定种子以实现可重复的随机顺序。方法直接修改原列表,无返回值。
125 0
|
3月前
|
Java 程序员 项目管理
Java 程序员不容错过的 Git Flow 全套学习资料及应用方法详解 Git Flow
本文详细介绍了Git Flow技术方案及其在Java项目中的应用实例,涵盖分支管理、版本发布与紧急修复流程,帮助开发者掌握高效的代码管理方法,提升团队协作效率。附示例操作及代码下载链接。
90 0