引用
开始
直接上代码
class Person {
String name;
int money;
}
class Client {
public static void main(String[] args) {
// Create a person named Bob and he has no money.
Person person = new Person();
person.name = "Bob";
person.money = 0;
// Check the person, if he has no money, set it as null
check(person);
// If the person turned to null, print he has no money, otherwise print he's rich
if (person == null) {
System.out.println(person.name + " has no money.");
} else {
System.out.println(person.name + " is rich.");
}
}
private static void check(Person person) {
if (person.money <= 0) {
person = null;
}
}
}
这里有一个 Person 对象,包含两个字段,name 和 money;在main函数中创建一个person对象,名字叫 Bob,拥有的钱为0。
接着调用了一个 check() 方法。这个方法的作用是:检查这个人身上的钱,如果这个人没有钱,那么就将其置为 null,可以看到,刚才设置的 money 是0,所以这个 person 对象在 check() 方法中就会被置为 null
最后,如果 person 对象为 null,就会输出 Bob has no money.。从刚才的逻辑捋下来,这里一定会输出 Bob has no money.才对。但这就是最奇怪的地方,每次运行代码,输出结果都是 Bob is rich.
验证
有人就会问,你有没有检查过,在 main() 函数中的 person 对象,和你传入 check() 函数的对象是一样的吗?
我在调用 check() 函数之前,先输出了 person 对象的地址,然后在 check() 函数中, 再次打印了传入的 person 参数的地址。
class Person {
String name;
int money;
}
class Client {
public static void main(String[] args) {
// Create a person named Bob and he has no money.
Person person = new Person();
person.name = "Bob";
person.money = 0;
System.out.println("Prepare to check the person: " + person + ", his money is: " + person.money);
// Check the person, if he has no money, set it as null
check(person);
// If the person turned to null, print he has no money, otherwise print he's rich
if (person == null) {
System.out.println(person.name + " has no money.");
} else {
System.out.println(person.name + " is rich.");
}
}
private static void check(Person person) {
System.out.println("Start to check the person: " + person + ", his money is: " + person.money);
if (person.money <= 0) {
person = null;
}
}
}
Prepare to check the person: com.example.myapplication.Person@5c3bd550, his money is: 0
Start to check the person: com.example.myapplication.Person@5c3bd550, his money is: 0
Bob is rich.
可以看到,两者的内存地址是一模一样的,而且我还特意打印了 Bob 的 money 字段,一直都是0,并没有变化。
后来我又试着在 check() 函数中修改 person 对象的字段,以再次检验传入的对象和 main() 函数中的对象是不是同一个
class Client {
public static void main(String[] args) {
...
// Check the person, if he has no money, set it as null
check(person);
// If the person turned to null, print he has no money, otherwise print he's rich
if (person == null) {
System.out.println(person.name + " has no money.");
} else {
System.out.println(person.name + " is rich.");
}
}
private static void check(Person person) {
person.name = "Charlie";
...
}
}
Charlie is rich.
我在 check() 函数中把 person 对象的 name 从 Bob 改成 Charlie,最终 main() 函数中输出了 Charlie is rich.。这更说明了两者是一模一样的,check() 函数中的改动能够反映到 main() 函数中。
解析
如果两者是一模一样的,为什么 check() 函数中将 person 对象置为 null 之后,main() 函数中的对象却没有被置为 null 呢?
这涉及到 Java 值传递的特性。在 Java 中,创建出的对象位于内存中,占据内存的一小块。在代码中传递对象时,程序不是把这块内存区域传来传去,而是传递这块内存区域的地址,只要程序知道内存地址在哪里,就能正确地读写这块区域了
这就是为什么main函数和check函数的person对象的内存地址是一样的了,在java中,向函数中传递参数时,Java 会把内存地址拷贝一份再传递。
举个通俗的例子,对象占用的内存就像一间房子。进入这间房子需要钥匙,钥匙就是内存地址。当需要把对象传递给函数时,Java 会把钥匙复制一把,然后把复制的钥匙交给函数。
所以这个函数拿着这把复制的钥匙也能进入这间房子,也能修改里面的家具、摆件,也就是修改里面的字段。但是这个函数如果把钥匙丢掉,也就是把这个内存地址置为 null,是不会影响其他地方的。因为它拿到的只是一把复制的钥匙而已。
这就叫做值传递。与之相对的,另一种传值方式被称为引用传递。引用传递是指在调用方法时将实际参数的地址直接传递到方法中,那么在方法中对参数所进行的修改,将影响到实际参数。Java 中只有值传递,没有引用传递。
小结
当参数类型是基本数据类型时,传递的是实参的值,因此无法对实参进行修改。
当参数类型是非基本数据类型时,传递的是实参内存地址的拷贝,此时形参和实参都可以对此对象的字段进行修改,但是互相无法影响对方本身。