Java值传递

引用和对象在JVM内存中分布结构

在Java中,创建的实体对象在堆内存中开辟空间,而引用对象在栈内存中开辟空间。
基本数据类型、引用对象在栈内存中开辟空间。
引用对象存储的是实体对象的地址。

Thinking In Java 3版的第二章

Java里存放数据大都放在栈和堆里面。
引用和基本类型放在栈里面,因为这样会比较快。对象内容放在堆里面。
但是引用所在栈里面存放的是它对应的对象所在堆的地址,栈里的值,也就是堆的地址。
其实用引用作为参数说是值传递(堆中值的传递)和地址传递都可以。只是理解的角度不同。

基本数据类型作为参数传递的案例分析

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package valueParam;

public class Demo1 {
public static void main(String[] args) {
Double num = new Double(5);
System.out.println("num hashcode main="+num.hashCode());
System.out.println(num);
changeValue(num);
System.out.println(num);
}

public static void changeValue(Double x) {
System.out.println("x hashcode before="+x.hashCode());
x = x * 2;
System.out.println("x hashcode after="+x.hashCode());
}
}
1
2
3
4
5
num hashcode main=1075052544
5.0
x hashcode before=1075052544
x hashcode after=1076101120
5.0

基本数据类型(装箱和拆箱)作为参数传递,值不会发生改变。StringIntegerDoubleFloat都是final类型的类,是不可以变。
changeValue方法参数x的值,是在栈中创建的一个引用对象,x的引用对象指向的地址和num引用对象指向同一个地址(num=5的值,赋值给x)。因为Integer不可变,$x=x2$的时候,新建一个Integer的堆对象,这时changeValue的x栈对象指向这个$x=x2$堆对象。
所以,main()中的num栈对象还是指向原来的堆对象,num值不会发生改变,改变的只是changeValue()内x堆对象的值。看hashcode值,前后发生了变化。

如果是基本数据类型(intfloatdouble),main()double num=5changeValue(double x)。这种情况,因为基本数据类型都是在栈中创建的,即num在栈中创建存储单元值(5),把这值(5)赋值给x变量在栈中创建的存储单元,x和num都指向栈中的值5,但这是不同的存储单元。

例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package valueParam;

public class Demo2 {
String str = new String("good");
char[] ch = { 'a', 'b', 'c' };

public static void main(String args[]) {
Demo2 ex = new Demo2();
System.out.println("ex.str.hashcode="+ex.str.hashCode());
ex.change(ex.str, ex.ch);
System.out.println("str:"+ex.str);
System.out.println(ex.ch);
}

public void change(String str, char ch[]) {
System.out.println("str.hashcode before="+str.hashCode());
str = "test ok";
System.out.println("str.hashcode after="+str.hashCode());

ch[0] = 'g';
}
}
1
2
3
4
5
ex.str.hashcode=3178685
str.hashcode before=3178685
str.hashcode after=-1422516182
str:good
gbc

String是不可变的,所以在test ok新建一个String对象,Main#strchange#str是指向不同的堆对象地址,change#str的改变,不会影响main#str的值。
ch数组是引用对象,ch[0] = 'g';这一句,意思把内部ch指向的数据区域(也就是实际存放数组内容的地方)里面的第一个字符改成g,还是在原来指向的数据区域上操作,并没有改变内部ch的数据地址,所以这个修改也会反映到外部的ch。
数组是引用数据类型(非基本数据类型),数组就是一个用来存储一系列变量值的命名区域,因此可以用数组来组织变量,其实,数组也是一个变量,它存储的是相同类型的一组数据。

引用对象作为参数传递的案例分析

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package valueParam;

public class Demo5 {
// 定义一个改变对象属性的方法
public static void changeName(Person p) {
p.setName("Rose");
}
public static void main(String[] args) {
// 定义一个Person对象,person是这个对象的引用
Person person = new Person();
// 先显示这个对象的name属性
System.out.println(person.getName());
// 调用changeName(Person p)方法
changeName(person);
// 再显示这个对象的name属性,看是否发生了变化
System.out.println(person.getName());
}
}

