浅谈Java-String到底是值传递还是引用传递?

参数传递

如果你学过 C/C++应该很好理解,就是所谓的 "值传递""指针传递"
首先声明一点:Java 中的参数传递,只有值传递,没有所谓的引用传递。
所谓的这个,就大有学问了,主要分为了两种类型:基本数据类型和对象

基本数据类型

众所周知,Java 有 8 种基本数据类型,分别是 int、long、byte、short、float、double 、char 和 boolean。它们的“值”直接存储在栈中,每当作为参数传递时,都会将原始值复制一份新的出来,给方法用(也就是说,复制出来的值,在存储在方法栈中)。
值在被调用方法结束时从栈中清除。

举个例子:

public class Test {
	
	public static void main(String[] args) {
		Test t1 = new Test();
		int a = 1;
		t1.paramTest(1);
		System.out.println("main: " + a);
		
	}
	
	private void intParamTest(int a) {
		a++;
		System.out.println(a);
	}

}

输出结果:
2
main: 1
我们可以看到,虽然我们去调用了方法让 a 的值自增 1 ,但是 main 方法中的 a 还是 1
实践出真知:我们可以得出一个结论,基本数据类型的值传递,对原始的值是不会造成影响的

对象类型

对于对象来说,参数传递,也是值传递,只是这个,不再像基本数据类型一样,将对象拷贝到自己的方法栈中。
我们都知道对象是存储在堆中,在进行参数传递的时候,这个,是对象在堆中的引用
如果我们对 传递过来的对象进行修改,修改的就是堆中那个对象。
举个例子:

public class Test {
	
	public static void main(String[] args) {
		Test t1 = new Test();
		Person p = new Person("小冯同学",18);
		System.out.println("方法调用前:" + p.name);
		t1.personTest(p);
		System.out.println("方法调用后:" + p.name);
		
	}
	
	private void personTest(Person p) {
		p.name = "张三同学";
	}

}
class Person{
	public String name;
	public int age;
	public Person(String name,int age) {
		this.name = name;
		this.age = age;
	}
}

输出结果:
方法调用前:小冯同学
方法调用后:张三同学
看到这里,我们可以得出一个结论:对象作为参数的进行传递的时候,传递的值是对象的引用地址。

总结

根据以上两个结论,我们总结一下:

  • 当参数类型是基本数据类型时,传递的值是一个拷贝值,拷贝值是生是死不影响原来的值
  • 当参数类型是一个对象时,传递的值是对象的引用地址,操作的是同一个对象

分享一个小故事,大概是两年前刚开始接触 SSM框架的时候,当时有一个需求就是:插入一个 user,然后返回插入成功和 user的基本信息(包括 userid),然后我们呢当时数据库的 user表的 id自增的,插入的 user是没有 id的,id是插入后 mysql自增主键生成的,当时借助了 mybatis的主键回添功能,大概长这样子:

<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
    insert into t_user (u_name,u_age) values (#{name},#{age});
</insert>

然后之前插入的那条 user记录就有 id了,当时年少气盛的我还不懂什么值传递引用传递,当时的表情是这样的

属实是 Java 基础太拉了,哈哈
然后现在发现其实原理就这么简单,插入后把 id 查出来,直接塞到你之前插入的那个 user 对象中 o 了

关于 String

相信大家在面试场上,面试官经常会问你一些关于 String 的问题
然后你发现你之前背的结论不管用了(基本数据类型传递的值是一个拷贝,对象传递的值是引用
还是实践出真知:

public class Test {
	
	public static void main(String[] args) {
		Test t1 = new Test();
		String s = "hello";
		t1.append(s);
		System.out.println("main: " + s);
		
	}
	
	private void append(String s) {
		s += " world";
		System.out.println("method: " + s);
	}

}

输出结果:
method: hello world
main: hello
很多同学应该也是这个表情

“String 是对象啊,传递的不是它的引用吗,操作的不是真实的对象吗,为什么没有改变啊”

其实,String是一个很特殊的对象,你可以把 String 归类到基本数据类型中去
为什么会这样,还要从 String 的关键字 final 说起
大家都知道被 final 修饰的都是常量,常量有什么特点?当然是不能改变啊
所以我们对 String 进行改变的时候,编译器在底层是为我们重新生成了一个 String 对象,来看一道经典面试题:

	public static void main(String[] args) {
		String s1 = "a";
		String s2 = s1;
		s2 = "ab";
		String s3 = new String("ab");
		String s4 = s3 + "cd";
		String s5 = "abcd";
	}

请问这段程序一共生成了多少个对象?
有没有猜到 5 个的,正确答案就是 5 个。
池化技术了解过吧?线程池,对象池,人才池...
String 底层用到的就是 常量池
第二行:s1 = "a" ,生成常量 a 放入池中,然后 s1 指定这个常量的地址
第三行:s2 = s1 ,就是生成一个 s2 的变量指向 s1 的地址。
第四行:s2 = "ab",是把 "a" 变成的 "ab" 么?当然不是,常量当然不能被改变,那怎么办?当然是重新生成一个常量 "ab",放入池中,然后 s2 指向 "ab" 的地址
第五行: s3 = new String("ab"),使用了 new 关键字,所以会生成一个新的对象,但是 new String("ab") 中的 "ab"是重新生成的对象,放在堆中
第六行:池里没有
"cd" ,创建 "cd" 扔到池子里,s3 是 "ab" ,用加号连接起来就是 "abcd",池里没有 "abcd",创建 "abcd" 扔到池里
第七行:
s5 = "abcd",因为池里有 "abcd",所以直接复用就是,不用重新创建
所以一共生成了 5 个对象,分别是:
"a","ab","ab","cd","abcd"**
据我所知,Java 里用得最多的数据类型就是 String,如果不停的创建 String 对象,对内存来说应该是一笔很大的开销,使用常量池可以对其进行复用,算是对 String 的优化
可能不太准确,hhh,也欢迎大佬在下面评论
文笔不是很专业,但是希望每篇博客都能吸收到新的知识,认真沉淀~

2023.01.30 更新
==我是分隔线=
贴一张常量池里的常量:




这里有一个疑惑,这里的常量 cd,和我想象中的有点不一样,它前面有一个看不出来是什么的符号,占了三个字节
我新加了一行代码 String s6 = s2 + "cd";,发现复用了常量池中的 cd常量,我猜这个常量前面的特殊符号应该是一个标志,标志这是拼接产生的。

posted @ 2022-05-05 16:41  fengzeng  阅读(1847)  评论(7编辑  收藏  举报