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

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

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

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

面试题

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




















@Testpublic 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=Stevenafter change : arr=[公众号, 程序, 新视界]after change : name=二师兄

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

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

值传递与引用传递

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

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

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

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

基础类型和String的传递过程

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

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

先来看一下基础类型和String类型通常在内存中存在的形式。更多展现形式我们在本系列文章的前几篇文章中已经多次提到。image.png总结一下就是方法内基础类型的引用和值分配在栈中,String类型引用在栈中,值在堆(堆或字符串常量池)中。

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










@Testpublic 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对象的内存结构变化。










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

起初时内存结构与第一张图一样,name在栈中存储,值Tom在字符串常量池中存储。当调用change方法时,内存结构如下:image.png在调用方法时,change方法的形参str复制了一份变量name存储的引用地址,同样指向字符串“Tom”。

当执行到方法内的str赋值时,内存结构如下: image.png此时可以看到方法内执行的赋值操作只是改变了str变量中存储的字符串的地址,并未影响到原有的name变量所指向的字符串。

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

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

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

引用类型的值传递

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
























@Testpublic 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;  }}

在执行change方法之前,内存结构图如下:image.png

当执行change方法中的set方法之后,内存结构变为如下:

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

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

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












@Testpublic 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的内存结构之后,很多令人疑惑的问题便迎刃而解了?所以,赶紧深入学习一下吧。

目录
相关文章
|
1月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
1月前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
74 9
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
26天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
66 14
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
29天前
|
安全 Java 开发者
Java中WAIT和NOTIFY方法必须在同步块中调用的原因
在Java多线程编程中,`wait()`和`notify()`方法是实现线程间协作的关键。这两个方法必须在同步块或同步方法中调用,这一要求背后有着深刻的原因。本文将深入探讨为什么`wait()`和`notify()`方法必须在同步块中调用,以及这一机制如何确保线程安全和避免死锁。
41 4
|
29天前
|
Java
深入探讨Java中的中断机制:INTERRUPTED和ISINTERRUPTED方法详解
在Java多线程编程中,中断机制是协调线程行为的重要手段。了解和正确使用中断机制对于编写高效、可靠的并发程序至关重要。本文将深入探讨Java中的`Thread.interrupted()`和`Thread.isInterrupted()`方法的区别及其应用场景。
30 4
|
1月前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
35 6

热门文章

最新文章