终极解释: java方法传递参数的方式

时间:2022-10-31 21:29:16
如果你还对此问题不清楚,或者似懂非懂有些疑惑,请看下文,看完此文,保证不用再看其他文章。

首先,我们来看下现有网上大多数文章对此问题是如何解释的。

如果你已经搜索过这个问题了,那么你会很容易看到大批的答案都是“java参数的传递方式是值传递”,除此之外还会解释一通什么是引用传递。

那么,请思考一个问题,什么是值传递?什么是引用传递?

请看下面一段C语言:

#include <stdio.h> 

fun(int arg) {
arg = arg + 2;
}

funAnother(int & arg) {
*arg = *arg + 2;
}

main()
{
int a = 1;
printf(a);
fun(a);
printf(a);
funAnother(*a);
}

fun函数直接传递值,通常被人们称之为值传递;funAnother中传递的是地址,通常被人们称作地址传递,人们将此概念带到面向对象的语言中就叫做引用传递。

为了完全弄明白此文要阐述的问题,请你务必在此处停下来想清楚什么叫传递,程序语言中传递指的是什么?也许你从开始学习第一门语言就学着写函数或者方法并熟练的使用他们,你一开始没有思考函数和方法的调用是什么样的一个过程,后来你更加习惯于写函数和方法并且代码写的相当漂亮,你已经忘记了此时可能需要问这个问题,但是请你现在好好思考一下它,或者你还真不明白。

如果你没有思考上面的问题,或者说你没有思考清楚就看到了这里,说明两点:

1、
你觉得这个问题不值得考虑,你已经对它很清楚的知道。
2、
你没有耐心去思考这个问题,你急于想指导答案
3、
你觉得我在瞎掰,扯这么多干什么╮(╯▽╰)╭

如果不是1。
是2,我要告诉你,这个问题设计编译器的相关知识和汇编语言相关知识,篇幅所限,我不会在这里解释,还请自行Google - -。
是3,我现在就可以告诉你答案:java中参数传递方式既不能叫值传递,也不能叫引用传递。值传递和引用传递用在描述C++还差不多,描述java不贴切。

下面我们通过几个例子来解释下我为什么这么说。

String

我们先看下String这个特殊类作为参数的传递(这里用String 举例):

    private String testString(String test) {
System.out.println(test.hashCode());
test = new String("234");
return test;
}

String testStr = new String("123");
String testStr1 = "123";
String testStr2 = testStr;
String testStr3 = testStr1;
System.out.println(testStr.hashCode());
System.out.println(testStr1.hashCode());
System.out.println(testStr2.hashCode());
System.out.println(testStr3.hashCode());

System.out.println(testString(testStr).hashCode());
System.out.println(testStr.hashCode());

此处解释下,hashCode()在api中的解释大意是对该对象在内存地址中的直接或间接运算,地址相同该值一定相同,反之亦然。

在我电脑上打印结果是这样的:

1:48690
2:48690
3:48690
4:48690
inner:48690
5:49683
6:48690

你会看编号为5的结果不一样。请你思考片刻在继续看。

对于String,jvm对其处理是这样的:
new String(“123”) 和 “123”对于jvm来说是一样处理的,首先看到这个之后,jvm会在内存中初始化一个常量,这块内存中的值就是字符串’1’, ‘2’, ‘3’,它们三个紧邻,jvm通过其他方式确定它们三个一起构成了一个变量的值,类型为字符串。你也许已经想到了,那么初始化在内存的那块呢,对了,这就是地址的概念,其实初始化到那块是不确定的,并且初始化到内存的哪块对我们来说都是不用关心的,我们只需要关心它的开始地址是哪里就行了,这样即使你需要关心这个字符串所占的内存,也可以计算出来它所跨区域的所有内存地址。另外,java中你其实并不需要知道确切的地址是多少,jvm有意对此进行了屏蔽。

前面说到jvm会初始化一个常量的“123”在内存中,对于String这种类型,对它的特殊处理就在于以后你声明多少变量=”123”,它都不会在初始化一个“123”出来,jvm只会将第一次初始化的那个首地址赋值给你声明的变量,这就是为什么第1和2的打印结果相同的原因,至于第3为什么和第2相同、第4和第1相同,很好理解,不再多说。

testString方法中有一个打印编号为inner,结果和外面的编号1、2打印相同,但是这里有不同的含义:

