Java 值传递 & 引用传递 引发的思考

本文深入探讨Java中值传递与引用传递的概念,通过实验对比基本数据类型与引用类型在方法调用中的行为差异,揭示Java实际上采用的是值传递机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

最近(其实已经好几天前了)看到一个“Java到底是值传递还是引用传递?”的问题。感觉挺有思考价值,在一顿基础知识狂补之后,终于是盘明白了。特此总结。

前面讲 Java基础 - Java变量总结 的时候,没有涉及到一种分类,基本类型引用类型;这里顺便也补充一下。

那么直接进入问题:Java到底是值传递还是引用传递?

前戏稍微有点长…急性子的可以直接跳到 值传递引用传递 部分。

有些坑你不注意,永远不知道何时会掉下去 : )

值传递 & 引用传递

上一篇文章说到,搞清楚概念很重要!

那么,这次我们探究的值传递引用传递的概念是什么呢?

随处可见的定义,如下:

值传递pass by value)是指在调用方法时将实参复制一份传递到方法中,这样当方法对形参进行修改时不会影响到实参 1

//举个C语言的例子
void add(int x,int b){
	return a+b;
}

引用传递pass by reference)是指在调用方法时将实参的地址直接传递到方法中,那么在方法中对形参所进行的修改,将影响到实参 1

//这里同样举个C的例子
void change(char* str){
    *str = "Change Success.";
}

实参&形参

关于形参实参的概念,这里先简单介绍一下:

函数的参数即为形参,例如add( )方法中的abchange( )方法中的str
而我们调用时传入的参数即为实参

有机会我会把它放在递归里面讲(毕竟真正理解递归需要先理解函数调用流程)

那么Java中的变量传递呢?嘿,这得…

从变量类型说起

话说计算机,从电子管代表实现01逻辑开始,逐渐演化为…
不好意思,扯远了。我们分而治之。

基本类型&引用类型

Java中,数据类型被分为基本类型引用类型

基本类型:编程语言中内置的最小粒度的数据类型;Java基本类型包括四大类八种类型:

  • 整数类型
    • byte
    • short
    • int
    • long
  • 浮点数类型
    • float
    • double
  • 字符类型
    • char
  • 布尔类型
    • boolean

引用类型:引用也叫句柄,是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式。它主要包括 2

  • 接口
  • 数组
  • 字符串

ps:注意不要把这种分类和 Java基础 - Java变量总结 的划分混为一谈,划分标准不同!

划分完了,要不存储了解下?

Java内存结构

我们知道JavaC不同,Java给我们屏蔽了指针的概念,其语言本身是不能操作内存的。那么Java是怎么操作内存的呢?在回答这个问题之前,我们先看一下,Java程序内存结构。Java程序启动后,会初始化这些内存的数据 3

在这里插入图片描述
这里插播一下:关于内存结构内存模型的纠纷…这里就不展开了。

可自行点击本文参考 (。・∀・)ノ 4

简单来说,内存模型解决的是以下问题 5

  • Java 虚拟机规范定义了 Java 内存模型来屏蔽掉各种硬件和操作系统的内存差异,达到跨平台的内存访问效果。
  • 为了获得更好的执行性能,Java 内存模型没有限制执行引擎使用处理器的特定缓存器或缓存来和主内存 (可以和 RAM 类比,但是是虚拟机内存的一部分) 交互,工作内存(可类比高速缓存,也是虚拟机内存的一部分)为线程私有。
  • 工作内存和主内存的划分Java 堆,栈,方法区的划分不同,两者基本没有关系,如果勉强对应,则主内存可理解为堆中实例数据部分,工作内存则对应栈中部分区域。

此外,我们还可以查阅官方文档以论证其差别 17.4. Memory Model

所以我们经常讲的内存模型,准确而言应称为内存结构。 ( ̄_ ̄|||)

内存划分 2

根据上图Java内存结构,我们再次分而治之。

  • 程序计数器及其他隐含寄存器
    线程 私有。记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。


  • 是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有 线程共享的。

  • Java栈
    准确来说,应该叫虚拟机栈,栈中存放着栈帧

    栈是线程私有的,也就是线程之间的栈是隔离的;每个方法被执行的时候都会创建一个栈帧用于存储局部变量表操作数栈方法出口地址等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程 6

    下图表示了一个Java栈的模型以及栈帧的组成

