Java 参数传递问题详解

昨天实验室同门问了个关于Java参数传递的问题,虽然很基础。但是却挺有意思的,也时初学者可能比较疑惑的地方,今天就花点时间总结一下。如有错误还请批评指正。


问题引入

就是这样一个简单的问题,我在类中定义了一个全局的String strchar数组chars,并且在类的实例化中调用了**exchange()**方法,最后对原始str和chars的内容进行了输出。这时候读者不妨停下简单思考下此刻的输出应该时是什么呢?

import java.util.Arrays;

public class ParameterTransferTest {

    public static void main(String[] args) {
        ParameterTransfer p1 = new ParameterTransfer();
        p1.exchange(p1.str, p1.chars);
        System.out.println("str="+p1.str+",chars="+ Arrays.toString(p1.chars));
    }
}
class ParameterTransfer {
    public String str = "good";
    public char[] chars = {'a','b','c'};
    public void exchange(String str,char[] chars){
        str = "hello";
        chars[0] = 'd';
    }
}

也许你想说,str被更改成了"hello",chars[0]改成了‘d’,输出不就是:

str=hello,chars=[d,b,c]

其实不然,实际的输出是:

str=good,chars=[d,b,c]

也就是说,String的str并没有被改变chars数组被改变了,为什么会这样子呢?这就不得不说到Java的参数传递方式了。


Java参数传递

1.基本数据类型作为参数传递时,是传递值的拷贝
2.对象作为参数传递时,将对象在内存中的地址拷贝了一份传递给了参数。

我们分析一下前面代码的输出的原因。咦,不对啊,String和char数组不都是引用类型么,同样作为对象参数传递时,为什么一个可以更改一个不能更改呢?

这时候我们就不得不提到Java中String类型的特殊性了。

Java中String类型修改分析

在Java中,String类型的是依靠数组来实现的,而由于数组本身属于定长的数据类型。这就表明了其实String类型的对象,一旦生成就不可更改!

这时候,你可能就会很疑惑了,String声明了就不可更改?那我那些字符串的题目对传入的字符串str都是可以更改的啊?

举个栗子:

String str1 = "hello";
str1 = str1+"world";
System.out.println(str1);

输出结果:

helloworld

你看原始的str内容是hello,经过修改之后不就是变成了helloworld吗?这里其实Java中字符串对象的内容的修改都是通过引用地址的变化而实现的,而不是修改原始对象的内容

String类内存模型分析

我们知道,整个jvm的内存模型如下:

Java 参数传递问题详解_第1张图片

其中本地方法栈时运行c++native方法的栈区;程序计数器是指向程序当前运行的位置(以上两部分是java程序员比较少关心的两个区域);方法区是存储一些元数据信息,在jdk7之前又被叫做永久代,jdk8之后被叫做元数据空间,主要是存储一些静态的static方法、变量或者是类加载器classloader这样一些全局的变量;栈区是用来存储函数运行的一些临时的变量(本地方法栈、程序计数器和栈线程私有的,线程私有的意思就是每个线程在开辟和运行的过程中都会单独的创建这样一份内存);堆区主要是用来存储对象(堆区和方法区是全局共享的)

下面以上面的栗子代码,分析一下Jvm内存中String类型改变的过程,示例图如下:

Java 参数传递问题详解_第2张图片

  1. String str1 = “hello”;这句执行时会首先在栈内存中开辟一个叫str1的引用类型,str1是个指针,或者说str1是地址都是一样的意思,其本质是一个占用4字节int数地址

  2. 然后执行到后面的"hello"时,由于其没有采用new关键字开辟内存(这里之后会出专门的blog说直接用字符串进行String初始化和new关键字进行初始化的区别),其会首先到堆上的字符串常量池寻找是否有"hello"这个常量,没有的话就在堆上新开辟个空间并且把堆上的地址传给栈中的str1引用;如果常量池中已经有了"hello",就把它的地址传给栈中的str1

那么当执行str1 = str1+"world"时的值到底是怎么样改变的呢?示意图如下:
Java 参数传递问题详解_第3张图片

  1. 首先会判断常量池中有无“world”常量,没有的话会在堆上的常量池内创建“world”
  2. 之后,将原始的”hello“和"world"执行拼接操作,生成一个新的对象"helloworld"并将栈内的str1指向新的对象(将对象的地址传递给栈内的str)

综上所述,原始被创建的字符串"hello“在内存中全程没有被更改!所谓的改变其实是通过创建新的字符串并且改变了栈内的str1指向的引用地址来实现的。这也是所谓的String类型一旦被创建,就无法再进行修改。当更改str1指向别的对象时,如果没有其他再指向”hello“(其就会成为匿名对象),一段时间后该对象就会被jvm的GC自动回收。

