你们不要再吵了!|你们不要再吵了! Java只有值传递..

写在前边

  • 上次聊到Java8新特性 lambda时,有小伙伴在评论区提及到了lambda对于局部变量的引用,补充着博客的时候,知识点一发散就有了这篇对于值传递还是引用传递的思考。关于这个问题为何会有如此多的误区,这篇就来破解ta!
果然知识网的发散是无止境的!
知识储备--堆和栈
  • 堆是指动态分配内存的一块区域,一般由程序员手动分配,比如 Java 中的 new、c里边的malloc。
  • 栈是编译器帮我们分配好的区域,一般用于存放函数的参数值,局部变量等
有关堆栈的相关知识在 迷途指针 中有所提及。
数据类型
Java中除了基本数据类型,其他的均是引用类型,包括类、数组等等。
基本数据类型和引用类型的区别
先看一下这两个变量的区别
void test1(){ int cnt = 0; String str = new String("melo"); }

你们不要再吵了!|你们不要再吵了! Java只有值传递..
文章图片

  1. cnt是基本类型,值就直接保存在变量中(存放在栈上)
  2. 而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。
比如我们创建了一个 Student student = new Student("Melo");
  • 在堆中开辟一块内存(真正的对象存放在堆上),其中保存了name等数据 , 而student只是保存了该对象的地址(存放在栈上)
当我们修改变量时
void test1(){ int cnt = 0; cnt=1; String str = new String("melo"); str="Melo"; }

你们不要再吵了!|你们不要再吵了! Java只有值传递..
文章图片

对于基本类型 cnt,赋值运算符会直接改变变量的值,原来的值直接被覆盖掉了。
ta无依无靠,不像下边一样有房子可以住。
对于引用类型 str,赋值运算符只会改变引用中所保存的地址,虽然原来的地址被覆盖掉了,str指向了一个新的对象,但是原来的那个老对象没有发生变化,他还是老老实实待在原来的地方!!!
有学过c语言的同学应该很清楚,这里借助c语言中的“指针”打个比喻。
  • 引用类型str就相当于一个指针(旗子),插在了一个房子门口。现在给这个旗子挪个位置,只是让这个旗子放置在了另一个新的房子,原本的老房子还在那里,不会说因为你改变了旗子的位置,房子就塌了。
当然,原来那个房子没有旗子插着了,没有人住了。也不能总是放任ta在那占着空间,过段时间也许就会有人来把他给拆了回收了(JVM)。
这种没有地方引用到的对象就称为垃圾对象。
值传递
我们上次聊到lambda的时候,提及到了值传递,那里的拷贝副本,就是我们这里要说的值传递
  • 如果我们这里的方法块访问了外部的变量,而这个变量只是一个普通数据类型的话,相当于只是访问到了一份副本。当外部对这个变量进行修改时,lambda内部(只有副本)是无法感知到这个变量的修改的。
我们只是将实参传递给了方法的形参,将cnt值复制一份,赋值给形参val所以,函数内对形参的操作完全不会影响到实参真正存活的区域!而伴随着函数调用的结束,形参区域和其内的局部变量也会被释放。(方法栈的回收)
//基本类型的值传递 void unChange(int val) { val = 100; } unChange(cnt); // cnt 并没有被改变

引用传递 实参传递给形参时,形参其实用的就是实参本身(而不再单纯只是拷贝一份副本出来了),当该形参变量被修改时,实参变量也会同步修改。
Java中到底是引用传递还是值传递呢 内卷实例
//内卷 void involution(Student temp){ temp.setScore(100); }public static void main(String[] args) { Student student = new Student(); student.setName("Melo"); student.setScore(0); System.out.println("躺平时的成绩->"+student.getScore()); new TestQuote().involution(student); System.out.println("卷了几天后的成绩->"+student.getScore()); }

你们不要再吵了!|你们不要再吵了! Java只有值传递..
文章图片

  • 这里看起来,好像符合我们引用传递的定义诶?
    • 对形参temp的修改,会反馈到外部实参student那里去?看起来操作的是同一个变量的样子?
反内卷实例
看下边这段"反内卷"的代码实例
//反内卷 void againInvolution(Student temp){ temp = new Student(); temp.setScore(100); }public static void main(String[] args) { Student student = new Student(); student.setName("Melo"); student.setScore(0); System.out.println("企图内卷前的成绩->"+student.getScore()); new TestQuote().againInvolution(student); System.out.println("遭受反内卷后的成绩->"+student.getScore()); }

你们不要再吵了!|你们不要再吵了! Java只有值传递..
文章图片

  • 细心的同学可能发现了,我们这里多了一步操作 --> temp = new Student();
?
先给出答案吧,Java里边其实只有值传递!!!
  • 为什么这么说?
    其实我们这里的形参temp,只是拷贝了一份student的地址。可以理解为temp拷贝了这条指针,他也指向了student所指向的对象。
  • 也就是说,temp只是跟student同样指向了一个同一个对象而已,在第一个例子中,我们没有去重新修改temp的指向,所以会造成一种假象:我们对temp的修改似乎等价于对student的修改? 其实只是刚好两个指向了同一个对象而已!!
你们不要再吵了!|你们不要再吵了! Java只有值传递..
文章图片

  • 而如果我们对temp重新赋值了呢, temp = new Student();
你们不要再吵了!|你们不要再吵了! Java只有值传递..
文章图片

  • 对temp重新赋值后,此时temp就指向了另一个区域了,后续再对temp修改,根本不会影响原来的student指向的区域
    所以才会"反内卷"失败,跳出函数的时候,student所指向的对象成绩根本没有增长!!!
    ?
为什么会有误区呢?
  • 其实还是因为Java中数据类型的问题,基本数据类型看起来就像是值传递,而引用传递因为存放了地址,让我们能够访问到实参所指向的对象,容易让我们误以为我们的形参其实就等价于实参.
其他语言的引用 JS只有值传递,类似Java 指针传递(C语言)
注意指针传递跟引用传递是不一样的
  • 拿最老套的C语言手写swap来讲
#include void swap(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; } int main() { int a = 5; int b = 8; //需要传递地址 swap(&a, &b); printf("a = %d\n", a); printf("b = %d", b); }

引用传递(C++)
#include using namespace std; int main() { //&标识符 void swap(int& x,int& y); int a = 5; int b = 8; swap(a,b); return 0; }void swap(int& a,int& b){ int temp; temp = a; a = b; b = temp; }

总结 【你们不要再吵了!|你们不要再吵了! Java只有值传递..】如果该语言没有&,@这种取地址的操作符,一般来说就只有值传递的。如js和java,
  • 而c,Pascal,go这些是可以传引用和传值的。
最后
  • 其实关于Java到底是引用传递还是值传递这个问题。我们只需要理解好本质就好了,通过上边的那两幅图,理解好本质才是关键,万变不离其宗。

    推荐阅读