在这里插入图片描述

  • 栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素
    每个栈帧中包括:
    • 局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。
    • 操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。
    • 指向运行时常量池的引用:存储程序执行时可能用到常量的引用
    • 方法返回地址:存储方法执行完成后的返回地址。
  • 方法区
    方法区是一块所有线程共享内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。

    方法区可存储的内容有:

    • 类的全路径名
    • 类的直接超类的权全限定名
    • 类的访问修饰符
    • 类的类型(类或接口)
    • 类的直接接口全限定名的有序列表
    • 常量池(字段,方法信息,静态变量,类型引用(class))等。
  • 本地方法区
    本地方法栈的功能和虚拟机栈是基本一致的,并且也是线程私有的,它们的区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。

存储地址

扯完了内存结构,那我们上面提及的那些变量又是存到哪的呢?

我们分为以下几种讨论:

  • 基本数据类型的存储

    • 局部变量
      基本数据类型的局部变量存储到的是虚拟机栈的栈帧中,数据本身的值就是存储在栈空间里面。
    public method(){
    	int x = 502;
    	int y = 502;
    	int z = 520
    }
    

    其实int x = 502;在解释时会被拆为两条指令:

    int x;
    x = 502;
    

    首先JVM会创建名为x的变量,存在于局部变量表中(int x;),然后在中查找,是否有字面值为“502”的内容,如果有则将该x指向这个地址;如果没有,则JVM将开辟一块空间,用于存储“502”,并将x指向该地址(x = 502)

    因此,我们声明初始化基本数据类型的局部变量时,变量名及其字面值都是保持在中。

    那我们再来看看y变量的执行过程:
    JVM会先创建了一个变量y并在栈中找到了“502”的内容,这时y直接指向该内容。

    因而,栈中的数据在当前线程下是共享的。

在这里插入图片描述
本例中,xy共享字面值“502”,如果如果我们修改y的值会发生什么呢?

  y = 222;

JVM会去中寻找字面量为“222”的内容,发现没有,就会开辟一块内存空间存储这个内容,并且把y指向这个地址。

由此可见:基本数据类型的数据本身是不会改变的,当局部变量重新赋值时,并不是在内存中改变局部变量所指向的字面量内容,而是重新在栈中寻找已存在的相同的数据,若栈中不存在,则重新开辟内存存新数据,并且把要重新赋值的局部变量的引用指向新数据所在地址 2

  • 成员变量
    假设我们创建了以下这么一个类:
    //People
    public class People{
    	String name;
    	int age;
    	//...
    	
    } 
    
    当我们调用以下代码时
    People pp = new People();
    
    其变量存储如下:

在这里插入图片描述
这里我们需要记住的是:基本数据类型的成员变量名都存储于中,其生命周期和对象的是一致的。

  • 类变量
    类变量即类内静态变量,它储存在方法区常量池,类装载时就分配存储空间,随类消失而消失。注意,这个区域是线程共享的。

    public class Timer{
    	int static num = 0;
    	int static status = TIMER_STOP;
    	//...
    }
    

    上述变量的存储应为:
    在这里插入图片描述

  • 引用数据类型的存储
    我算想明白了,其实它存储在哪里,和它是不是引用数据类型没有关系,而是看它属于局部变量还是实例变量还是类变量!那它为啥叫引用类型啊?

    那是因为它…字面值存储的是对象的地址,并不是对象的实际内容。那么实际的内容存储在哪里?目前查阅的资料显示是在上。

    总结一下就是:

    • 如果这个引用变量是一个局部变量,那么变量名存储在栈中,而其指向的地址是在堆中的。
    • 如果这个引用变量是一个实例变量,那么变量名和其指向的地址是都是在堆中。
    • 如果这个引用变量是一个类变量,那么变量名是存储在方法区,而其指向的地址是在堆中。

值传递?引用传递?

结束了漫长的前戏,终于迎来了…正经的实验阶段了。

看完前面扯的这些,你肯定会说,哎呀,基本数据类型就是值传递嘛,引用数据类型就是引用传递嘛。别着急下结论嘛,长夜漫漫,我们坐下来喝杯茶,逐个论证。

第一回合:基本数据类型是值传递?

Demo

俺说了也不算,那写个小demo吧。

    private static void setage(int age) {
        System.out.println("setage()传入的年龄为:" + age);
        age = 99;
        System.out.println("setage()修改后的年龄为:" + age);
    }

    // TEST
    public static void main(final String[] args) {
    
        int Age = 22;

        setage(Age);

        System.out.println("调用setage()后的年龄:" + Age);
    }

结局

在这里插入图片描述

剖析

main方法定义了一个基本数据类型的变量Age并赋予初值22,随后调用setage()方法,这时setage()方法压栈,实参赋值给形参int age = Age;

这时处于栈顶的是setage(),它将执行一系列操作:

先将age值打印出来,22,后生仔哦。

然后将年龄调整为老爷爷级别:age = 99;

然后再打印一次年龄:99,真的变老爷爷了。

