最近在学习feign时,看了一些源码,其中有一段涉及到==的代码,于是复习了一下java中值传递和值引用的知识点
代码如下:
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
注意在if中开发人员使用“==”对两个对象进行比较,这里我比较费解,options是一个参数配置对象,里面记录的是一些参数配置,那么比较的应该是其内容才对。而实际比较其地址值,因此我先模拟传参过程,观察对象在传参过程中的变化
模拟代码如下:
public class MyTest {
private static final Person person = new Person("李四",20);
public void testOne(Person p){
System.out.println("入参p:" + p);
p = new Person("张三",18);
System.out.println("重新赋值后p:" + p);
}
public static void main(String[] args) {
MyTest m = new MyTest();
System.out.println("执行testOne前:" + person);
m.testOne(person);
System.out.println("执行testOne后:" + person);
}
public static class Person{
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
//省略get set
打印结果如下:
执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='张三', age=18}
执行testOne后:Person{name='李四', age=20}
根据打印结果可以得出以下结论:
1.person对象在进入testOne()方法前后,对象没有发生变化
2.person对象作为参数传递到testOne()方法内部后,person对象作为参数发生了变化
可能你认为private static final Person person = new Person("李四",20);这里的person对象使用final修饰的所以不会发生变化,那么我们不使用final修饰进行测试:
public class MyTest {
private Person person = new Person("李四",20);
public void testOne(Person p){
System.out.println("入参p:" + p);
p = new Person("张三",18);
System.out.println("重新赋值后p:" + p);
}
public static void main(String[] args) {
MyTest m = new MyTest();
System.out.println("执行testOne前:" + m.getPerson());
m.testOne(m.getPerson());
System.out.println("执行testOne后:" + m.getPerson());
}
结果如下:
执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='张三', age=18}
执行testOne后:Person{name='李四', age=20}
可以看到得到的结论是一样的。那么为什么会有这样的结果呢,这就需要读者具备一定的java基础知识,了解java对象是如何在内存中存储的。简单来说,对于引用数据类型,也就是对象来说,初始化一个对象,比如:
Person person = new Person("李四",20);
创建对象时,会在栈中创建变量person,在堆中初始化对象,栈中person变量持有对堆中("李四",20)对象的引用,也就是我们常说的,变量持有地址值。使用变量person,就可以获取到堆中对象的内容name,age。那么当变量person被当做参数传递进一个方法后会发生什么,传递的内容是什么,我们用两张图来表示:
这是new对象时:
这是对象作为参数传入方法时:
当对象作为参数传入方法时,实际传入的是person对象的一个副本,传入方法的是对与对象的引用,因此你可以看到当参数进入方法时,打印出来的与传入前是一致的,因为他们持有相同的引用,获取的内容是相同的
执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
此时对变量进行的set,get操作,实际上是对变量持有引用的对象进行操作,比如这时候去修改对象内部属性值:
public void testTwo(Person p){
System.out.println("入参p:" + p);
p.setName("王二麻子");
p.setAge(25);
System.out.println("重新赋值后p:" + p);
}
public static void main(String[] args) {
MyTest m = new MyTest();
System.out.println("执行testOne前:" + m.getPerson());
// m.testOne(m.getPerson());
m.testTwo(m.getPerson());
System.out.println("执行testOne后:" + m.getPerson());
}
结果如下:
执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='王二麻子', age=25}
执行testOne后:Person{name='王二麻子', age=25}
但是为什么之前对p重新赋值并没有效果呢?因为传入person后,执行了
p = new Person("张三",18);
这段代码在内存中的效果如图所示
于是我们看到了测试的结果,在方法内部打印person,对象属性已经发生了变化,在方法外部打印person,没有改变原来的person对象,因为一旦内部new了新的对象,person副本持有的引用发生了变化,与原对象的引用不再有任何关系,而方法结束时,person副本消亡,并没有对原对象进行任何操作。
执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='张三', age=18}
执行testOne后:Person{name='李四', age=20}
需要注意的时,这里person对象作为参数传入了方法,实际传入的是一个person对象的副本,他持有堆中对于person对象的引用,注意在没有改变这个引用前,对于person的操作,比如get,set仍旧是对原对象的内容进行操作。一旦这个引用发生了变化,对原对象就不会有影响了,这也正是上面两个测试方法产生不同结果的原因,我们可以得出结论当一个对象作为参数传入方法时,实际传递的是对于对象的引用,可以理解为引用传递,那么java中的值传递又是什么呢?我们使用基本数据类型int测试
测试代码:
public void testThree(int a){
System.out.println("入参a:" + a);
a = 20;
System.out.println("重新赋值后a:" + a);
}
public static void main(String[] args) {
MyTest m = new MyTest();
int b = 100;
System.out.println("执行testThree前:" + b);
m.testThree(b);
System.out.println("执行testThree后:" + b);
}
结果如下:
执行testThree前:100
入参a:100
重新赋值后a:20
执行testThree后:100
其实结论与上面相似,传入的是副本,方法内部改变的是副本值,方法结束,副本消亡,原来的变量的值不受影响,我一位同事用一句话来总结,方法只能改变方法内部的东西,外部的是没有办法改变的
《java编程思想》中有一句话描述引用传递很有意思。解释一下:文中的句柄说的就是引用
“12.2 制作本地副本
稍微总结一下:Java中的所有自变量或参数传递都是通过传递句柄进行的。也就是说,当我们传递“一个对象”时,实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修改,便相当于修改外部对象。”
同时《java核心技术卷I中》中也指出,java中任何传递都是按值传递的,只不过基本数据类型传递的是数值,而引用数据类型传递的是地址值。
在java核心技术卷I中也对此有描述,内容如下:
“按值调用表示方法接受到的是调用者提供的值。按引用调用表示方法接受到的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值”
事实上java核心技术卷I对值传递的描述就非常准确了,他直接指出了,值传递与引用传递的本质区别,即在方法内部,对变量的影响
总结一下
- java中对于基本数据类型都是值传递,传递的是一个副本值,副本在方法执行时生效,方法结束,副本消亡,原来的变量不会发生变化
- java中对于引用数据类型,传递的是其变量的副本,该副本持有与原变量对对象相同的引用,简单来说传递的是引用值
- 在引用值不发生变化时,可以访问对象内部,对对象进行操作,但需要注意,这个传入的参数仍旧是副本,并不是原对象,只是因为引用值相同,他可以对原对象进行访问
- 当副本持有的引用值发生变化时,与原对象已经没有关系,方法结束,副本消亡,不会对对象有影响