Java中只有值传递的理解

概述

不多BB,直接上题:

之前对这个知识点只是有一个经验性的认知,并没有深入的理解,碰到这个题之后我直接就给出了一个错误答案:map中的键值对是<KEY,TRY>,人家try模块下面都有return语句了啊,直接方法返回啊,正确答案是FINALLY

这个原理是和方法中的参数传递是一样,也就是不管在finally还是在方法参数中,只有值传递


方法的参数一共有两种,一个是基本数据类型变量,另外一个是引用变量,

那么为什么说只有值传递而没有引用传递呢?实际上Java中的引用就是一串长度固定的二进制数值,用来保存对象在内存中的地址,也是一个数值

要想比较好的理解,我们还是需要一些JVM知识,每个方法对应Java栈里面的一个栈帧,每个栈帧里面都有自己的局部变量表和操作数栈,而方法参数就是局部变量表中的一部分,当我们调用方法传递参数时,只是将当前的变量的副本传递进去,并没有将当前栈帧里面的局部变量本体传入方法中,当被调用方法获得了变量副本之后,就可以在自己的栈帧空间中进行操作,这其中还要分两种情况:

  • 如果参数为基本数据类型,那么直接将变量副本传递给被调用方法,这个方法在自己的栈帧空间中乐意怎么操作这个副本就怎么整,不影响变量实体,最后第二个输出语句仍然输出10

例如下面的代码:

public class Test
{
	public static void test(int i)
	{
		i = 1 + 2 -3 * 4;
	}
 
	public static void main(String[] args)
	{
		int i = 10;
		System.out.println(i);  //10
		test(i);
		System.out.println(string);  //10
	}
}
  • 如果参数为引用变量,这里可就有讲究了,首先在进行参数传递时还是将变量副本传递给被调用方法,但是这个副本就不是简单的字面量了,而是一个指向堆上面对象的地址,在被调用方法的栈帧中如果修改这个对象的数据,那么在原来的实体变量中也会被修改,因为对象在堆中,是共享的,这时两个引用变量全部指向同一个对象

例如下面的代码:

public class Test
{
	String a = "123";
	public static void test(Test test)
	{
		test.a = "abc";
	}
 
	public static void main(String[] args)
	{
		Test test1 = new Test();
		test1.a = "567";
		System.out.println(test1.a); //567
		
		test(test1);
		System.out.println(test1.a); //abc
		
	}
}

上面的代码在test方法中修改的test.a的实例变量,在main方法中也被修改了

但是还有一种情况,就是上面图片中的,将副本中的map设置为map = null,这样做并没有修改对象的数据,而是将引用自身的值修改了,不会影响到main方法中的值,只是在被调用方法的栈帧中,将自己的map给整没了


再来一个复杂一点的例子来加深理解:

import java.io.Console;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.*; 
 
class People
{   
	public int age;  
	public static void Swap(People a1, People a2)
	{     
		People tem;   
		tem = a1;    
		a1 = a2;     
		a2 = tem; 
	}  
	public static void Change(People a1, People a2)
	{      
		a1.age = 250;  
		a2.age = 250;   
	}
}
public class Empty
{ 
	public static void main(String[]args)    
	{        
		People a1 = new People();    
		People a2 = new People();  
		a1.age = 1;  
		a2.age = 2;  
		
		//这里没能交换,看起来是按值传递       
		People.Swap(a1, a2);      
		System.out.printf("%d \t %d", a1.age, a2.age);  //1,2           
		System.out.println();  
		
		//这里修改了变量,看起来是按引用传递        People.Change(a1, a2);    
		System.out.printf("%d \t %d", a1.age, a2.age); //250,250
	}
}

在被调用方法Swap的栈帧中,拥有自己的a1,a2变量副本,即使进行了交换,那么也是将自己空间内的引用交换了一个位置,并不影响main方法中的,也没有对对象进行修改,而Change方法就不一样了,给了他一个引用变量副本,他就开始把对象糟蹋了,最终还会反应到main方法中,因为他们指的都是同一对象

这里根据自己的经验做一个总结,如果方法参数为引用变量,那么变量副本进行=操作是没有什么问题的,要是执行 . 操作,那么就会修改对象的值,反应到main方法中


**上面已经对值传递有了一个比较清除的理解,因为finally语句中和方法的参数是一样的,**至于为什么,在周志明大神的《深入理解JVM》的187页,对try...catch...finally语句的做了一个字节码层面的剖析,最终的结论就是人家JVM字节码执行引擎就是这样做的,莫得办法

想要比较好的理解finallt语句中情况,就要知道一个点:

return语句是分为两个阶段执行的,

  1. 执行return语句后面的运算逻辑,将值保存起来,至于JVM是如何做的,我们插播一段原文:

我们一起来分析一下字节码的执行过程,从字节码的层面上看看为何会有这样的返回结果。字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶,作为方法返回值使用。 为了讲解方便,笔者给这个Slot起了个名字:returnValue)。如果这时没有出现异常,则会继续走到第5~9行,将变量x赋值为3,然后将之前保存在returnValue中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。 如果出现了异常,PC寄存器指针转到第10行,第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再将变量x的值改为3。 方法返回前同样将returnValue中保留的整数2读到了操作栈顶。 从第21行开始的代码,作用是变量x的值赋为3,并将栈顶的异常抛出,方法结束。

  1. 执行finally语句的逻辑,和被调用方法一样,但是finally语句中要是有return就直接返回,不使用try中的return了
  2. 如果finally中没有return,那么将之前计算好保存起来的值进行return

下面在finally语句 的角度举几个例子:

public static int func() {
    int result = 0;
    try {
        result = 1;
        return result;
    } catch (Exception e) {
        result = 2;
        return result;
    } finally {
        result = 3;
    }
}

正确的返回结果是,func返回1。

1)如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。

2)如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。

    public static Person funcPerson() {
        Person result = new Person(20);
        try {
            result.age = 30;
            return result;
        } catch (Exception e) {
            result.age = 40;
            return result;
        } finally {
            result.age = 50;
        }
    }

    static class Person {
        public int age;
        public Person(int age) {
            this.age = age;
        }
    }

该函数的返回类型是resultPerson,age为50,即在finally中更改了有效。

如果没有异常出现,而且finally语句中没有return,则会执行try里边的return,并且,会将变量暂存起来(对象存的是引用的地址),再去执行finally中的语句,这时候,如果返回值是基本数据类型或者字符串,则finally相当于更改副本,不会对暂存值有影响;但是,如果返回值是对象,则finally中的语句,仍会根据地址的副本,改变原对象的值。所以上边的例子,返回值的age为50。

public class TryTest{
	public static void main(String[] args){
		System.out.println(test());
	}
 
	private static int test(){
		int num = 10;
		try{
			System.out.println("try");
			return num += 80;
		}catch(Exception e){
			System.out.println("error");
		}finally{
			if (num > 20){
				System.out.println("num>20 : " + num);
			}
			System.out.println("finally");
			num = 100;
			return num;
		}
	}
}

输出结果如下:

try
num>20 : 90
finally
100

分析:try中的return语句同样被拆分了,finally中的return语句先于try中的return语句执行,因而try中的return被”覆盖“掉了,不再执行。


参考文章链接:

https://blog.csdn.net/ns_code/article/details/17485221?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

https://blog.csdn.net/Next_Second/article/details/73090994

https://blog.csdn.net/jiangnan2014/article/details/22944075

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://hadoo666.top/archives/java中只有值传递的理解md