这时函数执行完了,setage()栈帧出栈。于是那个老爷爷age也消失了…

剩下的是main()中那个后生仔Age,不过南柯一梦,它起身笑笑,我还年轻!

结论

基本数据类型是值传递。(✔)

第二回合:引用数据类型是引用传递?

Demo

这里先定义一个People类

public class People {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void supper(People people) {
        System.out.println("supper() 初始名 " + people.getName());
        people.setName("Root");
        System.out.println("supper() 改名后 " + people.getName());
    }

}

测试如下:

        People people = new People();
        people.setName("Tourists");
        people.supper(people);
        System.out.println("main() Name: " + people.getName());

结局

在这里插入图片描述

结论

那么从效果上看,引用数据类型引用传递咯。

这里俺不同意了,因为俺手里还有另一个Demo,为此要求加时赛,反正真金不怕火炼,来吧。

加时赛:引用数据类型是引用传递?

Demo

    private static void setname(String name) {

        System.out.println("setname()传入的名字为:" + name);
        name = "Tourists";
        System.out.println("setname()修改后的名字为:" + name);
    }

    // TEST
    public static void main(final String[] args) {

        String Name = "Root";

        setname(Name);

        System.out.println("调用setname()后的名字:" + Name);
    }

结局

在这里插入图片描述
哎呀,咋又像值传递了呢,这 咋整 啊?

加时赛剖析

是不是有点出乎意料?下面分析一下吧。

首先main()执行:String Name = "Root";

在这里插入图片描述
接着调用setname(Name);
这时setname()入栈,实参赋值给形参:

在这里插入图片描述
然后就被打印:传入的名字为:Root

接下来执行,name = "Tourists";由于堆内存中没有字面值“Tourists”的内容,因而JVM会创建,并让setname()name指向它。

在这里插入图片描述
然后打印,修改后的名字为:Tourists

嗯,没毛病!接着,setname()出栈

在这里插入图片描述
回到main()函数后,输出 调用setname()后的名字:Root

哎呀,那上面那个第二回合又是咋回事??

第二回合剖析

还是从main()中开始,创建了一个People实例people
在这里插入图片描述
接着调用people.setName("Tourists");

这时,setName()方法入栈,实参赋值给形参数,由于未找到面值为“Tourists”的内容,于是JVM将创建它,并将name指向该值。随后执行this.name = name;于是就发生了下图所示变化。

在这里插入图片描述
再接着,setName()方法出栈main()方法继续往下执行,执行到了people.supper(people);

supper()方法入栈,实参赋值给形参。
在这里插入图片描述
输出初始名,Tourists 没问题,Over~

随后是 people.setName("Root");
这个执行过程前面分析过了,执行后效果如下:

在这里插入图片描述
这时再输出改名后的值,输出改名后:Root 也没问题,Over~

再接下来,这个函数就执行完了,supper()方法出栈main()回到栈顶

在这里插入图片描述
这时我们再输出peoplename属性,你说它是什么来着?

毫无意外,Root。Over~

结语

真相只有一个
真相只有一个!

Java其实还是值传递的,只不过对于引用对象,值的内容是对象的引用4

如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。

如果是对引用类型的数据进行操作,分两种情况:

  • 一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。
  • 一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容 2

参考鸣谢


  1. 沉默王二 - 188W+程序员关注过的问题:Java到底是值传递还是引用传递? ↩︎ ↩︎

  2. 假不理 - 这一次,彻底解决Java的值传递和引用传递 ↩︎ ↩︎ ↩︎ ↩︎

  3. 什么是Java内存模型 ↩︎

  4. 为什么说Java中只有值传递 ↩︎ ↩︎

  5. 《深入理解 JAVA 虚拟机》 ↩︎

  6. 深入理解JVM-内存模型(jmm)和GC ↩︎