首先在调用testString(testStr)的时候,jvm并不是直接把testStr的地址传递过来(此处需注意“testStr的值”和“testStr的地址”的区别,这两者只是为了表达问题的一种叙述方式,testStr的值指的是它所指内存中的保存的值;testStr的地址指的“本质上其实是testStr这个变量真是的值”这句话很容易误导人请了解编译原理相关知识,但是这句话与后文中的相关介绍很有关系如果可以还是请搞清楚,容易理解的说就是指变量所在的内存首地址。),而是将testStr的首地址赋值给了另一个变量test,所以此时test和testStr同时指向了同一块内存地址但是两者又是不同的。所以此处打印hashCode必然相同。

但是请注意接下来一句test = new String(“234”),然后return test,紧接着外面打印了它的hashCode,也就是第5编号的打印,你会发现此时的值不同了。这也很好理解,new之后jvm重新初始化了一个字符串“234”,在内存的另一块,那么地址就不一样了。

如果读者将方法里test = new String(“234”)改成 test = new String(“123”),试试结果又是什么?

对于Integer、Double、Float、Boolean、Short等这些类对象作为参数传递跟String是一样的,自己测试一下。

int

对于int、double、short、float、byte、long、char、boolean这类基本数据类型作为参数传递,其实只是传递值而已,对于它们而言,本身就不用关心地址的概念。

Object

对于对象,你可以理解它在内存中其实是多个像String对象一样的嵌套。

public class Person {
protected String name;

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

public String getName()
{
return this.name;
}
}

类就像一个盒子,盒子里还可以有盒子,这就是上面说的类的嵌套的含义。Person类中定义了String类型的name属性,类里面定义了类。

先不看Name属性,我们先看下面的代码:

    private Person test(Person person) {
System.out.println("inner:" + person.hashCode());
person = new Person();
return person;
}

Person person = new Person();
System.out.println("1:" + person.hashCode());
System.out.println("2:" + test(person).hashCode());

我这里运行结果如下:

1:662441761
inner:662441761
2:1618212626

没错,你会发现它和前面讲的String方式是一样的。

再来验证一下,我们通过改变Name的值来验证是否如我们预期的和String一样:

    private Person test(Person person) {
System.out.println("inner:" + person.getName());
System.out.println("inner':" + person.getName().hashCode());
person.setName("Adam");
re

Person person = new Person();
person.setName("John");
System.out.println("1:" + person.getName());
System.out.println("1':" + person.getName().hashCode());
System.out.println("2:" + test(person).getName());
System.out.println("2':" + test(person).getName().hashCode());
System.out.println("3:" + person.getName());
System.out.println("3':" + person.getName().hashCode());

运行结果:

1:John
1':2314539
inner:John
inner'
:2314539
2:Adam
inner:Adam
inner':2035631
2'
:2035631
3:Adam
3':2035631

请思考一下结果为什么是这样?

没错,当Person对象person作为参数传递给test方法的时候,jvm将person的地址拷贝了一份副本传了进去,所以前面没有考虑Name的代码中inner编号的hashcode是一样的,当在test内部调用setName的时候,改变的依然是原person对象所在内存中Name位置的值,所以里面更改,就改变了外面的对象的属性值。

如果你足够仔细,估计你已经想到了一件事情,根据前面的测试,我们还不能够说明一点“jvm将person的地址拷贝了一份副本传了进去”,目前还不能确定是拷贝的副本还是它自己。

下面继续验证,我们将test稍微修改一下:

    private Person test(Person person) {
System.out.println("inner:" + person.getName());
System.out.println("inner':" + person.getName().hashCode());
person = new Person();
person.setName("Adam");
return person;
}

再次运行这段代码:

    Person person = new Person();
person.setName("John");
System.out.println("1:" + person.getName());
System.out.println("1':" + person.getName().hashCode());
System.out.println("2:" + test(person).getName());
System.out.println("2':" + test(person).getName().hashCode());
System.out.println("3:" + person.getName());
System.out.println("3':" + person.getName().hashCode());

结果如下:

1:John
1':2314539
inner:John
inner'
:2314539
2:Adam
inner:John
inner':2314539
2'
:2035631
3:John
3':2314539

请看,此时的编号3和编号1的结果是一样的,这就证明了传进来的是对外面person对象首地址的一个拷贝,而不是它自己。

在编程中使用最广泛的java集合类、JsonObject、jsonArray等这些类作为参数传递使用,要深刻理解java方法传递参数方式,以防使用错误导致数据不一致。

再回过头来请思考一下开头说的,java方法参数传递方式是值传递还是引用传递?我觉得理解了本质就不用去回答到底是值传递还是引用传递的问题了,如果面试中被问到这样的问题,请反问面试官“请解释值传递和引用传递的概念,好让我回答这个问题”。