原问题分析

知道了字符串类型的修改方式之后,我们再来分析下原问题:

  1. 首先因为,String和char[]都是对象引用类型的传递。所以再调用exchange()时,会分别把str的地址”good“字符串在堆内对应的地址)给了形参str,chars的堆内地址给了形参chars
  2. 然后修改了形参str的字符串内容,实际上就相当于让形参str从原始指向和类属性str一样的”good“位置,指向了新的位置”hello“位置但是类属性的str还是指向”good“,这也就是str没有被修改的原因,因为形参指向的”good“位置并不是被修改成了”hello”,而是其指向了一个新的地址
  3. 反观chars数组,当把类属性的chars地址给形参chars之后。由于形参chars只是指向的堆上类属性对象的地址,方法对chars的内容进行修改时,实际上是找到具体对象在堆上的地址,并对其进行修改(修改了原始地址内的对象内容)
  4. 这时候,由于类属性chars也还是指向该堆内地址,但是该地址内的内容已经在exchange()方法中修改了,因此输出chars的内容会显示修改。

问题进阶

如果原始问题修改为如下,输出又应该是什么结果呢?

import java.util.Arrays;

public class ParameterTransferTest {

    public static void main(String[] args) {
        ParameterTransfer p1 = new ParameterTransfer();
        p1.exchange();
        System.out.println("str="+p1.str+",chars="+ Arrays.toString(p1.chars));
    }
}
class ParameterTransfer {
    public String str = "good";
    public char[] chars = {'a','b','c'};
    public void exchange(){
        str = "hello";
        chars[0] = 'd';
    }
}

看似和之前没什么区别,只是exchange方法变成无输入参数的方法了,但时运行后的结果却是如下结果:

str=hello,chars=[d, b, c]

也就是原始定义的类属性str在执行过exchange方法后,确实是被改变了。这也就我同门疑惑的第二个点,他平时刷题也会使用到这种处理方法,定义一个全局类属性然后之后方法里也可以更改这个值。那这到底时什么原因呢?有什么区别呢?

分析程序运行时内存

下面来具体分析下原始问题和进阶问题在内存上究竟有什么区别:

初始问题时:

step1:首先当运行到新建ParameterTransfer对象p1时,会首先在栈上生成p1的引用实际指向堆内的对象地址,由于p1的两个属性也是对象引用类型,str和chars会分别在指向堆内的其他两个对象的地址,示意图如下:

Java 参数传递问题详解_第4张图片

step2:当执行到exchange方法时,会现在栈区内生成临时变量str和chars,其都为4byte的int地址数据,分别指向了堆内的字符串"good"和chars数组的位置(下图绿色箭头所指位置);

step3:当执行str = “hello"时有上分析可知,会先判断堆内常量池是否有"hello"的字符串,没有就新创建并将临时变量str指针指向该新位置(如图红色箭头所示),由于原始==p1的str属性还是指向"good”==所以并不会被更改;

step4:当执行到chars[0] = 'd’时,由于其不像String类型创建就无法修改,其是直接修改临时变量chars位置的原始值,将*‘a’替换为‘d’*,并且原始的类的属性chars指向的内存中相同的位置,所以输出p1.chars时会显示也被更改

step5:当exchange()方法执行完成后,临时变量str和chars都会被删除,若没有其他引用指向"hello",起就会成为匿名对象,一段时间后就被被GC清除

Java 参数传递问题详解_第5张图片

进阶问题时:

进阶问题时,主要的区别就是exchange()方法不再需要形参str和chars,没有形参传递的过程。那么实际在执行的过程中

str = "hello";
chars[0] = 'd';

都时更改的原始引用的位置,示意图如下:

Java 参数传递问题详解_第6张图片

此时在exchange()方法执行时,str和chars都是使用的全局的类属性,而不是经过参数传递的临时变量,因此更改会生效。

Tips

由于java参数传递的特性,原始数据类型如int,float等在参数传递时只是将原始数据的值拷贝给了形参,之后的一切修改其实和原始数据是没有任何关系的。那我们怎么样可以做到堆原始数据的更改呢?
比方说我们在进行dfs(深度优先遍历)时,可能需要带着一个int类型的长度参数,就可以将原始的int型长度传递到一个长度为1的int型数组中,再将数组传递进dfs参数中,这样这个int的数值就可以随着函数的调用而更改

你可能感兴趣的:(总结,Java,java,字符串,引用传递,string,值传递)