<think>好的,我现在需要回答用户关于Java表达式与C++表达式差异的问题。首先,我得回忆一下这两种语言的基本语法和表达式结构,确保自己理解正确。然后,我应该从几个关键方面进行比较,比如语法结构、运算符重载、类型转换、作用域规则、内存管理、异常处理等。可能还需要考虑它们的面向对象特性在表达式中的体现,比如方法调用和函数指针的不同。 首先,用户的问题是比较Java表达式和C++表达式的差异。我需要明确“表达式”在编程中的定义,通常指的是由变量、常量、运算符等组成的代码片段,用于计算一个值。比如赋值表达式、算术表达式、逻辑表达式等。 接下来,我需要分点列出两者的不同之处。例如,C++支持运算符重载,而Java不支持,这会影响表达式的写法。另外,类型转换方面,C++有显式和隐式转换,而Java的自动装箱和拆箱可能带来不同的行为。作用域方面,C++允许更灵活的作用域操作,比如使用::访问全局变量,而Java没有这样的运算符。 内存管理也是一个重要点,C++中可以使用指针和引用,而Java引用更多是对象引用,没有指针算术。异常处理方面,Java的异常检查机制可能影响表达式中的异常抛出和处理方式。此外,布尔表达式在条件判断中的处理,比如C++允许整数作为条件,而Java必须使用布尔类型。 我需要确保每个差异点都有具体的例子说明,这样用户更容易理解。例如,在运算符重载部分,可以给出C++中重载+运算符的例子,并对比Java中无法这样做的情况。在类型转换时,可以比较Java的强制转换和C++的static_cast等不同方法。 同时,要注意用户提到的引用标记,虽然用户提供的引用内容似乎与问题关联不大,但可能需要适当引用示例中的格式。例如,在讨论语法差异时,引用用户提供的示例结构,但实际用户给出的引用是关于快速排序和知识产权的,可能与当前问题无关,可能需要忽略或仅按格式处理。 最后,生成相关问题,引导用户深入探讨,比如性能差异、应用场景、学习曲线等。这些问题需要与主题紧密相关,帮助用户进一步探索。 现在,我需要将这些思考整理成结构清晰的回答,确保每个差异点明确,举例恰当,并符合用户要求的格式,如使用正确的LaTeX数学表达式格式,分点说明等。同时,检查是否有遗漏的重要差异点,比如Lambda表达式、函数式接口在Java中的处理,或者C++中的模板元编程对表达式的影响,不过这可能更偏向于高级特性,可能超出基础表达式比较的范围,需要视情况简要提及。 总结下来,回答的结构可能分为:语法差异、运算符重载、类型转换、作用域、内存管理、布尔表达式处理、异常处理等部分,每部分简要说明并举例,最后生成相关问题。</think>### Java表达式与C++表达式的差异比较 Java和C++在表达式设计上存在显著差异,主要源于语言特性、内存管理和类型系统的不同。以下是关键差异点的详细分析: --- #### 1. **运算符重载** - **C++**: 允许自定义运算符重载,例如为类定义`+`或`==`的行为: ```cpp class Vector { public: Vector operator+(const Vector& other) { /* 实现向量加法 */ } }; ``` 表达式`v1 + v2`可直接用于自定义类型。 - **Java**: 不支持运算符重载,所有运算符仅能用于基本数据类型(如`int`, `double`),对象操作需通过方法调用,例如字符串拼接用`str1.concat(str2)`而非`+`[^1]。 --- #### 2. **类型转换** - **C++**: 支持显式转换(如`static_cast<int>(x)`)和隐式转换(如`double x = 5`自动转为`5.0`)。用户可自定义转换构造函数: ```cpp class MyClass { public: operator int() { return value; } // 隐式转换为int }; ``` - **Java**: 仅支持显式强制转换,且仅限兼容类型(如子类转父类)。基本类型需手动转换(如`(int) 3.14`),自动装箱/拆箱可能引发`NullPointerException`: ```java Integer a = null; int b = a; // 运行时错误 ``` --- #### 3. **作用域与访问控制** - **C++**: 支持全局变量和`::`作用域解析符,表达式可访问全局或局部变量: ```cpp int global = 10; void func() { int local = global; // 直接访问全局变量 } ``` - **Java**: 无全局变量,静态成员通过类名访问(如`MyClass.staticVar`)。作用域严格依赖代码块,无法跨类直接访问变量。 --- #### 4. **内存管理** - **C++**: 表达式可直接操作指针和引用,支持指针算术: ```cpp int arr[5] = {1, 2, 3}; int* ptr = arr; int x = *(ptr + 2); // x = 3 ``` - **Java**: 对象通过引用访问,无指针概念。表达式中的对象赋值实为引用传递: ```java List<Integer> list1 = new ArrayList<>(); List<Integer> list2 = list1; // list2与list1指向同一对象 ``` --- #### 5. **布尔表达式** - **C++**: 允许非布尔类型(如`int`)作为条件,`0`为`false`,非零为`true`: ```cpp if (5) { /* 执行 */ } // 合法 ``` - **Java**: 条件表达式必须为布尔类型,否则编译错误: ```java if (5) { } // 错误:int无法转换为boolean ``` --- #### 6. **异常处理** - **C++**: 表达式可能抛出任意类型异常(包括基本类型),但无强制检查: ```cpp int divide(int a, int b) { if (b == 0) throw "Division by zero"; return a / b; } ``` - **Java**: 受检异常(checked exceptions)需在方法签名声明或捕获: ```java void readFile() throws IOException { // 可能抛出IOException的表达式 } ``` --- ###
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值