Java中的值传递和引用传递的理解(带例子、通俗易懂)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44296929/article/details/102681878

一、首先了解一些基本的概念:

1、Java不管是基本类型还是引用类型,参数传递的方式只有一种值传递而有两种表现:值传递引用传递,是因为对象的属性不同。

2、首先了解一下基本类型和引用类型在JVM内存中的存储方式:点击了解更多
基本类型:int a = 5;jvm会在栈中开辟一块空间存储变量a并赋值为5。
引用类型:Sample s = new Sample();JVM会在堆中开辟一块空间存储Sample对象,并在栈中开辟一块空间存储对象的引用s(存储Sample对象在堆中的地址),并将s指向堆中的Sample对象。
特殊类型:String(是一种特殊的引用类型),JVM做了一些优化处理(下面我们再来详细的解释)。

二、下面我们来举几个例子:

a、首先是在一个方法中传入一个基本类型的参数:输出结果为5

public class Demo01 {
	public static void main(String[] args) {
		int a =5;
		int[] arr={5};
		add(a);
		add(arr[0]);
		System.out.println(a);		//5
		System.out.println(arr[0]);	//5
	}
	public static void add(int a){
		a++;
	}
	public static void add(int [] arr){
		arr[0]++;
	}
}

分析一下上面的过程:(我们这里只说一下int a = 5 的过程 int [] arr ={5}其实是一样的原理)
1、首先执行main方法的主线程会先在栈中申请一块内存空间,然后分配给变量a并赋值为5。
2、然后执行add(int a)方法时,会将变量a复制一份,然后传入add方法中的方法体去执行。
3、复制的变量a并不是原来的变量,只不过值也是5而已。
4、方法结束,方法外打印a的值,由于原来的a并没有改变,所以输出的还是5。

b、再来看一下在方法中传入一个引用类型:输出结果为 after 和 1

public class Demo02 {
    public static void main(String[] args) {
        Person p = new Person();
        p.setName("before");
        p.setAge(0);
        changePerson(p);
        System.out.println("p.name="+p.getName());			//p.name=after
        System.out.println("p.age="+p.getAge());			//p.age=1 		
    }
 
    private static void changePerson(Person p) {
        p.setName("after");
        p.setAge(1);
    }
}
 
class Person{
    private int age;
    private String name;
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

我们来分析一下上面代码的运行过程:
1、JVM首先看方法区中有没有关于Person的*.class类、方法等信息,如果没有则将方法等类信息储存到方法区中,然后创建一个Person对象和一个对象的引用p,在堆中申请一块区域储存Person对象并将堆中的Person对象与方法区的class类信息等相关联,并在栈中申请一块区域存放p(存放Person对象在堆中的内存地址)指向堆中的Person对象,如果方法区中有该类的相关信息,则直接将堆中的对象和方法区的对象相关联。
2、此时我们调用p的setName方法和setAge方法会根据p引用找到堆中的Person对象,Person对象的类信息和方法等是存在方法区中的,Person对象去方法区中找到该类的相关方法然后执行,将Person的属性改为name:before,age:0。
3、然后调用changePerson方法时,虚拟机会复制一个引用p,然后将复制以后的p也指向堆中的Person对象,虽然是两个引用(在栈中分别占用一小块内存区域,但是他们的引用地址是相同的都是指向堆中的Person对象的)。
4、将复制后的p传入changePerson方法中,重复2的过程,将对象的属性改为age:1,name:after。
5、方法结束,方法外打印p中的变量值,因为p和复制后的p指向的都是堆中的同一个对象,所以复制到方法中的p改变了Person对象,所以最后输出的值也是改变的。

c、我们最后看一下在方法中传入String类型的参数:输出结果为:String:A、 StringBuffer:AA

public class Demo03 {
	public static void main(String[] args) {
		String s="A";
		StringBuilder ss=new StringBuilder("A");
		add(s);
		add(ss);
		System.out.println("s:"+s);		//A
		System.out.println("ss:"+ss);	//AA
	}
	public static void add(String s){
		s+="A";
	}
	public static void add(StringBuilder s){
		s.append("A");
	}
}

1)或许你会很诧异,有的人之前就是这么认为的基本类型是值传递,而引用类型是引用传递,那么为什么String的值没有变化呢,是String不是引用类型吗,显然不是,好,现在我们分析一下上面的过程:
首先说一下String,其实String创建对象的过程是比较复杂的,String s = “A”; 和 String s = new String(“A”);也是不同的,有兴趣可以了解一下:点击了解 String创建对象的详细过程,我们这里是第一种方法创建的:
1、首先执行main方法的线程会去方法区中的运行时常量池中查看是否有常量"A",有则直接将引用s(和上面对象一样也是s也存储在栈中)指向常量池中的"A",如果没有则先在常量池中添加常量"A",并将引用s指向"A",显然我们这里常量池中还没有A,所以创建一个常量A,并让在栈中s引用指向A。
2、此时调用add方法,将s传入add方法中,虚拟机会复制一份s,将复制的s传入add方法中,复制后的s和s是两个不同的引用(在栈中的地址不同但是值相同),都是指向常量池中的常量A。
3、这一步非常关键,进入方法中以后,A会变成AA,然后会在常量池中判断常量池中是否会有AA,显然没有,所以会将复制之后的引用s指向常量AA,结束方法。
4、方法结束后,在方法外打印s,因为s并没有改变,还是指向的是常量池中的A,所以会输出A。

2)那么为什么StringBuffer的值改变了呢?下面说下上面的代码内部实现过程(其实和其它的引用类型的对象相同,简单的描述一下):
StringBuffer是在Java5提出的,它和String不同的是,StringBuffer类的对象能够被多次修改而不产生未使用的对象,StringBuffer的内部封装了对字符串操作的方法。
1、首先创建一个StringBuffer对象,在堆中申请一块区域然后存放该对象,然后在栈中存放ss引用(值为StringBuffer在堆中的地址)并指向堆中的 StringBuffer对象。
2、调用StringBuffer的add方法,虚拟机会复制一份ss然后传入到add方法中,ss和复制后的ss值一样,都是指向堆中的StringBuffer对象。
3、传入以后调用append方法将StringBuffer的值变为AA,然后调用toString方法将返回给StringBuffer对象。
4、方法结束,方法外输出ss,因为ss也指向的是StringBuufer对象,所以会输出最后toString返回的值。

总结:
Java中的传递方式只有一种,就是值传递,只不过根据传入不同参数,传入的对象属性不同而有两种表现形式而已,所以不要泛泛的认为基本类型就是值传递,引用类型就是引用传递,那是不对的。

最后,如果文章对你有帮助,请点个赞吧,上面都是个人理解,如果有理解错的地方,请指正,谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_44296929/article/details/102681878