class Person {
private String name = "Jack";

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
1
2
Jack
Rose


main()new一个对象Person,实际分配了两个对象:
堆:新创建的Person类的实体对象。
栈:指向该对象的引用对象person
changeName()中,在栈中新建一个引用对象p,引用对象p指向main()中创建的堆对象persion
【注意:在java中,新创建的实体对象在堆内存中开辟空间,而引用变量在栈内存中开辟空间】
如上图所示,左侧是堆空间,用来分配内存给新创建的实体对象,红色框是新建的Person类的实体对象,000012是该实体对象的起始地址;而右侧是栈空间,用来给引用变量和一些临时变量分配内存,新实体对象的引用person就在其中,可以看到它的存储单元的内容是000012,记录的正是新建Person类实体对象的起始地址,也就是说它指向该实体对象。
调用了changeName()方法,person引用对象的将自己的存储单元传递给p引用对象(person和p指向同一个堆空间地址),也就是将实体对象的地址传给了p引用对象。在changeName()方法中对p的一切操作都是针对p所指向的这个存储单元(堆空间的地址)。
那为什么对象内部能够发生变化呢?
那是p所指向的那个存储单元中的内容是实体对象的地址,使得p也指向了该实体对象,所以才能改变对象内部的属性!
这也是我们大多数人会误以为是“引用传递”的终极原因!

例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package valueParam;

public class Demo3 {
public static void main(String[] args) {
/**
* Test 1: Methods can't modify numeric parameters
* 基本数据类型
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);

/**
* Test 2: Methods can change the state of object parameters
*/
System.out.println("\nTesting tripleSalary:");
Employee harry = new Employee("Harry", 50000);
System.out.println("Before: "+harry.hashCode()+" salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());

/**
* Test 3: Methods can't attach new objects to object parameters
*/
System.out.println("\nTesting swap:");
Employee a = new Employee("Alice", 70000);
Employee b = new Employee("Bob", 60000);
System.out.println("Before: "+a.hashCode()+" a=" + a.getName());
System.out.println("Before: "+b.hashCode()+" b=" + b.getName());
swap(a, b);
System.out.println("After: "+a.hashCode()+" a=" + a.getName());
System.out.println("After: "+b.hashCode()+" b=" + b.getName());
}

private static void swap(Employee x, Employee y) {
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: "+x.hashCode()+" x=" + x.getName());
System.out.println("End of method: "+y.hashCode()+" y=" + y.getName());
}

private static void tripleSalary(Employee x) {
x.raiseSalary(200);
System.out.println("End of method: "+x.hashCode()+" salary=" + x.getSalary());
}

private static void tripleValue(double x) {
x = 3 * x;
System.out.println("End of Method X= " + x);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Testing tripleValue:基本数据类型,在栈开辟独立的存储单元,2个栈对象操作独立的存储单元,相互不影响;在tripleValue方法中,percent栈对象值赋值给x栈对象。
Before: percent=10.0
x End = 30.0
After: percent=10.0

Testing tripleSalary:栈引用对象harry和x都是指向同一个堆地址。在tripleSalary方法中,栈对象x对堆中实体对象的修改,直接影响harry引用对象指向堆对象的值。
harry Before hashCode: 581409841 salary=50000
x End hashCode: 581409841 salary=10000000
harry After hashCode: 581409841 salary=10000000

Testing swap:栈对象a、b指向2个不同的堆对象空间,在swap方法内,新建2个栈对象x、y指向栈对象a、b所指的堆对象。那么a,x和b,y都是指向同一个堆对象,那么x,y栈对象的值进行交换,并不会影响a和b,只是x,y所指的堆对象地址变化。如果,x和y对堆对象进行操作,必然会影响到a和b获取的值。
Before: 704603837 a=Alice
Before: 1051858901 b=Bob
x End hashCode: 1051858901 x=Bob
y End hashCode: 704603837 y=Alice
After: 704603837 a=Alice
After: 1051858901 b=Bob

例子3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package valueParam;

public class Demo4 {
static void swap(StringBuffer a2, StringBuffer b2) {
a2.append(" more");
System.out.println("--------a-----------" + a2); // One more
System.out.println("--------b-----------" + b2); // Two
b2 = a2;
System.out.println("--------b-----------" + b2); // One more
}

public static void main(String args[]) {
StringBuffer a1 = new StringBuffer("One");
StringBuffer b1 = new StringBuffer("Two");
swap(a1, b1);
System.out.println("a is " + a1 + "\nb is " + b1);
}
}
1
2
3
4
5
--------a-----------One more
--------b-----------Two
--------b-----------One more
a is One more
b is Two

b2 = a2;这里是指b2指向a2的堆地址,对应main中的b1并没有影响。

总结

Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个复制,指向同一个堆地址。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用是永远不会改变的。
Java程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题,传递的是栈对象。不会直接传堆对象本身。
Java中方法参数传递方式是按值传递。如果参数是基本类型,传递的是基本类型的字面量值的拷贝。如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。(实际上创建2个栈对象)
值传递的精髓是:传递的是存储单元中的内容,而非地址或者引用!
值传递和引用传递的参数类型是值类型还是引用类型无关。对于值传递,无论是值类型还是引用类型,都会在调用栈上创建一个副本,不同是,对于值类型而言,这个副本就是整个原始值的复制。
而对于引用类型而言,由于引用类型的实例在堆中,在栈上只有它的一个引用(一般情况下是指针),其副本也只是这个引用的复制,而不是整个原始对象的复制。
值类型和引用类型(这不是在说值传递)的最大区别:值类型用做参数会被复制,但是很多人误以为这个区别是值类型的特性。其实这是值传递带来的效果,和值类型本身没有关系。
真正的引用传递(pass by reference)是指当引用传递给函数时,被调用的函数获得的是对原值的引用,而非原值的副本。如果函数修改了参数值,那么调用代码中的值也会改变,那是因为引用和参数使用了内存中的同一块地址。

# Java

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×